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']
// 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 {
const parts = auction.domain.split('.')
if (parts.length < 2) return false
@ -99,6 +119,9 @@ export default function AcquirePage() {
const [loading, setLoading] = useState(true)
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 [selectedPlatform, setSelectedPlatform] = useState('All')
const [searchFocused, setSearchFocused] = useState(false)
@ -113,10 +136,10 @@ export default function AcquirePage() {
setLoading(true)
try {
const [allFeed, endingFeed, hotFeed, pounceFeed] = await Promise.all([
api.getMarketFeed({ source: 'all', limit: 100, sortBy: 'time' }),
api.getMarketFeed({ source: 'external', endingWithin: 24, limit: 50, sortBy: 'time' }),
api.getMarketFeed({ source: 'external', limit: 150, sortBy: 'time' }),
api.getMarketFeed({ source: 'external', endingWithin: 24, limit: 100, sortBy: 'time' }),
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 => ({
@ -136,12 +159,19 @@ export default function AcquirePage() {
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 || []))
setEndingSoon(externalOnly(endingFeed.items || []))
setHotAuctions(externalOnly(hotFeed.items || []))
setAllAuctions(toAuctions(allFeed.items || []))
setEndingSoon(toAuctions(endingFeed.items || []))
setHotAuctions(toAuctions(hotFeed.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) {
console.error('Failed to load auctions:', error)
} finally {
@ -173,9 +203,13 @@ export default function AcquirePage() {
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(amount)
}
const getTimeColor = (timeRemaining: string) => {
if (timeRemaining.includes('m') && !timeRemaining.includes('h')) return 'text-red-400'
if (timeRemaining.includes('h') && parseInt(timeRemaining) < 12) return 'text-amber-400'
const getTimeColor = (endTime: string) => {
if (!endTime) return 'text-white/40'
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'
}
@ -224,11 +258,11 @@ export default function AcquirePage() {
{/* Stats Grid */}
<div className="grid grid-cols-3 gap-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>
<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>
<div className="bg-orange-500/[0.05] border border-orange-500/20 p-2">
@ -411,15 +445,15 @@ export default function AcquirePage() {
</div>
<div className="grid grid-cols-3 gap-8 text-right">
<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>
<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>
<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>
</div>

View File

@ -19,14 +19,14 @@ const tiers = [
period: '',
description: 'Test the waters. Zero risk.',
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: '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: '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: 'Yield (Intent Routing)', highlight: false, available: false },
],
cta: 'Start Free',
highlighted: false,
@ -41,14 +41,15 @@ const tiers = [
period: '/mo',
description: 'The smart investor\'s choice.',
features: [
{ text: '50 Watchlist Domains', highlight: true, available: true },
{ text: 'Market Feed', highlight: true, available: true, sublabel: 'Curated' },
{ 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: 'Pounce Score', highlight: true, available: true },
{ 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: 'Yield (Intent Routing)', highlight: true, available: true, sublabel: '70% Rev Share' },
],
cta: 'Upgrade to Trader',
highlighted: true,
@ -63,14 +64,15 @@ const tiers = [
period: '/mo',
description: 'For serious domain investors.',
features: [
{ text: '500 Watchlist Domains', highlight: true, available: true },
{ text: 'Market Feed', highlight: true, available: true, sublabel: 'Priority' },
{ 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: 'Score + SEO Data', highlight: true, available: true },
{ 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: 'Yield (Intent Routing)', highlight: true, available: true, sublabel: 'Priority Routes' },
],
cta: 'Go Tycoon',
highlighted: false,
@ -83,11 +85,12 @@ const comparisonFeatures = [
{ 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: '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: '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: 'Yield (Intent Routing)', scout: '—', trader: '70% Rev Share', tycoon: 'Priority Routes' },
]
const faqs = [

View File

@ -107,7 +107,11 @@ function formatPrice(price: number, currency = 'USD'): string {
function isSpam(domain: string): boolean {
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
onClick={() => setHideSpam(!hideSpam)}
title={hideSpam ? "Showing clean domains only - click to show all" : "Showing all domains - click to hide spam"}
className={clsx(
"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" />
Clean
{hideSpam ? 'Clean' : 'All'}
</button>
<div className="flex-1" />