feat: Pounce listings in acquire table, yield remove button, portfolio shows yield status
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:09:51 +01:00
parent 99ccfbd23f
commit f963b33b32
9 changed files with 278 additions and 189 deletions

View File

@ -96,7 +96,7 @@ export default function AboutPage() {
</li> </li>
<li className="flex justify-between border-b border-white/5 pb-2"> <li className="flex justify-between border-b border-white/5 pb-2">
<span>Refresh Rate</span> <span>Refresh Rate</span>
<span className="text-white">Real-time</span> <span className="text-white">Live</span>
</li> </li>
<li className="flex justify-between"> <li className="flex justify-between">
<span>Data Source</span> <span>Data Source</span>
@ -114,7 +114,7 @@ export default function AboutPage() {
{[ {[
{ icon: Target, title: 'Precision', desc: 'Accurate data. No guesswork. Every check counts.' }, { icon: Target, title: 'Precision', desc: 'Accurate data. No guesswork. Every check counts.' },
{ icon: Shield, title: 'Privacy', desc: 'Your strategy stays yours. We never share or sell data.' }, { icon: Shield, title: 'Privacy', desc: 'Your strategy stays yours. We never share or sell data.' },
{ icon: Zap, title: 'Speed', desc: 'Real-time intel. You see it first.' }, { icon: Zap, title: 'Speed', desc: 'Fast intel. You move first.' },
{ icon: Users, title: 'Transparency', desc: 'Clear pricing. No surprises. Ever.' }, { icon: Users, title: 'Transparency', desc: 'Clear pricing. No surprises. Ever.' },
].map((value, i) => ( ].map((value, i) => (
<div <div

View File

@ -63,6 +63,7 @@ interface Auction {
age_years: number | null age_years: number | null
tld: string tld: string
affiliate_url: string affiliate_url: string
is_pounce?: boolean
} }
type TabType = 'all' | 'ending' | 'hot' type TabType = 'all' | 'ending' | 'hot'
@ -206,10 +207,32 @@ export default function AcquirePage() {
return endMs > (nowMs - 2000) // 2s grace return endMs > (nowMs - 2000) // 2s grace
}) })
if (isAuthenticated) return activeAuctions // Convert Pounce Direct items to Auction format and mark them
return activeAuctions.filter(isVanityDomain) const pounceAuctions: Auction[] = pounceItems.map(item => ({
domain: item.domain,
platform: 'Pounce',
platform_url: item.url,
current_bid: item.price,
currency: item.currency,
num_bids: 0,
end_time: '',
time_remaining: '',
buy_now_price: item.price,
reserve_met: null,
traffic: null,
age_years: null,
tld: item.tld,
affiliate_url: item.url,
is_pounce: true, // Special flag
}))
// Apply auth filter
const filteredAuctions = isAuthenticated ? activeAuctions : activeAuctions.filter(isVanityDomain)
// Pounce Direct always on top
return [...pounceAuctions, ...filteredAuctions]
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeTab, allAuctions, endingSoon, hotAuctions, isAuthenticated, tick]) }, [activeTab, allAuctions, endingSoon, hotAuctions, pounceItems, isAuthenticated, tick])
const filteredAuctions = displayAuctions.filter(auction => { const filteredAuctions = displayAuctions.filter(auction => {
if (searchQuery && !auction.domain.toLowerCase().includes(searchQuery.toLowerCase())) return false if (searchQuery && !auction.domain.toLowerCase().includes(searchQuery.toLowerCase())) return false
@ -407,27 +430,49 @@ export default function AcquirePage() {
href={auction.affiliate_url} href={auction.affiliate_url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="block p-3 bg-[#0A0A0A] border border-white/[0.08] active:bg-white/[0.03] transition-all" className={clsx(
"block p-3 border active:bg-white/[0.03] transition-all",
auction.is_pounce
? "bg-accent/[0.03] border-accent/20"
: "bg-[#0A0A0A] border-white/[0.08]"
)}
> >
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="text-sm font-bold text-white font-mono truncate"> <div className="flex items-center gap-2">
{auction.domain} {auction.is_pounce && (
<span className="flex items-center gap-0.5 px-1 py-0.5 bg-accent/10 border border-accent/20 text-[8px] font-bold text-accent uppercase shrink-0">
<Diamond className="w-2.5 h-2.5" /> Direct
</span>
)}
<span className="text-sm font-bold text-white font-mono truncate">
{auction.domain}
</span>
</div> </div>
<div className="flex items-center gap-2 mt-1 text-[10px] font-mono text-white/30"> <div className="flex items-center gap-2 mt-1 text-[10px] font-mono text-white/30">
<span className="uppercase">{auction.platform}</span> {auction.is_pounce ? (
<span></span> <span className="flex items-center gap-1 text-accent">
<span className={getTimeColor(auction.end_time)}> <ShieldCheck className="w-3 h-3" /> Verified Instant
<Clock className="w-3 h-3 inline mr-1" /> </span>
{calcTimeRemaining(auction.end_time)} ) : (
</span> <>
<span className="uppercase">{auction.platform}</span>
<span></span>
<span className={getTimeColor(auction.end_time)}>
<Clock className="w-3 h-3 inline mr-1" />
{calcTimeRemaining(auction.end_time)}
</span>
</>
)}
</div> </div>
</div> </div>
<div className="text-right shrink-0"> <div className="text-right shrink-0">
<div className="text-sm font-bold text-accent font-mono"> <div className="text-sm font-bold text-accent font-mono">
{formatCurrency(auction.current_bid)} {formatCurrency(auction.current_bid)}
</div> </div>
{auction.num_bids > 0 && ( {auction.is_pounce ? (
<div className="text-[10px] text-accent/60 font-mono">Buy Now</div>
) : auction.num_bids > 0 && (
<div className="text-[10px] text-white/30 font-mono"> <div className="text-[10px] text-white/30 font-mono">
{auction.num_bids} bids {auction.num_bids} bids
</div> </div>
@ -501,94 +546,53 @@ export default function AcquirePage() {
</div> </div>
)} )}
{/* Featured Direct Listings */}
{pounceItems.length > 0 && (
<div className="mb-16">
<div className="flex items-center justify-between gap-4 mb-6">
<div className="flex items-center gap-3">
<div className="flex items-center gap-2 px-3 py-1 bg-accent/10 border border-accent/20">
<Diamond className="w-4 h-4 text-accent" />
<span className="text-xs font-bold uppercase tracking-widest text-accent">Direct Listings</span>
</div>
<span className="text-[10px] font-mono text-white/30">// 0% COMMISSION</span>
</div>
</div>
<div className="grid grid-cols-3 gap-4">
{pounceItems.slice(0, 3).map((item) => (
<Link
key={item.id}
href={item.url}
className="group border border-white/10 bg-[#050505] hover:border-accent/50 transition-all p-6"
>
<div className="flex items-center gap-2 mb-4">
<span className="w-1.5 h-1.5 bg-accent animate-pulse" />
<span className="text-[10px] font-bold uppercase tracking-widest text-white/40">Available</span>
</div>
<h3 className="font-mono text-xl text-white font-medium mb-4 truncate group-hover:text-accent transition-colors">
{item.domain}
</h3>
<div className="flex items-center justify-between">
<span className="font-mono text-lg text-accent">{formatCurrency(item.price)}</span>
{item.verified && (
<span className="flex items-center gap-1 text-[10px] text-accent">
<ShieldCheck className="w-3 h-3" /> Verified
</span>
)}
</div>
</Link>
))}
</div>
</div>
)}
{/* Search & Filters Bar */} {/* Search & Filters Bar */}
<div className="mb-6 sticky top-20 z-30 backdrop-blur-xl bg-[#020202]/90 border-y border-white/[0.08] py-4 -mx-6 px-6"> <div className="mb-6 sticky top-20 z-30 backdrop-blur-xl bg-[#020202]/90 border-y border-white/[0.08] py-4 -mx-6 px-6">
<div className="flex gap-4 justify-between items-center"> <div className="flex gap-4 justify-between items-center">
{/* Search */} {/* Search */}
<div className="relative w-[400px] group"> <div className="relative w-[400px] group">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-white/40 group-focus-within:text-accent transition-colors" /> <Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-white/40 group-focus-within:text-accent transition-colors" />
<input <input
type="text" type="text"
placeholder="Search assets..." placeholder="Search assets..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-12 pr-4 py-3 bg-[#0A0A0A] border border-white/10 text-white placeholder:text-white/20 font-mono text-sm focus:outline-none focus:border-accent transition-all" className="w-full pl-12 pr-4 py-3 bg-[#0A0A0A] border border-white/10 text-white placeholder:text-white/20 font-mono text-sm focus:outline-none focus:border-accent transition-all"
/> />
</div> </div>
{/* Filters */} {/* Filters */}
<div className="flex gap-2"> <div className="flex gap-2">
<select <select
value={selectedPlatform} value={selectedPlatform}
onChange={(e) => setSelectedPlatform(e.target.value)} onChange={(e) => setSelectedPlatform(e.target.value)}
className="appearance-none px-4 py-3 bg-[#0A0A0A] border border-white/10 text-white font-mono text-sm focus:outline-none focus:border-accent cursor-pointer" className="appearance-none px-4 py-3 bg-[#0A0A0A] border border-white/10 text-white font-mono text-sm focus:outline-none focus:border-accent cursor-pointer"
> >
{PLATFORMS.map(p => <option key={p.id} value={p.id}>{p.name}</option>)} {PLATFORMS.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
</select> </select>
<div className="flex border border-white/10 bg-[#0A0A0A]"> <div className="flex border border-white/10 bg-[#0A0A0A]">
{[ {[
{ id: 'all' as const, label: 'All', icon: Gavel }, { id: 'all' as const, label: 'All', icon: Gavel },
{ id: 'ending' as const, label: 'Ending', icon: Timer }, { id: 'ending' as const, label: 'Ending', icon: Timer },
{ id: 'hot' as const, label: 'Hot', icon: Flame }, { id: 'hot' as const, label: 'Hot', icon: Flame },
].map((tab) => ( ].map((tab) => (
<button <button
key={tab.id} key={tab.id}
onClick={() => setActiveTab(tab.id)} onClick={() => setActiveTab(tab.id)}
className={clsx( className={clsx(
"px-5 py-3 flex items-center gap-2 text-xs font-bold uppercase tracking-widest transition-all border-r border-white/10 last:border-r-0", "px-5 py-3 flex items-center gap-2 text-xs font-bold uppercase tracking-widest transition-all border-r border-white/10 last:border-r-0",
activeTab === tab.id activeTab === tab.id
? "bg-white/10 text-white border-b-2 border-accent" ? "bg-white/10 text-white border-b-2 border-accent"
: "text-white/40 hover:text-white border-b-2 border-transparent" : "text-white/40 hover:text-white border-b-2 border-transparent"
)} )}
> >
<tab.icon className={clsx("w-4 h-4", activeTab === tab.id ? "text-accent" : "")} /> <tab.icon className={clsx("w-4 h-4", activeTab === tab.id ? "text-accent" : "")} />
{tab.label} {tab.label}
</button> </button>
))} ))}
</div>
</div> </div>
</div>
</div> </div>
</div> </div>
@ -604,43 +608,68 @@ export default function AcquirePage() {
</div> </div>
{/* Table Body */} {/* Table Body */}
{loading ? ( {loading ? (
<div className="flex items-center justify-center py-20"> <div className="flex items-center justify-center py-20">
<Loader2 className="w-6 h-6 text-accent animate-spin" /> <Loader2 className="w-6 h-6 text-accent animate-spin" />
</div> </div>
) : filteredAuctions.length === 0 ? ( ) : filteredAuctions.length === 0 ? (
<div className="text-center py-20 text-white/30 font-mono">No assets found</div> <div className="text-center py-20 text-white/30 font-mono">No assets found</div>
) : ( ) : (
<div> <div>
{filteredAuctions.map((auction, i) => ( {filteredAuctions.map((auction, i) => (
<a <a
key={`${auction.domain}-${i}`} key={`${auction.domain}-${i}`}
href={auction.affiliate_url} href={auction.affiliate_url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="grid grid-cols-[1fr_100px_120px_100px_80px] gap-4 items-center px-6 py-4 border-b border-white/[0.03] hover:bg-white/[0.02] transition-all group" className={clsx(
"grid grid-cols-[1fr_100px_120px_100px_80px] gap-4 items-center px-6 py-4 border-b transition-all group",
auction.is_pounce
? "border-accent/20 bg-accent/[0.03] hover:bg-accent/[0.06]"
: "border-white/[0.03] hover:bg-white/[0.02]"
)}
> >
<div className="font-mono text-base text-white group-hover:text-accent transition-colors truncate"> <div className="flex items-center gap-3">
{auction.domain} {auction.is_pounce && (
<div className="flex items-center gap-1 px-1.5 py-0.5 bg-accent/10 border border-accent/20 shrink-0">
<Diamond className="w-3 h-3 text-accent" />
<span className="text-[8px] font-bold text-accent uppercase">Direct</span>
</div>
)}
<span className="font-mono text-base text-white group-hover:text-accent transition-colors truncate">
{auction.domain}
</span>
</div> </div>
<div className="text-center text-xs font-mono text-white/40 uppercase"> <div className="text-center text-xs font-mono text-white/40 uppercase">
{auction.platform} {auction.is_pounce ? (
<span className="flex items-center justify-center gap-1 text-accent">
<ShieldCheck className="w-3 h-3" /> Verified
</span>
) : auction.platform}
</div> </div>
<div className="text-right font-mono text-base text-accent"> <div className="text-right font-mono text-base text-accent">
{formatCurrency(auction.current_bid)} {formatCurrency(auction.current_bid)}
{auction.is_pounce && (
<div className="text-[9px] text-accent/60 font-mono">Buy Now</div>
)}
</div> </div>
<div className={clsx("text-center text-xs font-mono", getTimeColor(auction.end_time))}> <div className={clsx("text-center text-xs font-mono", auction.is_pounce ? "text-accent" : getTimeColor(auction.end_time))}>
{calcTimeRemaining(auction.end_time)} {auction.is_pounce ? 'Instant' : calcTimeRemaining(auction.end_time)}
</div> </div>
<div className="flex justify-end"> <div className="flex justify-end">
<div className="w-8 h-8 border border-white/10 flex items-center justify-center text-white/30 group-hover:bg-white group-hover:text-black transition-all"> <div className={clsx(
"w-8 h-8 border flex items-center justify-center transition-all",
auction.is_pounce
? "border-accent/30 text-accent group-hover:bg-accent group-hover:text-black"
: "border-white/10 text-white/30 group-hover:bg-white group-hover:text-black"
)}>
<ExternalLink className="w-4 h-4" /> <ExternalLink className="w-4 h-4" />
</div> </div>
</div> </div>
</a> </a>
))} ))}
</div> </div>
)} )}
</div> </div>
{/* Stats Footer */} {/* Stats Footer */}
@ -657,18 +686,18 @@ export default function AcquirePage() {
<div className="bg-[#080808] p-10 text-left"> <div className="bg-[#080808] p-10 text-left">
<div className="inline-flex items-center justify-center w-10 h-10 border border-accent/20 bg-accent/5 text-accent mb-4"> <div className="inline-flex items-center justify-center w-10 h-10 border border-accent/20 bg-accent/5 text-accent mb-4">
<Filter className="w-5 h-5" /> <Filter className="w-5 h-5" />
</div> </div>
<h3 className="text-xl font-display text-white mb-2">Eliminate Noise.</h3> <h3 className="text-xl font-display text-white mb-2">Eliminate Noise.</h3>
<p className="text-white/50 mb-6 max-w-md text-sm"> <p className="text-white/50 mb-6 max-w-md text-sm">
Our Trader plan filters 99% of junk domains automatically. Our Trader plan filters 99% of junk domains automatically.
</p> </p>
<Link <Link
href="/pricing" href="/pricing"
className="inline-flex items-center gap-2 px-6 py-3 bg-accent text-black text-xs font-bold uppercase tracking-widest hover:bg-white transition-all" className="inline-flex items-center gap-2 px-6 py-3 bg-accent text-black text-xs font-bold uppercase tracking-widest hover:bg-white transition-all"
> >
Upgrade <TrendingUp className="w-4 h-4" /> Upgrade <TrendingUp className="w-4 h-4" />
</Link> </Link>
</div> </div>
</div> </div>
)} )}
</div> </div>

View File

@ -149,7 +149,7 @@ export async function GET(request: NextRequest) {
letterSpacing: '0.02em', letterSpacing: '0.02em',
}} }}
> >
Domain Intelligence Real-time Market Data Domain Intelligence Pricing Intel
</div> </div>
</div> </div>
</div> </div>

View File

@ -7,7 +7,7 @@ export async function generateTLDMetadata(tld: string, price?: number, trend?: n
const trendText = trend ? (trend > 0 ? `+${trend.toFixed(1)}%` : `${trend.toFixed(1)}%`) : '' const trendText = trend ? (trend > 0 ? `+${trend.toFixed(1)}%` : `${trend.toFixed(1)}%`) : ''
const title = `.${tldUpper} Domain Pricing & Market Analysis ${new Date().getFullYear()}` const title = `.${tldUpper} Domain Pricing & Market Analysis ${new Date().getFullYear()}`
const description = `Complete .${tldUpper} domain pricing intelligence${price ? ` starting at $${price.toFixed(2)}` : ''}${trendText ? ` (${trendText} trend)` : ''}. Compare registration, renewal, and transfer costs across major registrars. Real-time market data and price alerts.` const description = `Complete .${tldUpper} domain pricing intelligence${price ? ` starting at $${price.toFixed(2)}` : ''}${trendText ? ` (${trendText} trend)` : ''}. Compare registration, renewal, and transfer costs across major registrars. Updated daily with market data and price alerts.`
return { return {
title, title,

View File

@ -321,7 +321,7 @@ export default function HomePage() {
{ {
module: '01', module: '01',
title: 'Intelligence', title: 'Intelligence',
desc: '"Identify Targets." We scan 886+ TLDs in real-time to uncover hidden opportunities.', desc: '"Identify Targets." We scan 886+ TLDs to uncover pricing traps, trends, and opportunities.',
features: [ features: [
{ icon: Scan, title: 'Global Scan', desc: 'Zone file analysis' }, { icon: Scan, title: 'Global Scan', desc: 'Zone file analysis' },
{ icon: Target, title: 'Valuation AI', desc: 'Instant fair-market value' }, { icon: Target, title: 'Valuation AI', desc: 'Instant fair-market value' },

View File

@ -43,7 +43,7 @@ function GitHubIcon({ className }: { className?: string }) {
} }
const benefits = [ const benefits = [
{ text: 'Real-time Market Feed', icon: Zap }, { text: 'Live Market Feed', icon: Zap },
{ text: 'Daily Scan Reports', icon: Shield }, { text: 'Daily Scan Reports', icon: Shield },
{ text: 'Yield Intel Access', icon: TrendingUp }, { text: 'Yield Intel Access', icon: TrendingUp },
] ]

View File

@ -94,6 +94,9 @@ export default function PortfolioPage() {
const [menuOpen, setMenuOpen] = useState(false) const [menuOpen, setMenuOpen] = useState(false)
const [navDrawerOpen, setNavDrawerOpen] = useState(false) const [navDrawerOpen, setNavDrawerOpen] = useState(false)
// Yield domains - to show which are in Yield
const [yieldDomains, setYieldDomains] = useState<Set<string>>(new Set())
const tier = subscription?.tier || 'scout' const tier = subscription?.tier || 'scout'
const isScout = tier === 'scout' const isScout = tier === 'scout'
@ -102,12 +105,15 @@ export default function PortfolioPage() {
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
setLoading(true) setLoading(true)
try { try {
const [domainsData, summaryData] = await Promise.all([ const [domainsData, summaryData, yieldData] = 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())))
} catch (err) { } catch (err) {
console.error('Failed to load portfolio:', err) console.error('Failed to load portfolio:', err)
} finally { } finally {
@ -334,7 +340,7 @@ export default function PortfolioPage() {
<Plus className="w-4 h-4" /> <Plus className="w-4 h-4" />
<span className="hidden sm:inline">Add Domain</span> <span className="hidden sm:inline">Add Domain</span>
</button> </button>
</div> </div>
</section> </section>
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* ═══════════════════════════════════════════════════════════════════════ */}
@ -356,7 +362,7 @@ export default function PortfolioPage() {
> >
<Plus className="w-4 h-4" />Add Domain <Plus className="w-4 h-4" />Add Domain
</button> </button>
</div> </div>
) : ( ) : (
<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 */}
@ -401,15 +407,20 @@ export default function PortfolioPage() {
<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-2 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>
)} )}
{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 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 <ShieldCheck className="w-2.5 h-2.5" /> Verified
</span> </span>
)} )}
</div> {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>
</div> </div>
</div> </div>
@ -434,27 +445,27 @@ export default function PortfolioPage() {
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{!domain.is_dns_verified && ( {!domain.is_dns_verified && (
<button <button
onClick={() => setVerifyingDomain(domain)} onClick={() => setVerifyingDomain(domain)}
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>
)} )}
<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" className="p-1.5 text-white/30 hover:text-white 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}
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>
)} )}
@ -506,32 +517,39 @@ export default function PortfolioPage() {
</div> </div>
{/* Status */} {/* Status */}
<div className="flex justify-center"> <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 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-1 px-2 py-1 text-[9px] font-mono uppercase bg-accent/10 text-accent border border-accent/20"> <>
<ShieldCheck className="w-3 h-3" /> 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">
</span> <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">
<Coins className="w-2.5 h-2.5" />
</span>
)}
</>
) : ( ) : (
<button <button
onClick={() => setVerifyingDomain(domain)} onClick={() => setVerifyingDomain(domain)}
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-2">
{!domain.is_sold && domain.is_dns_verified && ( {!domain.is_sold && domain.is_dns_verified && (
<Link <Link
href="/terminal/listing" 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" 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 Sell
</Link> </Link>
)} )}
<button <button
onClick={() => handleRefreshValue(domain.id)} onClick={() => handleRefreshValue(domain.id)}
@ -584,7 +602,7 @@ export default function PortfolioPage() {
<Navigation className="w-5 h-5" /> <Navigation className="w-5 h-5" />
<span className="text-[9px] font-mono uppercase tracking-wider">Menu</span> <span className="text-[9px] font-mono uppercase tracking-wider">Menu</span>
</button> </button>
</div> </div>
</nav> </nav>
{/* Navigation Drawer */} {/* Navigation Drawer */}
@ -597,11 +615,11 @@ export default function PortfolioPage() {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Image src="/pounce-icon.png" alt="Pounce" width={24} height={24} /> <Image src="/pounce-icon.png" alt="Pounce" width={24} height={24} />
<span className="font-display text-sm text-white">Terminal</span> <span className="font-display text-sm text-white">Terminal</span>
</div> </div>
<button onClick={() => setNavDrawerOpen(false)} className="p-1 text-white/40 hover:text-white"> <button onClick={() => setNavDrawerOpen(false)} className="p-1 text-white/40 hover:text-white">
<X className="w-5 h-5" /> <X className="w-5 h-5" />
</button> </button>
</div> </div>
</div> </div>
<div className="p-4 space-y-6"> <div className="p-4 space-y-6">
@ -626,8 +644,8 @@ export default function PortfolioPage() {
)} )}
</Link> </Link>
))} ))}
</div> </div>
</div> </div>
))} ))}
<div className="pt-4 border-t border-white/[0.08]"> <div className="pt-4 border-t border-white/[0.08]">
@ -639,14 +657,14 @@ export default function PortfolioPage() {
<Settings className="w-4 h-4" /> <Settings className="w-4 h-4" />
<span className="text-sm font-medium">Settings</span> <span className="text-sm font-medium">Settings</span>
</Link> </Link>
<button <button
onClick={() => { logout(); setNavDrawerOpen(false) }} onClick={() => { logout(); setNavDrawerOpen(false) }}
className="w-full flex items-center gap-3 px-3 py-2.5 text-rose-400/60 hover:text-rose-400 transition-colors" className="w-full flex items-center gap-3 px-3 py-2.5 text-rose-400/60 hover:text-rose-400 transition-colors"
> >
<LogOut className="w-4 h-4" /> <LogOut className="w-4 h-4" />
<span className="text-sm font-medium">Logout</span> <span className="text-sm font-medium">Logout</span>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
@ -677,7 +695,7 @@ export default function PortfolioPage() {
)} )}
{toast && <Toast message={toast.message} type={toast.type} onClose={hideToast} />} {toast && <Toast message={toast.message} type={toast.type} onClose={hideToast} />}
</div> </div>
) )
} }
@ -729,52 +747,52 @@ function AddDomainModal({ onClose, onSuccess, showToast }: {
</div> </div>
<form onSubmit={handleSubmit} className="p-4 space-y-4"> <form onSubmit={handleSubmit} className="p-4 space-y-4">
<div> <div>
<label className="block text-[10px] font-mono text-white/40 uppercase mb-2">Domain Name *</label> <label className="block text-[10px] font-mono text-white/40 uppercase mb-2">Domain Name *</label>
<input <input
type="text" type="text"
value={domain} value={domain}
onChange={(e) => setDomain(e.target.value)} onChange={(e) => setDomain(e.target.value)}
placeholder="example.com" placeholder="example.com"
className="w-full px-4 py-3 bg-white/[0.02] border border-white/[0.08] text-white text-sm font-mono placeholder:text-white/20 focus:border-accent focus:outline-none" className="w-full px-4 py-3 bg-white/[0.02] border border-white/[0.08] text-white text-sm font-mono placeholder:text-white/20 focus:border-accent focus:outline-none"
autoFocus autoFocus
/> />
</div> </div>
<div> <div>
<label className="block text-[10px] font-mono text-white/40 uppercase mb-2">Purchase Price (USD)</label> <label className="block text-[10px] font-mono text-white/40 uppercase mb-2">Purchase Price (USD)</label>
<input <input
type="number" type="number"
step="0.01" step="0.01"
value={purchasePrice} value={purchasePrice}
onChange={(e) => setPurchasePrice(e.target.value)} onChange={(e) => setPurchasePrice(e.target.value)}
placeholder="100" placeholder="100"
className="w-full px-4 py-3 bg-white/[0.02] border border-white/[0.08] text-white text-sm font-mono placeholder:text-white/20 focus:border-accent focus:outline-none" className="w-full px-4 py-3 bg-white/[0.02] border border-white/[0.08] text-white text-sm font-mono placeholder:text-white/20 focus:border-accent focus:outline-none"
/> />
</div> </div>
<div> <div>
<label className="block text-[10px] font-mono text-white/40 uppercase mb-2">Registrar</label> <label className="block text-[10px] font-mono text-white/40 uppercase mb-2">Registrar</label>
<input <input
type="text" type="text"
value={registrar} value={registrar}
onChange={(e) => setRegistrar(e.target.value)} onChange={(e) => setRegistrar(e.target.value)}
placeholder="Namecheap, GoDaddy, etc." placeholder="Namecheap, GoDaddy, etc."
className="w-full px-4 py-3 bg-white/[0.02] border border-white/[0.08] text-white text-sm font-mono placeholder:text-white/20 focus:border-accent focus:outline-none" className="w-full px-4 py-3 bg-white/[0.02] border border-white/[0.08] text-white text-sm font-mono placeholder:text-white/20 focus:border-accent focus:outline-none"
/> />
</div> </div>
<button <button
type="submit" type="submit"
disabled={loading || !domain.trim()} disabled={loading || !domain.trim()}
className="w-full py-3 bg-accent text-black text-sm font-bold uppercase tracking-wider flex items-center justify-center gap-2 disabled:opacity-50 hover:bg-white transition-colors" className="w-full py-3 bg-accent text-black text-sm font-bold uppercase tracking-wider flex items-center justify-center gap-2 disabled:opacity-50 hover:bg-white transition-colors"
> >
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Plus className="w-4 h-4" />} {loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Plus className="w-4 h-4" />}
Add to Portfolio Add to Portfolio
</button> </button>
</form> </form>
</div> </div>
</div> </div>
) )
} }
@ -847,16 +865,16 @@ function DNSVerificationModal({ domain, onClose, onVerified, showToast }: {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h2 className="text-lg font-display text-white">Verify Ownership</h2> <h2 className="text-lg font-display text-white">Verify Ownership</h2>
<button onClick={onClose} className="p-1 text-white/40 hover:text-white"> <button onClick={onClose} className="p-1 text-white/40 hover:text-white">
<X className="w-5 h-5" /> <X className="w-5 h-5" />
</button> </button>
</div> </div>
<p className="text-xs font-mono text-white/40 mt-1">Add a DNS TXT record to prove you own {domain.domain}</p> <p className="text-xs font-mono text-white/40 mt-1">Add a DNS TXT record to prove you own {domain.domain}</p>
</div> </div>
{loading ? ( {loading ? (
<div className="flex items-center justify-center py-16"> <div className="flex items-center justify-center py-16">
<Loader2 className="w-6 h-6 text-accent animate-spin" /> <Loader2 className="w-6 h-6 text-accent animate-spin" />
</div> </div>
) : verificationData ? ( ) : verificationData ? (
<div className="p-4 space-y-4"> <div className="p-4 space-y-4">
<div> <div>
@ -871,10 +889,10 @@ function DNSVerificationModal({ domain, onClose, onVerified, showToast }: {
> >
{copied === 'name' ? <Check className="w-4 h-4 text-accent" /> : <Copy className="w-4 h-4" />} {copied === 'name' ? <Check className="w-4 h-4 text-accent" /> : <Copy className="w-4 h-4" />}
</button> </button>
</div>
</div> </div>
</div>
<div> <div>
<label className="block text-[10px] font-mono text-white/40 uppercase mb-2">TXT Value</label> <label className="block text-[10px] font-mono text-white/40 uppercase mb-2">TXT Value</label>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="flex-1 px-4 py-3 bg-white/[0.02] border border-white/[0.08] text-accent text-sm font-mono overflow-x-auto"> <div className="flex-1 px-4 py-3 bg-white/[0.02] border border-white/[0.08] text-accent text-sm font-mono overflow-x-auto">
@ -886,12 +904,12 @@ function DNSVerificationModal({ domain, onClose, onVerified, showToast }: {
> >
{copied === 'value' ? <Check className="w-4 h-4 text-accent" /> : <Copy className="w-4 h-4" />} {copied === 'value' ? <Check className="w-4 h-4 text-accent" /> : <Copy className="w-4 h-4" />}
</button> </button>
</div> </div>
</div> </div>
<div className="p-3 bg-white/[0.02] border border-white/[0.08] text-[11px] font-mono text-white/40 leading-relaxed"> <div className="p-3 bg-white/[0.02] border border-white/[0.08] text-[11px] font-mono text-white/40 leading-relaxed">
💡 DNS changes can take 1-5 minutes to propagate. If verification fails, wait a moment and try again. 💡 DNS changes can take 1-5 minutes to propagate. If verification fails, wait a moment and try again.
</div> </div>
<button <button
onClick={handleCheck} onClick={handleCheck}
@ -900,12 +918,12 @@ function DNSVerificationModal({ domain, onClose, onVerified, showToast }: {
> >
{checking ? <Loader2 className="w-4 h-4 animate-spin" /> : <RefreshCw className="w-4 h-4" />} {checking ? <Loader2 className="w-4 h-4 animate-spin" /> : <RefreshCw className="w-4 h-4" />}
Check Verification Check Verification
</button> </button>
</div> </div>
) : ( ) : (
<div className="py-16 text-center text-white/40 text-sm font-mono"> <div className="py-16 text-center text-white/40 text-sm font-mono">
Failed to load verification data Failed to load verification data
</div> </div>
)} )}
</div> </div>
</div> </div>

View File

@ -5,7 +5,7 @@ import {
TrendingUp, DollarSign, Zap, Plus, CheckCircle2, Clock, AlertCircle, TrendingUp, DollarSign, Zap, Plus, CheckCircle2, Clock, AlertCircle,
MousePointer, Target, Wallet, RefreshCw, ChevronRight, Copy, Check, MousePointer, Target, Wallet, RefreshCw, ChevronRight, Copy, Check,
XCircle, Sparkles, Loader2, Eye, Gavel, Menu, Settings, Shield, LogOut, XCircle, Sparkles, Loader2, Eye, Gavel, Menu, Settings, Shield, LogOut,
Crown, Coins, Tag, X, Briefcase Crown, Coins, Tag, X, Briefcase, Trash2
} from 'lucide-react' } from 'lucide-react'
import { api, YieldDomain, YieldTransaction } from '@/lib/api' import { api, YieldDomain, YieldTransaction } from '@/lib/api'
import { useStore } from '@/lib/store' import { useStore } from '@/lib/store'
@ -148,6 +148,7 @@ export default function YieldPage() {
const [showActivateModal, setShowActivateModal] = useState(false) const [showActivateModal, setShowActivateModal] = useState(false)
const [refreshing, setRefreshing] = useState(false) const [refreshing, setRefreshing] = useState(false)
const [menuOpen, setMenuOpen] = useState(false) const [menuOpen, setMenuOpen] = useState(false)
const [deletingId, setDeletingId] = useState<number | null>(null)
useEffect(() => { checkAuth() }, [checkAuth]) useEffect(() => { checkAuth() }, [checkAuth])
@ -159,6 +160,19 @@ export default function YieldPage() {
finally { setLoading(false); setRefreshing(false) } finally { setLoading(false); setRefreshing(false) }
}, []) }, [])
const handleDeleteYield = useCallback(async (domainId: number, domainName: string) => {
if (!confirm(`Remove ${domainName} from Yield? This will stop all revenue tracking.`)) return
setDeletingId(domainId)
try {
await api.deleteYieldDomain(domainId)
fetchDashboard()
} catch (err) {
console.error('Failed to remove yield domain:', err)
} finally {
setDeletingId(null)
}
}, [fetchDashboard])
useEffect(() => { fetchDashboard() }, [fetchDashboard]) useEffect(() => { fetchDashboard() }, [fetchDashboard])
const stats = dashboard?.stats const stats = dashboard?.stats
@ -285,13 +299,14 @@ export default function YieldPage() {
) : ( ) : (
<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]">
{/* Header */} {/* Header */}
<div className="hidden lg:grid grid-cols-[1fr_80px_120px_80px_80px_80px] 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_80px_120px_80px_80px_80px_60px] gap-4 px-3 py-2 text-[10px] font-mono text-white/40 uppercase tracking-wider border-b border-white/[0.08]">
<div>Domain</div> <div>Domain</div>
<div className="text-center">Status</div> <div className="text-center">Status</div>
<div>Intent</div> <div>Intent</div>
<div className="text-right">Clicks</div> <div className="text-right">Clicks</div>
<div className="text-right">Conv.</div> <div className="text-right">Conv.</div>
<div className="text-right">Revenue</div> <div className="text-right">Revenue</div>
<div className="text-right">Action</div>
</div> </div>
{dashboard.domains.map((domain: YieldDomain) => ( {dashboard.domains.map((domain: YieldDomain) => (
@ -307,14 +322,27 @@ export default function YieldPage() {
</div> </div>
<StatusBadge status={domain.status} /> <StatusBadge status={domain.status} />
</div> </div>
<div className="flex justify-between text-[10px] font-mono text-white/40"> <div className="flex items-center justify-between">
<span>{domain.total_clicks} clicks</span> <div className="flex gap-4 text-[10px] font-mono text-white/40">
<span className="text-accent font-bold">${domain.total_revenue}</span> <span>{domain.total_clicks} clicks</span>
<span className="text-accent font-bold">${domain.total_revenue}</span>
</div>
<button
onClick={() => handleDeleteYield(domain.id, domain.domain)}
disabled={deletingId === domain.id}
className="p-1.5 text-white/30 hover:text-rose-400 disabled:opacity-50 transition-colors"
>
{deletingId === domain.id ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Trash2 className="w-4 h-4" />
)}
</button>
</div> </div>
</div> </div>
{/* Desktop */} {/* Desktop */}
<div className="hidden lg:grid grid-cols-[1fr_80px_120px_80px_80px_80px] gap-4 items-center px-3 py-3"> <div className="hidden lg:grid grid-cols-[1fr_80px_120px_80px_80px_80px_60px] gap-4 items-center px-3 py-3">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-8 h-8 bg-white/[0.02] border border-white/[0.06] flex items-center justify-center text-accent text-xs font-bold font-mono"> <div className="w-8 h-8 bg-white/[0.02] border border-white/[0.06] flex items-center justify-center text-accent text-xs font-bold font-mono">
{domain.domain.charAt(0).toUpperCase()} {domain.domain.charAt(0).toUpperCase()}
@ -326,6 +354,20 @@ export default function YieldPage() {
<div className="text-right text-xs font-mono text-white/60">{domain.total_clicks}</div> <div className="text-right text-xs font-mono text-white/60">{domain.total_clicks}</div>
<div className="text-right text-xs font-mono text-white/60">{domain.total_conversions}</div> <div className="text-right text-xs font-mono text-white/60">{domain.total_conversions}</div>
<div className="text-right text-sm font-bold font-mono text-accent">${domain.total_revenue}</div> <div className="text-right text-sm font-bold font-mono text-accent">${domain.total_revenue}</div>
<div className="flex justify-end">
<button
onClick={() => handleDeleteYield(domain.id, domain.domain)}
disabled={deletingId === domain.id}
className="p-1.5 text-white/30 hover:text-rose-400 disabled:opacity-50 transition-colors"
title="Remove from Yield"
>
{deletingId === domain.id ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Trash2 className="w-4 h-4" />
)}
</button>
</div>
</div> </div>
</div> </div>
))} ))}

View File

@ -421,7 +421,7 @@ export default function YieldPage() {
{ icon: PieChart, title: 'Revenue Share', desc: 'Industry-leading 70/30 split. We only make money when you do.' }, { icon: PieChart, title: 'Revenue Share', desc: 'Industry-leading 70/30 split. We only make money when you do.' },
{ icon: Target, title: '20+ Verticals', desc: 'Finance, Insurance, Travel, Health, B2B Services, and more.' }, { icon: Target, title: '20+ Verticals', desc: 'Finance, Insurance, Travel, Health, B2B Services, and more.' },
{ icon: Shield, title: 'Swiss Partners', desc: 'Direct API integrations with premium Swiss & EU brands.' }, { icon: Shield, title: 'Swiss Partners', desc: 'Direct API integrations with premium Swiss & EU brands.' },
{ icon: BarChart3, title: 'Live Analytics', desc: 'Real-time dashboard showing every click, lead, and conversion.' }, { icon: BarChart3, title: 'Live Analytics', desc: 'Live dashboard showing every click, lead, and conversion.' },
{ icon: RefreshCw, title: 'Auto-Optimization', desc: 'AI automatically routes to the highest-paying partner for each visitor.' }, { icon: RefreshCw, title: 'Auto-Optimization', desc: 'AI automatically routes to the highest-paying partner for each visitor.' },
{ icon: Zap, title: 'Instant DNS', desc: 'Zero downtime. Verify ownership and start earning in < 5 minutes.' } { icon: Zap, title: 'Instant DNS', desc: 'Zero downtime. Verify ownership and start earning in < 5 minutes.' }
].map((feat, i) => ( ].map((feat, i) => (