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
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:
@ -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>
|
||||
|
||||
@ -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 = [
|
||||
|
||||
@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user