feat: Pricing page with Yield feature, Stripe LIVE keys
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-13 18:26:17 +01:00
parent ddeb25446e
commit 6074506539
3 changed files with 70 additions and 28 deletions

View File

@ -77,6 +77,26 @@ const PLATFORMS = [
const PREMIUM_TLDS = ['com', 'io', 'ai', 'co', 'de', 'ch', 'net', 'org', 'app', 'dev', 'xyz'] const PREMIUM_TLDS = ['com', 'io', 'ai', 'co', 'de', 'ch', 'net', 'org', 'app', 'dev', 'xyz']
// Live time calculation (same as Terminal)
function calcTimeRemaining(endTimeIso?: string): string {
if (!endTimeIso) return 'N/A'
const end = new Date(endTimeIso).getTime()
const now = Date.now()
const diff = end - now
if (diff <= 0) return 'Ended'
const seconds = Math.floor(diff / 1000)
const days = Math.floor(seconds / 86400)
const hours = Math.floor((seconds % 86400) / 3600)
const mins = Math.floor((seconds % 3600) / 60)
if (days > 0) return `${days}d ${hours}h`
if (hours > 0) return `${hours}h ${mins}m`
if (mins > 0) return `${mins}m`
return '< 1m'
}
function isVanityDomain(auction: Auction): boolean { function isVanityDomain(auction: Auction): boolean {
const parts = auction.domain.split('.') const parts = auction.domain.split('.')
if (parts.length < 2) return false if (parts.length < 2) return false
@ -99,6 +119,9 @@ export default function AcquirePage() {
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [activeTab, setActiveTab] = useState<TabType>('all') const [activeTab, setActiveTab] = useState<TabType>('all')
// Stats from API (real totals, not array lengths)
const [stats, setStats] = useState({ total: 0, ending: 0, direct: 0 })
const [searchQuery, setSearchQuery] = useState('') const [searchQuery, setSearchQuery] = useState('')
const [selectedPlatform, setSelectedPlatform] = useState('All') const [selectedPlatform, setSelectedPlatform] = useState('All')
const [searchFocused, setSearchFocused] = useState(false) const [searchFocused, setSearchFocused] = useState(false)
@ -113,10 +136,10 @@ export default function AcquirePage() {
setLoading(true) setLoading(true)
try { try {
const [allFeed, endingFeed, hotFeed, pounceFeed] = await Promise.all([ const [allFeed, endingFeed, hotFeed, pounceFeed] = await Promise.all([
api.getMarketFeed({ source: 'all', limit: 100, sortBy: 'time' }), api.getMarketFeed({ source: 'external', limit: 150, sortBy: 'time' }),
api.getMarketFeed({ source: 'external', endingWithin: 24, limit: 50, sortBy: 'time' }), api.getMarketFeed({ source: 'external', endingWithin: 24, limit: 100, sortBy: 'time' }),
api.getMarketFeed({ source: 'external', limit: 50, sortBy: 'score' }), api.getMarketFeed({ source: 'external', limit: 50, sortBy: 'score' }),
api.getMarketFeed({ source: 'pounce', limit: 10 }), api.getMarketFeed({ source: 'pounce', limit: 20 }),
]) ])
const convertToAuction = (item: MarketItem): Auction => ({ const convertToAuction = (item: MarketItem): Auction => ({
@ -136,12 +159,19 @@ export default function AcquirePage() {
affiliate_url: item.url, affiliate_url: item.url,
}) })
const externalOnly = (items: MarketItem[]) => items.filter(i => !i.is_pounce).map(convertToAuction) const toAuctions = (items: MarketItem[]) => items.map(convertToAuction)
setAllAuctions(externalOnly(allFeed.items || [])) setAllAuctions(toAuctions(allFeed.items || []))
setEndingSoon(externalOnly(endingFeed.items || [])) setEndingSoon(toAuctions(endingFeed.items || []))
setHotAuctions(externalOnly(hotFeed.items || [])) setHotAuctions(toAuctions(hotFeed.items || []))
setPounceItems(pounceFeed.items || []) setPounceItems(pounceFeed.items || [])
// Set real totals from API
setStats({
total: allFeed.total || allFeed.auction_count || 0,
ending: endingFeed.total || 0,
direct: pounceFeed.pounce_direct_count || pounceFeed.total || 0,
})
} catch (error) { } catch (error) {
console.error('Failed to load auctions:', error) console.error('Failed to load auctions:', error)
} finally { } finally {
@ -173,9 +203,13 @@ export default function AcquirePage() {
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(amount) return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(amount)
} }
const getTimeColor = (timeRemaining: string) => { const getTimeColor = (endTime: string) => {
if (timeRemaining.includes('m') && !timeRemaining.includes('h')) return 'text-red-400' if (!endTime) return 'text-white/40'
if (timeRemaining.includes('h') && parseInt(timeRemaining) < 12) return 'text-amber-400' const diff = new Date(endTime).getTime() - Date.now()
if (diff <= 0) return 'text-white/20'
const hours = diff / (1000 * 60 * 60)
if (hours < 1) return 'text-red-400'
if (hours < 12) return 'text-amber-400'
return 'text-white/40' return 'text-white/40'
} }
@ -224,11 +258,11 @@ export default function AcquirePage() {
{/* Stats Grid */} {/* Stats Grid */}
<div className="grid grid-cols-3 gap-2"> <div className="grid grid-cols-3 gap-2">
<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">
<div className="text-lg font-bold text-white tabular-nums">{allAuctions.length}</div> <div className="text-lg font-bold text-white tabular-nums">{stats.total || allAuctions.length}</div>
<div className="text-[9px] font-mono text-white/30 uppercase tracking-wider">Total</div> <div className="text-[9px] font-mono text-white/30 uppercase tracking-wider">Total</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">
<div className="text-lg font-bold text-accent tabular-nums">{endingSoon.length}</div> <div className="text-lg font-bold text-accent tabular-nums">{stats.ending || endingSoon.length}</div>
<div className="text-[9px] font-mono text-accent/60 uppercase tracking-wider">Ending</div> <div className="text-[9px] font-mono text-accent/60 uppercase tracking-wider">Ending</div>
</div> </div>
<div className="bg-orange-500/[0.05] border border-orange-500/20 p-2"> <div className="bg-orange-500/[0.05] border border-orange-500/20 p-2">
@ -411,15 +445,15 @@ export default function AcquirePage() {
</div> </div>
<div className="grid grid-cols-3 gap-8 text-right"> <div className="grid grid-cols-3 gap-8 text-right">
<div> <div>
<div className="text-3xl font-display text-white mb-1">{allAuctions.length.toLocaleString()}</div> <div className="text-3xl font-display text-white mb-1">{(stats.total || allAuctions.length).toLocaleString()}</div>
<div className="text-[10px] uppercase tracking-widest text-white/30 font-mono">Live Auctions</div> <div className="text-[10px] uppercase tracking-widest text-white/30 font-mono">Live Auctions</div>
</div> </div>
<div> <div>
<div className="text-3xl font-display text-accent mb-1">{endingSoon.length.toLocaleString()}</div> <div className="text-3xl font-display text-accent mb-1">{(stats.ending || endingSoon.length).toLocaleString()}</div>
<div className="text-[10px] uppercase tracking-widest text-accent/60 font-mono">Ending 24h</div> <div className="text-[10px] uppercase tracking-widest text-accent/60 font-mono">Ending 24h</div>
</div> </div>
<div> <div>
<div className="text-3xl font-display text-white mb-1">{pounceItems.length.toLocaleString()}</div> <div className="text-3xl font-display text-white mb-1">{(stats.direct || pounceItems.length).toLocaleString()}</div>
<div className="text-[10px] uppercase tracking-widest text-white/30 font-mono">Direct</div> <div className="text-[10px] uppercase tracking-widest text-white/30 font-mono">Direct</div>
</div> </div>
</div> </div>

View File

@ -19,14 +19,14 @@ const tiers = [
period: '', period: '',
description: 'Test the waters. Zero risk.', description: 'Test the waters. Zero risk.',
features: [ features: [
{ text: 'Market Overview', highlight: false, available: true },
{ text: 'Basic Search', highlight: false, available: true },
{ text: '5 Watchlist Domains', highlight: false, available: true },
{ text: 'Market Feed', highlight: false, available: true, sublabel: 'Raw' }, { text: 'Market Feed', highlight: false, available: true, sublabel: 'Raw' },
{ text: 'Alert Speed', highlight: false, available: true, sublabel: 'Daily' }, { text: 'Alert Speed', highlight: false, available: true, sublabel: 'Daily' },
{ text: 'Pounce Score', highlight: false, available: false }, { text: '5 Watchlist Domains', highlight: false, available: true },
{ text: '2 Sniper Alerts', highlight: false, available: true }, { text: '2 Sniper Alerts', highlight: false, available: true },
{ text: 'TLD Intel', highlight: false, available: true, sublabel: 'Public' },
{ text: 'Pounce Score', highlight: false, available: false },
{ text: 'Marketplace', highlight: false, available: true, sublabel: 'Buy Only' }, { text: 'Marketplace', highlight: false, available: true, sublabel: 'Buy Only' },
{ text: 'Yield (Intent Routing)', highlight: false, available: false },
], ],
cta: 'Start Free', cta: 'Start Free',
highlighted: false, highlighted: false,
@ -41,14 +41,15 @@ const tiers = [
period: '/mo', period: '/mo',
description: 'The smart investor\'s choice.', description: 'The smart investor\'s choice.',
features: [ features: [
{ text: '50 Watchlist Domains', highlight: true, available: true },
{ text: 'Market Feed', highlight: true, available: true, sublabel: 'Curated' }, { text: 'Market Feed', highlight: true, available: true, sublabel: 'Curated' },
{ text: 'Alert Speed', highlight: true, available: true, sublabel: 'Hourly' }, { text: 'Alert Speed', highlight: true, available: true, sublabel: 'Hourly' },
{ text: '50 Watchlist Domains', highlight: true, available: true },
{ text: '10 Sniper Alerts', highlight: true, available: true },
{ text: 'TLD Intel', highlight: true, available: true, sublabel: 'Renewal Prices' }, { text: 'TLD Intel', highlight: true, available: true, sublabel: 'Renewal Prices' },
{ text: 'Pounce Score', highlight: true, available: true }, { text: 'Pounce Score', highlight: true, available: true },
{ text: '5 Listings', highlight: true, available: true, sublabel: '0% Fee' }, { text: '5 Listings', highlight: true, available: true, sublabel: '0% Fee' },
{ text: '10 Sniper Alerts', highlight: true, available: true },
{ text: 'Portfolio', highlight: true, available: true, sublabel: '25 Domains' }, { text: 'Portfolio', highlight: true, available: true, sublabel: '25 Domains' },
{ text: 'Yield (Intent Routing)', highlight: true, available: true, sublabel: '70% Rev Share' },
], ],
cta: 'Upgrade to Trader', cta: 'Upgrade to Trader',
highlighted: true, highlighted: true,
@ -63,14 +64,15 @@ const tiers = [
period: '/mo', period: '/mo',
description: 'For serious domain investors.', description: 'For serious domain investors.',
features: [ features: [
{ text: '500 Watchlist Domains', highlight: true, available: true },
{ text: 'Market Feed', highlight: true, available: true, sublabel: 'Priority' }, { text: 'Market Feed', highlight: true, available: true, sublabel: 'Priority' },
{ text: 'Alert Speed', highlight: true, available: true, sublabel: 'Real-Time' }, { text: 'Alert Speed', highlight: true, available: true, sublabel: 'Real-Time' },
{ text: '500 Watchlist Domains', highlight: true, available: true },
{ text: '50 Sniper Alerts', highlight: true, available: true },
{ text: 'TLD Intel', highlight: true, available: true, sublabel: 'Full History' }, { text: 'TLD Intel', highlight: true, available: true, sublabel: 'Full History' },
{ text: 'Score + SEO Data', highlight: true, available: true }, { text: 'Score + SEO Data', highlight: true, available: true },
{ text: '50 Listings', highlight: true, available: true, sublabel: 'Featured' }, { text: '50 Listings', highlight: true, available: true, sublabel: 'Featured' },
{ text: '50 Sniper Alerts', highlight: true, available: true },
{ text: 'Unlimited Portfolio', highlight: true, available: true }, { text: 'Unlimited Portfolio', highlight: true, available: true },
{ text: 'Yield (Intent Routing)', highlight: true, available: true, sublabel: 'Priority Routes' },
], ],
cta: 'Go Tycoon', cta: 'Go Tycoon',
highlighted: false, highlighted: false,
@ -83,11 +85,12 @@ const comparisonFeatures = [
{ name: 'Market Feed', scout: 'Raw (Unfiltered)', trader: 'Curated (Spam-Free)', tycoon: 'Curated + Priority' }, { name: 'Market Feed', scout: 'Raw (Unfiltered)', trader: 'Curated (Spam-Free)', tycoon: 'Curated + Priority' },
{ name: 'Alert Speed', scout: 'Daily', trader: 'Hourly', tycoon: 'Real-Time (10 min)' }, { name: 'Alert Speed', scout: 'Daily', trader: 'Hourly', tycoon: 'Real-Time (10 min)' },
{ name: 'Watchlist', scout: '5 Domains', trader: '50 Domains', tycoon: '500 Domains' }, { name: 'Watchlist', scout: '5 Domains', trader: '50 Domains', tycoon: '500 Domains' },
{ name: 'Marketplace', scout: 'Buy Only', trader: 'Sell (0% Fee)', tycoon: 'Sell + Featured' }, { name: 'Sniper Alerts', scout: '2', trader: '10', tycoon: '50' },
{ name: 'TLD Intel', scout: 'Public Trends', trader: 'Renewal Prices', tycoon: 'Full History' }, { name: 'TLD Intel', scout: 'Public Trends', trader: 'Renewal Prices', tycoon: 'Full History' },
{ name: 'Valuation', scout: 'Locked', trader: 'Pounce Score', tycoon: 'Score + SEO' }, { name: 'Valuation', scout: 'Locked', trader: 'Pounce Score', tycoon: 'Score + SEO' },
{ name: 'Sniper Alerts', scout: '2', trader: '10', tycoon: '50' }, { name: 'Marketplace', scout: 'Buy Only', trader: '5 Listings (0% Fee)', tycoon: '50 Featured' },
{ name: 'Portfolio', scout: '—', trader: '25 Domains', tycoon: 'Unlimited' }, { name: 'Portfolio', scout: '—', trader: '25 Domains', tycoon: 'Unlimited' },
{ name: 'Yield (Intent Routing)', scout: '—', trader: '70% Rev Share', tycoon: 'Priority Routes' },
] ]
const faqs = [ const faqs = [

View File

@ -107,7 +107,11 @@ function formatPrice(price: number, currency = 'USD'): string {
function isSpam(domain: string): boolean { function isSpam(domain: string): boolean {
const name = domain.split('.')[0] const name = domain.split('.')[0]
return /[-\d]/.test(name) // Spam = contains hyphens OR is mostly numbers OR very long with numbers
if (name.includes('-')) return true
if (name.length > 4 && /\d/.test(name)) return true // Short names with numbers are OK (e.g., "4chan")
if (/^\d+$/.test(name)) return true // Pure number domains
return false
} }
// ============================================================================ // ============================================================================
@ -645,13 +649,14 @@ export default function MarketPage() {
<button <button
onClick={() => setHideSpam(!hideSpam)} onClick={() => setHideSpam(!hideSpam)}
title={hideSpam ? "Showing clean domains only - click to show all" : "Showing all domains - click to hide spam"}
className={clsx( className={clsx(
"px-3 py-1.5 text-xs font-mono transition-colors flex items-center gap-1.5 border", "px-3 py-1.5 text-xs font-mono transition-colors flex items-center gap-1.5 border",
hideSpam ? "bg-white/10 text-white border-white/20" : "text-white/40 border-transparent hover:text-white/60" hideSpam ? "bg-accent/20 text-accent border-accent/30" : "text-white/40 border-transparent hover:text-white/60"
)} )}
> >
<Ban className="w-3 h-3" /> <Ban className="w-3 h-3" />
Clean {hideSpam ? 'Clean' : 'All'}
</button> </button>
<div className="flex-1" /> <div className="flex-1" />