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