Public messaging: Landing & Pricing weniger Yield, mehr Terminal/Intelligence/Market/Watchlist
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:
@ -162,7 +162,7 @@ export default function AdminPage() {
|
|||||||
// Load data when tab changes
|
// Load data when tab changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user?.is_admin) return
|
if (!user?.is_admin) return
|
||||||
loadAdminData()
|
loadAdminData()
|
||||||
}, [activeTab, user?.is_admin])
|
}, [activeTab, user?.is_admin])
|
||||||
|
|
||||||
const toggleSidebar = () => {
|
const toggleSidebar = () => {
|
||||||
@ -199,27 +199,27 @@ export default function AdminPage() {
|
|||||||
setNewsletter(nlData.subscribers)
|
setNewsletter(nlData.subscribers)
|
||||||
setNewsletterTotal(nlData.total)
|
setNewsletterTotal(nlData.total)
|
||||||
} else if (activeTab === 'system') {
|
} else if (activeTab === 'system') {
|
||||||
const [healthData, schedulerData] = await Promise.all([
|
const [healthData, schedulerData] = await Promise.all([
|
||||||
api.getSystemHealth().catch(() => null),
|
api.getSystemHealth().catch(() => null),
|
||||||
api.getSchedulerStatus().catch(() => null),
|
api.getSchedulerStatus().catch(() => null),
|
||||||
])
|
])
|
||||||
setSystemHealth(healthData)
|
setSystemHealth(healthData)
|
||||||
setSchedulerStatus(schedulerData)
|
setSchedulerStatus(schedulerData)
|
||||||
const backupData = await api.listDbBackups(20).catch(() => ({ backups: [] }))
|
const backupData = await api.listDbBackups(20).catch(() => ({ backups: [] }))
|
||||||
setBackups(backupData.backups || [])
|
setBackups(backupData.backups || [])
|
||||||
const opsHistory = await api.getOpsAlertsHistory(50).catch(() => ({ events: [] }))
|
const opsHistory = await api.getOpsAlertsHistory(50).catch(() => ({ events: [] }))
|
||||||
setOpsAlertsHistory(opsHistory.events || [])
|
setOpsAlertsHistory(opsHistory.events || [])
|
||||||
} else if (activeTab === 'activity') {
|
} else if (activeTab === 'activity') {
|
||||||
const logData = await api.getActivityLog(50, 0).catch(() => ({ logs: [], total: 0 }))
|
const logData = await api.getActivityLog(50, 0).catch(() => ({ logs: [], total: 0 }))
|
||||||
setActivityLog(logData.logs)
|
setActivityLog(logData.logs)
|
||||||
setActivityLogTotal(logData.total)
|
setActivityLogTotal(logData.total)
|
||||||
} else if (activeTab === 'blog') {
|
} else if (activeTab === 'blog') {
|
||||||
const blogData = await api.getAdminBlogPosts(50, 0).catch(() => ({ posts: [], total: 0 }))
|
const blogData = await api.getAdminBlogPosts(50, 0).catch(() => ({ posts: [], total: 0 }))
|
||||||
setBlogPosts(blogData.posts)
|
setBlogPosts(blogData.posts)
|
||||||
setBlogPostsTotal(blogData.total)
|
setBlogPostsTotal(blogData.total)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Failed to load admin data')
|
setError(err instanceof Error ? err.message : 'Failed to load admin data')
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
@ -569,9 +569,9 @@ export default function AdminPage() {
|
|||||||
{/* Page Content */}
|
{/* Page Content */}
|
||||||
<main className="px-4 sm:px-6 lg:px-8 py-6">
|
<main className="px-4 sm:px-6 lg:px-8 py-6">
|
||||||
{/* Messages */}
|
{/* Messages */}
|
||||||
{error && (
|
{error && (
|
||||||
<div className="mb-6 p-4 bg-red-500/10 border border-red-500/20 flex items-center gap-3">
|
<div className="mb-6 p-4 bg-red-500/10 border border-red-500/20 flex items-center gap-3">
|
||||||
<AlertCircle className="w-5 h-5 text-red-400 shrink-0" />
|
<AlertCircle className="w-5 h-5 text-red-400 shrink-0" />
|
||||||
<p className="text-sm text-red-400 flex-1 font-mono">{error}</p>
|
<p className="text-sm text-red-400 flex-1 font-mono">{error}</p>
|
||||||
<button onClick={() => setError(null)} className="text-red-400 hover:text-red-300">
|
<button onClick={() => setError(null)} className="text-red-400 hover:text-red-300">
|
||||||
<X className="w-4 h-4" />
|
<X className="w-4 h-4" />
|
||||||
@ -592,37 +592,37 @@ export default function AdminPage() {
|
|||||||
{/* Tab Content */}
|
{/* Tab Content */}
|
||||||
{loading && activeTab !== 'earnings' ? (
|
{loading && activeTab !== 'earnings' ? (
|
||||||
<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-red-400 animate-spin" />
|
<Loader2 className="w-6 h-6 text-red-400 animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Overview Tab */}
|
{/* Overview Tab */}
|
||||||
{activeTab === 'overview' && stats && (
|
{activeTab === 'overview' && stats && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
<StatCard title="Total Users" value={stats.users.total} subtitle={`${stats.users.new_this_week} new this week`} icon={Users} />
|
<StatCard title="Total Users" value={stats.users.total} subtitle={`${stats.users.new_this_week} new this week`} icon={Users} />
|
||||||
<StatCard title="Domains" value={stats.domains.watched} subtitle={`${stats.domains.portfolio} in portfolios`} icon={Eye} />
|
<StatCard title="Domains" value={stats.domains.watched} subtitle={`${stats.domains.portfolio} in portfolios`} icon={Eye} />
|
||||||
<StatCard title="TLDs" value={stats.tld_data.unique_tlds} subtitle={`${stats.tld_data.price_records.toLocaleString()} prices`} icon={Globe} />
|
<StatCard title="TLDs" value={stats.tld_data.unique_tlds} subtitle={`${stats.tld_data.price_records.toLocaleString()} prices`} icon={Globe} />
|
||||||
<StatCard title="Newsletter" value={stats.newsletter_subscribers} icon={Mail} accent />
|
<StatCard title="Newsletter" value={stats.newsletter_subscribers} icon={Mail} accent />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid lg:grid-cols-3 gap-4">
|
<div className="grid lg:grid-cols-3 gap-4">
|
||||||
{[
|
{[
|
||||||
{ tier: 'scout', icon: Zap, color: 'text-white/40', bg: 'bg-white/5', border: 'border-white/10' },
|
{ tier: 'scout', icon: Zap, color: 'text-white/40', bg: 'bg-white/5', border: 'border-white/10' },
|
||||||
{ tier: 'trader', icon: TrendingUp, color: 'text-accent', bg: 'bg-accent/10', border: 'border-accent/20' },
|
{ tier: 'trader', icon: TrendingUp, color: 'text-accent', bg: 'bg-accent/10', border: 'border-accent/20' },
|
||||||
{ tier: 'tycoon', icon: Crown, color: 'text-amber-400', bg: 'bg-amber-500/10', border: 'border-amber-500/20' },
|
{ tier: 'tycoon', icon: Crown, color: 'text-amber-400', bg: 'bg-amber-500/10', border: 'border-amber-500/20' },
|
||||||
].map(({ tier, icon: Icon, color, bg, border }) => (
|
].map(({ tier, icon: Icon, color, bg, border }) => (
|
||||||
<div key={tier} className={clsx("p-6 border", bg, border)}>
|
<div key={tier} className={clsx("p-6 border", bg, border)}>
|
||||||
<div className="flex items-center gap-3 mb-3">
|
<div className="flex items-center gap-3 mb-3">
|
||||||
<Icon className={clsx("w-5 h-5", color)} />
|
<Icon className={clsx("w-5 h-5", color)} />
|
||||||
<span className="text-sm font-medium text-white/60 capitalize">{tier}</span>
|
<span className="text-sm font-medium text-white/60 capitalize">{tier}</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-4xl font-bold text-white font-mono">{stats.subscriptions[tier] || 0}</p>
|
<p className="text-4xl font-bold text-white font-mono">{stats.subscriptions[tier] || 0}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid lg:grid-cols-2 gap-4">
|
<div className="grid lg:grid-cols-2 gap-4">
|
||||||
<div className="p-6 border border-white/10 bg-white/[0.02]">
|
<div className="p-6 border border-white/10 bg-white/[0.02]">
|
||||||
<h3 className="text-lg font-bold text-white mb-2">Active Auctions</h3>
|
<h3 className="text-lg font-bold text-white mb-2">Active Auctions</h3>
|
||||||
<p className="text-4xl font-bold text-white font-mono">{stats.auctions.toLocaleString()}</p>
|
<p className="text-4xl font-bold text-white font-mono">{stats.auctions.toLocaleString()}</p>
|
||||||
@ -703,7 +703,7 @@ export default function AdminPage() {
|
|||||||
|
|
||||||
{/* Users Tab */}
|
{/* Users Tab */}
|
||||||
{activeTab === 'users' && (
|
{activeTab === 'users' && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex flex-wrap items-center gap-4">
|
<div className="flex flex-wrap items-center gap-4">
|
||||||
<div className="relative flex-1 max-w-md">
|
<div className="relative flex-1 max-w-md">
|
||||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-white/30" />
|
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-white/30" />
|
||||||
@ -717,136 +717,136 @@ export default function AdminPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={handleExportUsers} className="flex items-center gap-2 px-5 py-3 bg-white/5 border border-white/10 text-white text-sm hover:bg-white/10">
|
<button onClick={handleExportUsers} className="flex items-center gap-2 px-5 py-3 bg-white/5 border border-white/10 text-white text-sm hover:bg-white/10">
|
||||||
<Download className="w-4 h-4" /> Export CSV
|
<Download className="w-4 h-4" /> Export CSV
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PremiumTable
|
<PremiumTable
|
||||||
data={users}
|
data={users}
|
||||||
keyExtractor={(u) => u.id}
|
keyExtractor={(u) => u.id}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
key: 'user',
|
key: 'user',
|
||||||
header: 'User',
|
header: 'User',
|
||||||
render: (u) => (
|
render: (u) => (
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-white">{u.email}</p>
|
<p className="font-medium text-white">{u.email}</p>
|
||||||
<p className="text-xs text-white/40">{u.name || 'No name'}</p>
|
<p className="text-xs text-white/40">{u.name || 'No name'}</p>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'status',
|
key: 'status',
|
||||||
header: 'Status',
|
header: 'Status',
|
||||||
hideOnMobile: true,
|
hideOnMobile: true,
|
||||||
render: (u) => (
|
render: (u) => (
|
||||||
<div className="flex items-center gap-1.5 flex-wrap">
|
<div className="flex items-center gap-1.5 flex-wrap">
|
||||||
{u.is_admin && <Badge variant="accent" size="xs">Admin</Badge>}
|
{u.is_admin && <Badge variant="accent" size="xs">Admin</Badge>}
|
||||||
{u.is_verified && <Badge variant="success" size="xs">Verified</Badge>}
|
{u.is_verified && <Badge variant="success" size="xs">Verified</Badge>}
|
||||||
{!u.is_active && <Badge variant="error" size="xs">Inactive</Badge>}
|
{!u.is_active && <Badge variant="error" size="xs">Inactive</Badge>}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'tier',
|
key: 'tier',
|
||||||
header: 'Tier',
|
header: 'Tier',
|
||||||
render: (u) => (
|
render: (u) => (
|
||||||
<Badge variant={u.subscription.tier === 'tycoon' ? 'warning' : u.subscription.tier === 'trader' ? 'accent' : 'default'} size="sm">
|
<Badge variant={u.subscription.tier === 'tycoon' ? 'warning' : u.subscription.tier === 'trader' ? 'accent' : 'default'} size="sm">
|
||||||
{u.subscription.tier_name}
|
{u.subscription.tier_name}
|
||||||
</Badge>
|
</Badge>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'domains',
|
key: 'domains',
|
||||||
header: 'Domains',
|
header: 'Domains',
|
||||||
hideOnMobile: true,
|
hideOnMobile: true,
|
||||||
render: (u) => <span className="text-white/60 font-mono">{u.domain_count}</span>,
|
render: (u) => <span className="text-white/60 font-mono">{u.domain_count}</span>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
header: 'Actions',
|
header: 'Actions',
|
||||||
align: 'right',
|
align: 'right',
|
||||||
render: (u) => (
|
render: (u) => (
|
||||||
<div className="flex items-center justify-end gap-2">
|
<div className="flex items-center justify-end gap-2">
|
||||||
<select
|
<select
|
||||||
value={u.subscription.tier}
|
value={u.subscription.tier}
|
||||||
onChange={(e) => handleUpgradeUser(u.id, e.target.value)}
|
onChange={(e) => handleUpgradeUser(u.id, e.target.value)}
|
||||||
className="px-2 py-1.5 bg-white/5 border border-white/10 text-white text-xs font-mono"
|
className="px-2 py-1.5 bg-white/5 border border-white/10 text-white text-xs font-mono"
|
||||||
>
|
>
|
||||||
<option value="scout">Scout</option>
|
<option value="scout">Scout</option>
|
||||||
<option value="trader">Trader</option>
|
<option value="trader">Trader</option>
|
||||||
<option value="tycoon">Tycoon</option>
|
<option value="tycoon">Tycoon</option>
|
||||||
</select>
|
</select>
|
||||||
<TableActionButton icon={Shield} onClick={() => handleToggleAdmin(u.id, u.is_admin)} variant={u.is_admin ? 'accent' : 'default'} title={u.is_admin ? 'Remove admin' : 'Make admin'} />
|
<TableActionButton icon={Shield} onClick={() => handleToggleAdmin(u.id, u.is_admin)} variant={u.is_admin ? 'accent' : 'default'} title={u.is_admin ? 'Remove admin' : 'Make admin'} />
|
||||||
<TableActionButton icon={Trash2} onClick={() => handleDeleteUser(u.id, u.email)} variant="danger" disabled={u.is_admin} title="Delete user" />
|
<TableActionButton icon={Trash2} onClick={() => handleDeleteUser(u.id, u.email)} variant="danger" disabled={u.is_admin} title="Delete user" />
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
emptyIcon={<Users className="w-12 h-12 text-white/10" />}
|
emptyIcon={<Users className="w-12 h-12 text-white/10" />}
|
||||||
emptyTitle="No users found"
|
emptyTitle="No users found"
|
||||||
/>
|
/>
|
||||||
<p className="text-sm text-white/40 font-mono">Showing {users.length} of {usersTotal} users</p>
|
<p className="text-sm text-white/40 font-mono">Showing {users.length} of {usersTotal} users</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Newsletter Tab */}
|
{/* Newsletter Tab */}
|
||||||
{activeTab === 'newsletter' && (
|
{activeTab === 'newsletter' && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-sm text-white/60 font-mono">{newsletterTotal} subscribers</p>
|
<p className="text-sm text-white/60 font-mono">{newsletterTotal} subscribers</p>
|
||||||
<button
|
<button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const data = await api.exportNewsletterEmails()
|
const data = await api.exportNewsletterEmails()
|
||||||
const blob = new Blob([data.emails.join('\n')], { type: 'text/plain' })
|
const blob = new Blob([data.emails.join('\n')], { type: 'text/plain' })
|
||||||
const a = document.createElement('a')
|
const a = document.createElement('a')
|
||||||
a.href = URL.createObjectURL(blob)
|
a.href = URL.createObjectURL(blob)
|
||||||
a.download = 'newsletter-emails.txt'
|
a.download = 'newsletter-emails.txt'
|
||||||
a.click()
|
a.click()
|
||||||
}}
|
}}
|
||||||
className="px-5 py-2.5 bg-red-500 text-white font-bold uppercase tracking-wider text-sm hover:bg-red-400"
|
className="px-5 py-2.5 bg-red-500 text-white font-bold uppercase tracking-wider text-sm hover:bg-red-400"
|
||||||
>
|
>
|
||||||
Export Emails
|
Export Emails
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<PremiumTable
|
<PremiumTable
|
||||||
data={newsletter}
|
data={newsletter}
|
||||||
keyExtractor={(s) => s.id}
|
keyExtractor={(s) => s.id}
|
||||||
columns={[
|
columns={[
|
||||||
{ key: 'email', header: 'Email', render: (s) => <span className="text-white font-mono">{s.email}</span> },
|
{ key: 'email', header: 'Email', render: (s) => <span className="text-white font-mono">{s.email}</span> },
|
||||||
{ key: 'status', header: 'Status', render: (s) => <Badge variant={s.is_active ? 'success' : 'error'} dot>{s.is_active ? 'Active' : 'Unsubscribed'}</Badge> },
|
{ key: 'status', header: 'Status', render: (s) => <Badge variant={s.is_active ? 'success' : 'error'} dot>{s.is_active ? 'Active' : 'Unsubscribed'}</Badge> },
|
||||||
{ key: 'subscribed', header: 'Subscribed', render: (s) => <span className="text-white/60 font-mono text-sm">{new Date(s.subscribed_at).toLocaleDateString()}</span> },
|
{ key: 'subscribed', header: 'Subscribed', render: (s) => <span className="text-white/60 font-mono text-sm">{new Date(s.subscribed_at).toLocaleDateString()}</span> },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* System Tab */}
|
{/* System Tab */}
|
||||||
{activeTab === 'system' && (
|
{activeTab === 'system' && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="border border-white/10 bg-[#0a0a0a]">
|
<div className="border border-white/10 bg-[#0a0a0a]">
|
||||||
<div className="px-6 py-4 border-b border-white/10">
|
<div className="px-6 py-4 border-b border-white/10">
|
||||||
<h3 className="text-sm font-bold text-white uppercase tracking-wider">System Status</h3>
|
<h3 className="text-sm font-bold text-white uppercase tracking-wider">System Status</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="divide-y divide-white/[0.06]">
|
<div className="divide-y divide-white/[0.06]">
|
||||||
{[
|
{[
|
||||||
{ label: 'Database', ok: systemHealth?.database === 'healthy', text: systemHealth?.database || 'Unknown' },
|
{ label: 'Database', ok: systemHealth?.database === 'healthy', text: systemHealth?.database || 'Unknown' },
|
||||||
{ label: 'Email (SMTP)', ok: systemHealth?.email_configured, text: systemHealth?.email_configured ? 'Configured' : 'Not configured' },
|
{ label: 'Email (SMTP)', ok: systemHealth?.email_configured, text: systemHealth?.email_configured ? 'Configured' : 'Not configured' },
|
||||||
{ label: 'Stripe', ok: systemHealth?.stripe_configured, text: systemHealth?.stripe_configured ? 'Configured' : 'Not configured' },
|
{ label: 'Stripe', ok: systemHealth?.stripe_configured, text: systemHealth?.stripe_configured ? 'Configured' : 'Not configured' },
|
||||||
{ label: 'Scheduler', ok: schedulerStatus?.scheduler_running, text: schedulerStatus?.scheduler_running ? 'Running' : 'Stopped' },
|
{ label: 'Scheduler', ok: schedulerStatus?.scheduler_running, text: schedulerStatus?.scheduler_running ? 'Running' : 'Stopped' },
|
||||||
].map((item) => (
|
].map((item) => (
|
||||||
<div key={item.label} className="px-6 py-4 flex items-center justify-between">
|
<div key={item.label} className="px-6 py-4 flex items-center justify-between">
|
||||||
<span className="text-white/60 font-mono">{item.label}</span>
|
<span className="text-white/60 font-mono">{item.label}</span>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
{item.ok ? <CheckCircle className="w-4 h-4 text-accent" /> : <XCircle className="w-4 h-4 text-amber-400" />}
|
{item.ok ? <CheckCircle className="w-4 h-4 text-accent" /> : <XCircle className="w-4 h-4 text-amber-400" />}
|
||||||
<span className={clsx("font-mono text-sm", item.ok ? 'text-accent' : 'text-amber-400')}>{item.text}</span>
|
<span className={clsx("font-mono text-sm", item.ok ? 'text-accent' : 'text-amber-400')}>{item.text}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid lg:grid-cols-2 gap-6">
|
<div className="grid lg:grid-cols-2 gap-6">
|
||||||
<div className="border border-white/10 bg-[#0a0a0a]">
|
<div className="border border-white/10 bg-[#0a0a0a]">
|
||||||
<div className="px-6 py-4 border-b border-white/10">
|
<div className="px-6 py-4 border-b border-white/10">
|
||||||
<h3 className="text-sm font-bold text-white uppercase tracking-wider">Manual Triggers</h3>
|
<h3 className="text-sm font-bold text-white uppercase tracking-wider">Manual Triggers</h3>
|
||||||
@ -869,15 +869,15 @@ export default function AdminPage() {
|
|||||||
{runningOpsAlerts ? 'Running...' : 'Run Ops Alerts'}
|
{runningOpsAlerts ? 'Running...' : 'Run Ops Alerts'}
|
||||||
</button>
|
</button>
|
||||||
<button onClick={handleTriggerScrape} disabled={scraping} className="w-full flex items-center justify-center gap-2 px-5 py-3 bg-white/5 border border-white/10 text-white font-medium disabled:opacity-50 hover:bg-white/10">
|
<button onClick={handleTriggerScrape} disabled={scraping} className="w-full flex items-center justify-center gap-2 px-5 py-3 bg-white/5 border border-white/10 text-white font-medium disabled:opacity-50 hover:bg-white/10">
|
||||||
{scraping ? <Loader2 className="w-4 h-4 animate-spin" /> : <Globe className="w-4 h-4" />}
|
{scraping ? <Loader2 className="w-4 h-4 animate-spin" /> : <Globe className="w-4 h-4" />}
|
||||||
{scraping ? 'Scraping...' : 'Scrape TLD Prices'}
|
{scraping ? 'Scraping...' : 'Scrape TLD Prices'}
|
||||||
</button>
|
</button>
|
||||||
<button onClick={handleTriggerAuctionScrape} disabled={auctionScraping} className="w-full flex items-center justify-center gap-2 px-5 py-3 bg-white/5 border border-white/10 text-white font-medium disabled:opacity-50 hover:bg-white/10">
|
<button onClick={handleTriggerAuctionScrape} disabled={auctionScraping} className="w-full flex items-center justify-center gap-2 px-5 py-3 bg-white/5 border border-white/10 text-white font-medium disabled:opacity-50 hover:bg-white/10">
|
||||||
{auctionScraping ? <Loader2 className="w-4 h-4 animate-spin" /> : <Gavel className="w-4 h-4" />}
|
{auctionScraping ? <Loader2 className="w-4 h-4 animate-spin" /> : <Gavel className="w-4 h-4" />}
|
||||||
{auctionScraping ? 'Scraping...' : 'Scrape Auctions'}
|
{auctionScraping ? 'Scraping...' : 'Scrape Auctions'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border border-white/10 bg-[#0a0a0a]">
|
<div className="border border-white/10 bg-[#0a0a0a]">
|
||||||
<div className="px-6 py-4 border-b border-white/10">
|
<div className="px-6 py-4 border-b border-white/10">
|
||||||
@ -893,24 +893,24 @@ export default function AdminPage() {
|
|||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="text-sm text-white font-mono truncate">{b.name}</p>
|
<p className="text-sm text-white font-mono truncate">{b.name}</p>
|
||||||
<p className="text-xs text-white/40 font-mono">{new Date(b.modified_at).toLocaleString()}</p>
|
<p className="text-xs text-white/40 font-mono">{new Date(b.modified_at).toLocaleString()}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-white/40 font-mono">
|
<div className="text-xs text-white/40 font-mono">
|
||||||
{Math.round((b.size_bytes || 0) / 1024 / 1024)} MB
|
{Math.round((b.size_bytes || 0) / 1024 / 1024)} MB
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* TLD Tab */}
|
{/* TLD Tab */}
|
||||||
{activeTab === 'tld' && stats && (
|
{activeTab === 'tld' && stats && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
<StatCard title="Unique TLDs" value={stats.tld_data?.unique_tlds || 0} icon={Globe} />
|
<StatCard title="Unique TLDs" value={stats.tld_data?.unique_tlds || 0} icon={Globe} />
|
||||||
<StatCard title="Price Records" value={stats.tld_data?.price_records?.toLocaleString() || '0'} icon={Database} accent />
|
<StatCard title="Price Records" value={stats.tld_data?.price_records?.toLocaleString() || '0'} icon={Database} accent />
|
||||||
<StatCard title="Active Alerts" value={stats.price_alerts || 0} icon={Bell} />
|
<StatCard title="Active Alerts" value={stats.price_alerts || 0} icon={Bell} />
|
||||||
@ -919,18 +919,18 @@ export default function AdminPage() {
|
|||||||
|
|
||||||
<div className="border border-white/10 bg-[#0a0a0a] p-6">
|
<div className="border border-white/10 bg-[#0a0a0a] p-6">
|
||||||
<h3 className="text-sm font-bold text-white uppercase tracking-wider mb-4">TLD Price Management</h3>
|
<h3 className="text-sm font-bold text-white uppercase tracking-wider mb-4">TLD Price Management</h3>
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={handleTriggerScrape}
|
onClick={handleTriggerScrape}
|
||||||
disabled={scraping}
|
disabled={scraping}
|
||||||
className="flex items-center gap-2 px-5 py-3 bg-red-500 text-white font-bold uppercase tracking-wider disabled:opacity-50 hover:bg-red-400"
|
className="flex items-center gap-2 px-5 py-3 bg-red-500 text-white font-bold uppercase tracking-wider disabled:opacity-50 hover:bg-red-400"
|
||||||
>
|
>
|
||||||
{scraping ? <Loader2 className="w-4 h-4 animate-spin" /> : <Globe className="w-4 h-4" />}
|
{scraping ? <Loader2 className="w-4 h-4 animate-spin" /> : <Globe className="w-4 h-4" />}
|
||||||
{scraping ? 'Scraping...' : 'Scrape All Registrars'}
|
{scraping ? 'Scraping...' : 'Scrape All Registrars'}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Auctions Tab */}
|
{/* Auctions Tab */}
|
||||||
@ -940,7 +940,7 @@ export default function AdminPage() {
|
|||||||
<StatCard title="Total Auctions" value={stats.auctions?.toLocaleString() || '0'} icon={Gavel} />
|
<StatCard title="Total Auctions" value={stats.auctions?.toLocaleString() || '0'} icon={Gavel} />
|
||||||
<StatCard title="Platforms" value="4" subtitle="GoDaddy, Sedo, NameJet, DropCatch" icon={Globe} accent />
|
<StatCard title="Platforms" value="4" subtitle="GoDaddy, Sedo, NameJet, DropCatch" icon={Globe} accent />
|
||||||
<StatCard title="Clean Domains" value={Math.round((stats.auctions || 0) * 0.4).toLocaleString()} subtitle="~40% pass filter" icon={CheckCircle} />
|
<StatCard title="Clean Domains" value={Math.round((stats.auctions || 0) * 0.4).toLocaleString()} subtitle="~40% pass filter" icon={CheckCircle} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border border-white/10 bg-[#0a0a0a] p-6">
|
<div className="border border-white/10 bg-[#0a0a0a] p-6">
|
||||||
<h3 className="text-sm font-bold text-white uppercase tracking-wider mb-4">Auction Management</h3>
|
<h3 className="text-sm font-bold text-white uppercase tracking-wider mb-4">Auction Management</h3>
|
||||||
@ -952,9 +952,9 @@ export default function AdminPage() {
|
|||||||
{auctionScraping ? <Loader2 className="w-4 h-4 animate-spin" /> : <Gavel className="w-4 h-4" />}
|
{auctionScraping ? <Loader2 className="w-4 h-4 animate-spin" /> : <Gavel className="w-4 h-4" />}
|
||||||
{auctionScraping ? 'Scraping...' : 'Scrape All Platforms'}
|
{auctionScraping ? 'Scraping...' : 'Scrape All Platforms'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Activity Tab */}
|
{/* Activity Tab */}
|
||||||
{activeTab === 'activity' && (
|
{activeTab === 'activity' && (
|
||||||
@ -970,9 +970,9 @@ export default function AdminPage() {
|
|||||||
{ key: 'time', header: 'Time', hideOnMobile: true, render: (l) => <span className="text-white/40 font-mono text-sm">{new Date(l.created_at).toLocaleString()}</span> },
|
{ key: 'time', header: 'Time', hideOnMobile: true, render: (l) => <span className="text-white/40 font-mono text-sm">{new Date(l.created_at).toLocaleString()}</span> },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -9,30 +9,19 @@ import { useStore } from '@/lib/store'
|
|||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
import {
|
import {
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
ChevronRight,
|
|
||||||
Zap,
|
Zap,
|
||||||
Globe,
|
Globe,
|
||||||
Check,
|
Check,
|
||||||
Search,
|
|
||||||
Target,
|
Target,
|
||||||
Gavel,
|
Gavel,
|
||||||
Activity,
|
Activity,
|
||||||
Lock,
|
Lock,
|
||||||
Crosshair,
|
|
||||||
Coins,
|
|
||||||
Layers,
|
|
||||||
ArrowUpRight,
|
ArrowUpRight,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
Network,
|
|
||||||
Share2,
|
|
||||||
Key,
|
|
||||||
Shield,
|
Shield,
|
||||||
Radar,
|
Radar,
|
||||||
Scan,
|
Scan,
|
||||||
Radio,
|
|
||||||
Cpu,
|
|
||||||
Clock,
|
Clock,
|
||||||
ExternalLink
|
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
@ -175,8 +164,8 @@ export default function HomePage() {
|
|||||||
{/* Subline */}
|
{/* Subline */}
|
||||||
<div className="animate-slide-up opacity-0" style={{ animationDelay: '0.8s', animationFillMode: 'forwards' }}>
|
<div className="animate-slide-up opacity-0" style={{ animationDelay: '0.8s', animationFillMode: 'forwards' }}>
|
||||||
<p className="text-sm sm:text-lg lg:text-xl text-white/60 max-w-xl font-light leading-relaxed mb-8 sm:mb-12 lg:mx-0 mx-auto">
|
<p className="text-sm sm:text-lg lg:text-xl text-white/60 max-w-xl font-light leading-relaxed mb-8 sm:mb-12 lg:mx-0 mx-auto">
|
||||||
Transforming domains from static addresses into yield-bearing financial assets.
|
High-density domain intelligence for investors and operators.
|
||||||
<span className="text-white block mt-1 font-medium">Scan. Acquire. Route. Profit.</span>
|
<span className="text-white block mt-1 font-medium">Scan. Track. Trade.</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Stats Grid - Mobile 2x2 */}
|
{/* Stats Grid - Mobile 2x2 */}
|
||||||
@ -251,40 +240,40 @@ export default function HomePage() {
|
|||||||
<div className="max-w-[1200px] mx-auto">
|
<div className="max-w-[1200px] mx-auto">
|
||||||
<div className="grid lg:grid-cols-2 gap-10 lg:gap-24 items-center">
|
<div className="grid lg:grid-cols-2 gap-10 lg:gap-24 items-center">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-accent font-mono text-[10px] sm:text-xs uppercase tracking-[0.2em] mb-3 sm:mb-4 block">The Broken Model</span>
|
<span className="text-accent font-mono text-[10px] sm:text-xs uppercase tracking-[0.2em] mb-3 sm:mb-4 block">The Problem</span>
|
||||||
<h2 className="font-display text-2xl sm:text-4xl lg:text-5xl text-white leading-tight mb-6 sm:mb-8">
|
<h2 className="font-display text-2xl sm:text-4xl lg:text-5xl text-white leading-tight mb-6 sm:mb-8">
|
||||||
99% of portfolios are <br className="hidden sm:block"/><span className="text-white/30">bleeding cash.</span>
|
The domain market is <br className="hidden sm:block"/><span className="text-white/30">high-noise.</span>
|
||||||
</h2>
|
</h2>
|
||||||
<div className="space-y-4 sm:space-y-6 text-white/60 leading-relaxed text-sm sm:text-lg font-light">
|
<div className="space-y-4 sm:space-y-6 text-white/60 leading-relaxed text-sm sm:text-lg font-light">
|
||||||
<p>Investors pay renewal fees for years, hoping for a "Unicorn" sale that never happens.</p>
|
<p>Prices are fragmented across registrars and marketplaces. Most feeds are spam-heavy and hard to act on.</p>
|
||||||
<p>Traditional parking pays pennies. Marketplaces charge 20% fees. The system drains your capital.</p>
|
<p>The real traps are hidden: renewal inflation, policy changes, and timing windows you miss while you sleep.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="absolute -inset-4 bg-accent/5 blur-3xl" />
|
<div className="absolute -inset-4 bg-accent/5 blur-3xl" />
|
||||||
<div className="relative bg-[#050505] border border-white/10 p-6 sm:p-8 lg:p-12">
|
<div className="relative bg-[#050505] border border-white/10 p-6 sm:p-8 lg:p-12">
|
||||||
<span className="text-accent font-mono text-[10px] sm:text-xs uppercase tracking-[0.2em] mb-3 sm:mb-4 block">The Pounce Protocol</span>
|
<span className="text-accent font-mono text-[10px] sm:text-xs uppercase tracking-[0.2em] mb-3 sm:mb-4 block">The Pounce Terminal</span>
|
||||||
<h3 className="font-display text-xl sm:text-3xl text-white mb-6 sm:mb-8">Asset Class V2.0</h3>
|
<h3 className="font-display text-xl sm:text-3xl text-white mb-6 sm:mb-8">High Density. Low Noise.</h3>
|
||||||
<ul className="space-y-5 sm:space-y-6 font-mono text-xs sm:text-sm text-white/80">
|
<ul className="space-y-5 sm:space-y-6 font-mono text-xs sm:text-sm text-white/80">
|
||||||
<li className="flex items-start gap-3 sm:gap-4">
|
<li className="flex items-start gap-3 sm:gap-4">
|
||||||
<Radar className="w-4 h-4 sm:w-5 sm:h-5 text-accent mt-px shrink-0" />
|
<Radar className="w-4 h-4 sm:w-5 sm:h-5 text-accent mt-px shrink-0" />
|
||||||
<div>
|
<div>
|
||||||
<span className="text-white font-bold block mb-1">Deep Recon</span>
|
<span className="text-white font-bold block mb-1">Clean Market Feeds</span>
|
||||||
<span className="text-white/50 text-[11px] sm:text-sm">Zone file analysis reveals what's truly valuable.</span>
|
<span className="text-white/50 text-[11px] sm:text-sm">Spam-filtered auctions and opportunities you can act on.</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-3 sm:gap-4">
|
<li className="flex items-start gap-3 sm:gap-4">
|
||||||
<Zap className="w-4 h-4 sm:w-5 sm:h-5 text-accent mt-px shrink-0" />
|
<Scan className="w-4 h-4 sm:w-5 sm:h-5 text-accent mt-px shrink-0" />
|
||||||
<div>
|
<div>
|
||||||
<span className="text-white font-bold block mb-1">Frictionless Liquidity</span>
|
<span className="text-white font-bold block mb-1">Renewal & Pricing Intel</span>
|
||||||
<span className="text-white/50 text-[11px] sm:text-sm">Instant settlement. 0% Commission.</span>
|
<span className="text-white/50 text-[11px] sm:text-sm">Spot renewal traps, registrar deltas, and TLD inflation.</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-3 sm:gap-4">
|
<li className="flex items-start gap-3 sm:gap-4">
|
||||||
<Coins className="w-4 h-4 sm:w-5 sm:h-5 text-accent mt-px shrink-0" />
|
<Activity className="w-4 h-4 sm:w-5 sm:h-5 text-accent mt-px shrink-0" />
|
||||||
<div>
|
<div>
|
||||||
<span className="text-white font-bold block mb-1">Automated Yield</span>
|
<span className="text-white font-bold block mb-1">Watchlist Ops</span>
|
||||||
<span className="text-white/50 text-[11px] sm:text-sm">Domains pay for their own renewals.</span>
|
<span className="text-white/50 text-[11px] sm:text-sm">Monitor domains, get alerts, and move fast.</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -304,14 +293,14 @@ export default function HomePage() {
|
|||||||
<div>
|
<div>
|
||||||
<span className="text-accent font-mono text-[10px] sm:text-xs uppercase tracking-[0.2em] mb-3 sm:mb-4 block">Core Architecture</span>
|
<span className="text-accent font-mono text-[10px] sm:text-xs uppercase tracking-[0.2em] mb-3 sm:mb-4 block">Core Architecture</span>
|
||||||
<h2 className="font-display text-3xl sm:text-4xl lg:text-6xl text-white leading-none">
|
<h2 className="font-display text-3xl sm:text-4xl lg:text-6xl text-white leading-none">
|
||||||
The Lifecycle <br />
|
The Terminal <br />
|
||||||
<span className="text-white/30">Engine.</span>
|
<span className="text-white/30">Stack.</span>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<p className="hidden lg:block text-white/50 max-w-md text-sm font-mono mt-8 lg:mt-0 leading-relaxed text-right">
|
<p className="hidden lg:block text-white/50 max-w-md text-sm font-mono mt-8 lg:mt-0 leading-relaxed text-right">
|
||||||
// INTELLIGENCE_LAYER_ACTIVE<br />
|
// INTELLIGENCE_LAYER_ACTIVE<br />
|
||||||
// MARKET_PROTOCOL_READY<br />
|
// MARKET_PROTOCOL_READY<br />
|
||||||
// YIELD_GENERATION_STANDBY
|
// MONITORING_ONLINE
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -320,29 +309,29 @@ export default function HomePage() {
|
|||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
module: '01',
|
module: '01',
|
||||||
title: 'Intelligence',
|
title: 'Discover',
|
||||||
desc: '"Identify Targets." We scan 886+ TLDs to uncover pricing traps, trends, and opportunities.',
|
desc: '"Identify Targets." Find drops, auctions, and pricing anomalies without digging through spam.',
|
||||||
features: [
|
features: [
|
||||||
{ icon: Scan, title: 'Global Scan', desc: 'Zone file analysis' },
|
{ icon: Radar, title: 'Clean Feed', desc: 'No-bullshit filtering' },
|
||||||
{ icon: Target, title: 'Valuation AI', desc: 'Instant fair-market value' },
|
{ icon: Scan, title: 'TLD Intel', desc: 'Renewal & inflation signals' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
module: '02',
|
module: '02',
|
||||||
title: 'Market',
|
title: 'Acquire',
|
||||||
desc: '"Secure the Asset." Direct access to liquidity with verified owners and 0% commission.',
|
desc: '"Secure the Asset." Execute fast: auctions, direct listings, and verified owners.',
|
||||||
features: [
|
features: [
|
||||||
{ icon: ShieldCheck, title: 'Verified Owners', desc: 'Mandatory DNS check' },
|
{ icon: ShieldCheck, title: 'Verified Owners', desc: 'Mandatory DNS check' },
|
||||||
{ icon: Gavel, title: 'Direct Execution', desc: 'P2P transfers' },
|
{ icon: Gavel, title: 'Execution', desc: 'Auctions & direct deals' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
module: '03',
|
module: '03',
|
||||||
title: 'Yield',
|
title: 'Track',
|
||||||
desc: '"Deploy the Asset." Transform idle domains into automated revenue generators.',
|
desc: '"Stay Ahead." Monitor domains and get alerted the second something changes.',
|
||||||
features: [
|
features: [
|
||||||
{ icon: Layers, title: 'Intent Routing', desc: 'Traffic to partners' },
|
{ icon: Activity, title: 'Monitoring', desc: 'Health & change detection' },
|
||||||
{ icon: Coins, title: 'Passive Income', desc: 'Monthly payouts' },
|
{ icon: Zap, title: 'Alerts', desc: 'Fast notifications' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
].map((pillar, i) => (
|
].map((pillar, i) => (
|
||||||
@ -380,38 +369,6 @@ export default function HomePage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
|
||||||
{/* INTENT ROUTING */}
|
|
||||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
|
||||||
<section className="relative py-16 sm:py-32 px-4 sm:px-6 border-b border-white/[0.05] bg-[#050505] overflow-hidden">
|
|
||||||
<div className="absolute inset-0 bg-accent/[0.02]" />
|
|
||||||
<div className="max-w-[1200px] mx-auto relative z-10">
|
|
||||||
<div className="mb-12 sm:mb-20 text-center">
|
|
||||||
<span className="text-accent font-mono text-[10px] sm:text-xs uppercase tracking-[0.2em] mb-3 sm:mb-4 block">The Endgame</span>
|
|
||||||
<h2 className="font-display text-2xl sm:text-4xl lg:text-5xl text-white mb-4 sm:mb-6">Intent Routing™</h2>
|
|
||||||
<p className="text-white/50 max-w-2xl mx-auto text-sm sm:text-lg font-light leading-relaxed px-2">
|
|
||||||
Our engine detects user intent and routes traffic directly to high-paying partners.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 sm:gap-8">
|
|
||||||
{[
|
|
||||||
{ icon: Network, step: '1', title: 'Connect', desc: 'Point nameservers to ns.pounce.io' },
|
|
||||||
{ icon: Cpu, step: '2', title: 'Analyze', desc: 'We scan the semantic intent of your domain' },
|
|
||||||
{ icon: Share2, step: '3', title: 'Route', desc: 'Traffic is routed to vertical partners' },
|
|
||||||
].map((item, i) => (
|
|
||||||
<div key={i} className="bg-[#020202] border border-white/10 p-6 sm:p-10 relative group hover:border-accent/30 transition-colors">
|
|
||||||
<div className="w-10 h-10 sm:w-14 sm:h-14 bg-white/5 flex items-center justify-center mb-5 sm:mb-8 text-white group-hover:text-accent group-hover:bg-accent/10 transition-all">
|
|
||||||
<item.icon className="w-5 h-5 sm:w-7 sm:h-7" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-white font-bold text-base sm:text-xl mb-2 sm:mb-3">{item.step}. {item.title}</h3>
|
|
||||||
<p className="text-white/40 text-xs sm:text-sm font-mono leading-relaxed">{item.desc}</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||||
{/* MARKET DEEP DIVE */}
|
{/* MARKET DEEP DIVE */}
|
||||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||||
|
|||||||
@ -26,7 +26,7 @@ const tiers = [
|
|||||||
{ text: 'TLD Intel', highlight: false, available: true, sublabel: 'Public' },
|
{ text: 'TLD Intel', highlight: false, available: true, sublabel: 'Public' },
|
||||||
{ text: 'Pounce Score', highlight: false, available: false },
|
{ 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 },
|
{ text: 'Yield (Beta)', highlight: false, available: false },
|
||||||
],
|
],
|
||||||
cta: 'Enter Terminal',
|
cta: 'Enter Terminal',
|
||||||
highlighted: false,
|
highlighted: false,
|
||||||
@ -49,7 +49,7 @@ const tiers = [
|
|||||||
{ 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: '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' },
|
{ text: 'Yield (Beta)', highlight: false, available: true, sublabel: 'Optional' },
|
||||||
],
|
],
|
||||||
cta: 'Upgrade to Trader',
|
cta: 'Upgrade to Trader',
|
||||||
highlighted: true,
|
highlighted: true,
|
||||||
@ -62,7 +62,7 @@ const tiers = [
|
|||||||
icon: Crown,
|
icon: Crown,
|
||||||
price: '29',
|
price: '29',
|
||||||
period: '/mo',
|
period: '/mo',
|
||||||
description: 'Full firepower. Priority routes.',
|
description: 'Full firepower. Priority alerts.',
|
||||||
features: [
|
features: [
|
||||||
{ 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: '10 min' },
|
{ text: 'Alert Speed', highlight: true, available: true, sublabel: '10 min' },
|
||||||
@ -72,7 +72,7 @@ const tiers = [
|
|||||||
{ 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: 'Unlimited Portfolio', highlight: true, available: true },
|
{ text: 'Unlimited Portfolio', highlight: true, available: true },
|
||||||
{ text: 'Yield (Intent Routing)', highlight: true, available: true, sublabel: 'Priority Routes' },
|
{ text: 'Yield (Beta)', highlight: false, available: true, sublabel: 'Optional' },
|
||||||
],
|
],
|
||||||
cta: 'Go Tycoon',
|
cta: 'Go Tycoon',
|
||||||
highlighted: false,
|
highlighted: false,
|
||||||
@ -90,7 +90,7 @@ const comparisonFeatures = [
|
|||||||
{ name: 'Valuation', scout: 'Locked', trader: 'Pounce Score', tycoon: 'Score + SEO' },
|
{ name: 'Valuation', scout: 'Locked', trader: 'Pounce Score', tycoon: 'Score + SEO' },
|
||||||
{ name: 'Marketplace', scout: 'Buy Only', trader: '5 Listings (0% Fee)', tycoon: '50 Featured' },
|
{ 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' },
|
{ name: 'Yield (Beta)', scout: '—', trader: 'Optional', tycoon: 'Optional' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const faqs = [
|
const faqs = [
|
||||||
|
|||||||
@ -459,30 +459,30 @@ function ListingRow({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-[#020202] hover:bg-white/[0.02] transition-all group">
|
<div className="bg-[#020202] hover:bg-white/[0.02] transition-all group">
|
||||||
{/* Mobile */}
|
{/* Mobile */}
|
||||||
<div className="lg:hidden p-3">
|
<div className="lg:hidden p-3">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className={clsx("w-8 h-8 border flex items-center justify-center",
|
<div className={clsx("w-8 h-8 border flex items-center justify-center",
|
||||||
listing.is_verified ? "bg-accent/10 border-accent/20" : "bg-white/[0.02] border-white/[0.06]"
|
listing.is_verified ? "bg-accent/10 border-accent/20" : "bg-white/[0.02] border-white/[0.06]"
|
||||||
)}>
|
)}>
|
||||||
{listing.is_verified ? <Shield className="w-4 h-4 text-accent" /> : <AlertCircle className="w-4 h-4 text-amber-400" />}
|
{listing.is_verified ? <Shield className="w-4 h-4 text-accent" /> : <AlertCircle className="w-4 h-4 text-amber-400" />}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-sm font-bold text-white font-mono">{listing.domain}</span>
|
<span className="text-sm font-bold text-white font-mono">{listing.domain}</span>
|
||||||
{isTycoon && <span className="ml-2 px-1 py-0.5 text-[8px] font-mono bg-amber-400/10 text-amber-400 border border-amber-400/20">Featured</span>}
|
{isTycoon && <span className="ml-2 px-1 py-0.5 text-[8px] font-mono bg-amber-400/10 text-amber-400 border border-amber-400/20">Featured</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className={clsx("px-1.5 py-0.5 text-[9px] font-mono border",
|
<span className={clsx("px-1.5 py-0.5 text-[9px] font-mono border",
|
||||||
isActive ? "bg-accent/10 text-accent border-accent/20" :
|
isActive ? "bg-accent/10 text-accent border-accent/20" :
|
||||||
isDraft ? "bg-amber-400/10 text-amber-400 border-amber-400/20" :
|
isDraft ? "bg-amber-400/10 text-amber-400 border-amber-400/20" :
|
||||||
"bg-white/5 text-white/40 border-white/10"
|
"bg-white/5 text-white/40 border-white/10"
|
||||||
)}>{listing.status}</span>
|
)}>{listing.status}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-[10px] font-mono text-white/40 mb-2">
|
<div className="flex justify-between text-[10px] font-mono text-white/40 mb-2">
|
||||||
<span>${listing.asking_price?.toLocaleString() || 'Make Offer'}</span>
|
<span>${listing.asking_price?.toLocaleString() || 'Make Offer'}</span>
|
||||||
<span>{listing.view_count} views · {listing.inquiry_count} leads</span>
|
<span>{listing.view_count} views · {listing.inquiry_count} leads</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{isDraft && needsVerification && (
|
{isDraft && needsVerification && (
|
||||||
<button onClick={onVerify} className="flex-1 py-2 bg-amber-400/10 border border-amber-400/20 text-amber-400 text-[10px] font-mono uppercase flex items-center justify-center gap-1">
|
<button onClick={onVerify} className="flex-1 py-2 bg-amber-400/10 border border-amber-400/20 text-amber-400 text-[10px] font-mono uppercase flex items-center justify-center gap-1">
|
||||||
@ -495,9 +495,9 @@ function ListingRow({
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{isActive && (
|
{isActive && (
|
||||||
<a href={listing.public_url} target="_blank" className="flex-1 py-2 border border-white/[0.08] text-[10px] font-mono text-white/40 flex items-center justify-center gap-1">
|
<a href={listing.public_url} target="_blank" className="flex-1 py-2 border border-white/[0.08] text-[10px] font-mono text-white/40 flex items-center justify-center gap-1">
|
||||||
<ExternalLink className="w-3 h-3" />View
|
<ExternalLink className="w-3 h-3" />View
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
{isActive && (
|
{isActive && (
|
||||||
<button
|
<button
|
||||||
@ -520,36 +520,36 @@ function ListingRow({
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button onClick={onDelete} disabled={isDeleting}
|
<button onClick={onDelete} disabled={isDeleting}
|
||||||
className="px-4 py-2 border border-white/[0.08] text-white/40 hover:text-rose-400">
|
className="px-4 py-2 border border-white/[0.08] text-white/40 hover:text-rose-400">
|
||||||
{isDeleting ? <Loader2 className="w-4 h-4 animate-spin" /> : <Trash2 className="w-4 h-4" />}
|
{isDeleting ? <Loader2 className="w-4 h-4 animate-spin" /> : <Trash2 className="w-4 h-4" />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Desktop */}
|
{/* Desktop */}
|
||||||
<div className="hidden lg:grid grid-cols-[1fr_100px_100px_60px_60px_120px] gap-4 items-center px-3 py-3">
|
<div className="hidden lg:grid grid-cols-[1fr_100px_100px_60px_60px_120px] gap-4 items-center px-3 py-3">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className={clsx("w-8 h-8 border flex items-center justify-center",
|
<div className={clsx("w-8 h-8 border flex items-center justify-center",
|
||||||
listing.is_verified ? "bg-accent/10 border-accent/20" : "bg-white/[0.02] border-white/[0.06]"
|
listing.is_verified ? "bg-accent/10 border-accent/20" : "bg-white/[0.02] border-white/[0.06]"
|
||||||
)}>
|
)}>
|
||||||
{listing.is_verified ? <Shield className="w-4 h-4 text-accent" /> : <AlertCircle className="w-4 h-4 text-amber-400" />}
|
{listing.is_verified ? <Shield className="w-4 h-4 text-accent" /> : <AlertCircle className="w-4 h-4 text-amber-400" />}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-sm font-bold text-white font-mono group-hover:text-accent transition-colors">{listing.domain}</span>
|
<span className="text-sm font-bold text-white font-mono group-hover:text-accent transition-colors">{listing.domain}</span>
|
||||||
{isTycoon && <span className="ml-2 px-1 py-0.5 text-[8px] font-mono bg-amber-400/10 text-amber-400 border border-amber-400/20">Featured</span>}
|
{isTycoon && <span className="ml-2 px-1 py-0.5 text-[8px] font-mono bg-amber-400/10 text-amber-400 border border-amber-400/20">Featured</span>}
|
||||||
{!listing.is_verified && <span className="ml-2 text-[9px] text-amber-400/60 font-mono">Unverified</span>}
|
{!listing.is_verified && <span className="ml-2 text-[9px] text-amber-400/60 font-mono">Unverified</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right text-sm font-bold font-mono text-accent">${listing.asking_price?.toLocaleString() || '—'}</div>
|
<div className="text-right text-sm font-bold font-mono text-accent">${listing.asking_price?.toLocaleString() || '—'}</div>
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<span className={clsx("px-2 py-1 text-[9px] font-mono border",
|
<span className={clsx("px-2 py-1 text-[9px] font-mono border",
|
||||||
isActive ? "bg-accent/10 text-accent border-accent/20" :
|
isActive ? "bg-accent/10 text-accent border-accent/20" :
|
||||||
isDraft ? "bg-amber-400/10 text-amber-400 border-amber-400/20" :
|
isDraft ? "bg-amber-400/10 text-amber-400 border-amber-400/20" :
|
||||||
"bg-white/5 text-white/40 border-white/10"
|
"bg-white/5 text-white/40 border-white/10"
|
||||||
)}>{listing.status}</span>
|
)}>{listing.status}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right text-xs font-mono text-white/60">{listing.view_count}</div>
|
<div className="text-right text-xs font-mono text-white/60">{listing.view_count}</div>
|
||||||
<div className="text-right text-xs font-mono text-white/60">{listing.inquiry_count}</div>
|
<div className="text-right text-xs font-mono text-white/60">{listing.inquiry_count}</div>
|
||||||
<div className="flex items-center justify-end gap-1">
|
<div className="flex items-center justify-end gap-1">
|
||||||
{isDraft && needsVerification && (
|
{isDraft && needsVerification && (
|
||||||
<button onClick={onVerify} className="px-2 py-1 bg-amber-400/10 border border-amber-400/20 text-amber-400 text-[9px] font-mono uppercase hover:bg-amber-400/20 transition-colors">
|
<button onClick={onVerify} className="px-2 py-1 bg-amber-400/10 border border-amber-400/20 text-amber-400 text-[9px] font-mono uppercase hover:bg-amber-400/20 transition-colors">
|
||||||
@ -562,9 +562,9 @@ function ListingRow({
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{isActive && (
|
{isActive && (
|
||||||
<a href={listing.public_url} target="_blank" className="w-7 h-7 flex items-center justify-center border border-white/10 text-white/30 hover:text-accent">
|
<a href={listing.public_url} target="_blank" className="w-7 h-7 flex items-center justify-center border border-white/10 text-white/30 hover:text-accent">
|
||||||
<ExternalLink className="w-3.5 h-3.5" />
|
<ExternalLink className="w-3.5 h-3.5" />
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
{isActive && (
|
{isActive && (
|
||||||
<button
|
<button
|
||||||
@ -587,12 +587,12 @@ function ListingRow({
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button onClick={onDelete} disabled={isDeleting}
|
<button onClick={onDelete} disabled={isDeleting}
|
||||||
className="w-7 h-7 flex items-center justify-center border border-white/10 text-white/30 hover:text-rose-400">
|
className="w-7 h-7 flex items-center justify-center border border-white/10 text-white/30 hover:text-rose-400">
|
||||||
{isDeleting ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Trash2 className="w-3.5 h-3.5" />}
|
{isDeleting ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Trash2 className="w-3.5 h-3.5" />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -637,7 +637,7 @@ function MarkSoldModal({ listing, onClose, onDone }: { listing: Listing; onClose
|
|||||||
<div className="text-[10px] font-mono tracking-[0.2em] text-accent uppercase">Mark Sold</div>
|
<div className="text-[10px] font-mono tracking-[0.2em] text-accent uppercase">Mark Sold</div>
|
||||||
<h3 className="mt-1 text-lg font-display text-white">{listing.domain}</h3>
|
<h3 className="mt-1 text-lg font-display text-white">{listing.domain}</h3>
|
||||||
<p className="mt-1 text-xs font-mono text-white/40">Close the deal and capture GMV (optional).</p>
|
<p className="mt-1 text-xs font-mono text-white/40">Close the deal and capture GMV (optional).</p>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" onClick={onClose} className="p-1 text-white/40 hover:text-white" aria-label="Close">
|
<button type="button" onClick={onClose} className="p-1 text-white/40 hover:text-white" aria-label="Close">
|
||||||
<X className="w-5 h-5" />
|
<X className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
@ -658,7 +658,7 @@ function MarkSoldModal({ listing, onClose, onDone }: { listing: Listing; onClose
|
|||||||
<option value="removed">Removed</option>
|
<option value="removed">Removed</option>
|
||||||
<option value="other">Other</option>
|
<option value="other">Other</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[10px] font-mono text-white/40 uppercase mb-2">Deal Value (optional)</label>
|
<label className="block text-[10px] font-mono text-white/40 uppercase mb-2">Deal Value (optional)</label>
|
||||||
@ -877,7 +877,7 @@ function LeadsModal({ listing, onClose }: { listing: Listing; onClose: () => voi
|
|||||||
>
|
>
|
||||||
{updatingId === inq.id ? <Loader2 className="w-4 h-4 animate-spin" /> : <CheckCircle className="w-4 h-4" />}
|
{updatingId === inq.id ? <Loader2 className="w-4 h-4 animate-spin" /> : <CheckCircle className="w-4 h-4" />}
|
||||||
Read
|
Read
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -918,7 +918,7 @@ function LeadsModal({ listing, onClose }: { listing: Listing; onClose: () => voi
|
|||||||
Email
|
Email
|
||||||
<Mail className="w-4 h-4" />
|
<Mail className="w-4 h-4" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{closingId === inq.id && (
|
{closingId === inq.id && (
|
||||||
<div className="mt-3 p-3 border border-white/[0.10] bg-white/[0.02]">
|
<div className="mt-3 p-3 border border-white/[0.10] bg-white/[0.02]">
|
||||||
@ -955,14 +955,14 @@ function LeadsModal({ listing, onClose }: { listing: Listing; onClose: () => voi
|
|||||||
>
|
>
|
||||||
{updatingId === inq.id ? 'Closing…' : 'Confirm'}
|
{updatingId === inq.id ? 'Closing…' : 'Confirm'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* THREAD MODAL (nested) */}
|
{/* THREAD MODAL (nested) */}
|
||||||
@ -989,7 +989,7 @@ function LeadsModal({ listing, onClose }: { listing: Listing; onClose: () => voi
|
|||||||
{loadingThread ? (
|
{loadingThread ? (
|
||||||
<div className="flex items-center justify-center py-10">
|
<div className="flex items-center justify-center py-10">
|
||||||
<Loader2 className="w-6 h-6 text-accent animate-spin" />
|
<Loader2 className="w-6 h-6 text-accent animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
) : threadMessages.length === 0 ? (
|
) : threadMessages.length === 0 ? (
|
||||||
<div className="text-sm font-mono text-white/40">No messages yet.</div>
|
<div className="text-sm font-mono text-white/40">No messages yet.</div>
|
||||||
) : (
|
) : (
|
||||||
@ -1004,7 +1004,7 @@ function LeadsModal({ listing, onClose }: { listing: Listing; onClose: () => voi
|
|||||||
<div className="flex items-center justify-between text-[10px] font-mono text-white/40 mb-1">
|
<div className="flex items-center justify-between text-[10px] font-mono text-white/40 mb-1">
|
||||||
<span>{m.sender_user_id === user?.id ? 'You' : 'Buyer'}</span>
|
<span>{m.sender_user_id === user?.id ? 'You' : 'Buyer'}</span>
|
||||||
<span>{new Date(m.created_at).toLocaleString('en-US')}</span>
|
<span>{new Date(m.created_at).toLocaleString('en-US')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-white/80 whitespace-pre-line">{m.body}</div>
|
<div className="text-sm text-white/80 whitespace-pre-line">{m.body}</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
@ -1162,7 +1162,7 @@ function CreateListingWizard({ onClose, onSuccess, prefillDomain }: {
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Tag className="w-4 h-4 text-accent" />
|
<Tag className="w-4 h-4 text-accent" />
|
||||||
<span className="text-xs font-mono text-accent uppercase tracking-wider">New Listing</span>
|
<span className="text-xs font-mono text-accent uppercase tracking-wider">New Listing</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
{/* Step Indicators */}
|
{/* Step Indicators */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -1199,12 +1199,12 @@ function CreateListingWizard({ onClose, onSuccess, prefillDomain }: {
|
|||||||
<p className="text-xs font-mono text-white/40">Step 1 of 3: Set your domain and price</p>
|
<p className="text-xs font-mono text-white/40">Step 1 of 3: Set your domain and price</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[10px] font-mono text-white/40 uppercase mb-2">Select Domain from Portfolio *</label>
|
<label className="block text-[10px] font-mono text-white/40 uppercase mb-2">Select Domain from Portfolio *</label>
|
||||||
{loadingDomains ? (
|
{loadingDomains ? (
|
||||||
<div className="flex items-center justify-center py-4">
|
<div className="flex items-center justify-center py-4">
|
||||||
<Loader2 className="w-5 h-5 text-accent animate-spin" />
|
<Loader2 className="w-5 h-5 text-accent animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
) : portfolioDomains.length === 0 ? (
|
) : portfolioDomains.length === 0 ? (
|
||||||
<div className="p-4 bg-amber-400/5 border border-amber-400/20 text-center">
|
<div className="p-4 bg-amber-400/5 border border-amber-400/20 text-center">
|
||||||
<AlertCircle className="w-6 h-6 text-amber-400 mx-auto mb-2" />
|
<AlertCircle className="w-6 h-6 text-amber-400 mx-auto mb-2" />
|
||||||
@ -1230,7 +1230,7 @@ function CreateListingWizard({ onClose, onSuccess, prefillDomain }: {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[10px] font-mono text-white/40 uppercase mb-2">Price Type</label>
|
<label className="block text-[10px] font-mono text-white/40 uppercase mb-2">Price Type</label>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<button
|
<button
|
||||||
@ -1249,7 +1249,7 @@ function CreateListingWizard({ onClose, onSuccess, prefillDomain }: {
|
|||||||
)}>
|
)}>
|
||||||
Make Offer
|
Make Offer
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{priceType === 'fixed' && (
|
{priceType === 'fixed' && (
|
||||||
@ -1272,8 +1272,8 @@ function CreateListingWizard({ onClose, onSuccess, prefillDomain }: {
|
|||||||
className="w-full py-3 bg-accent text-black text-xs 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-xs 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" /> : <>Next: Verify Ownership <ArrowRight className="w-4 h-4" /></>}
|
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <>Next: Verify Ownership <ArrowRight className="w-4 h-4" /></>}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* STEP 2: DNS Verification */}
|
{/* STEP 2: DNS Verification */}
|
||||||
|
|||||||
@ -188,7 +188,7 @@ function EditModal({
|
|||||||
setSaving(false)
|
setSaving(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm" onClick={onClose}>
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm" onClick={onClose}>
|
||||||
<div
|
<div
|
||||||
@ -201,9 +201,9 @@ function EditModal({
|
|||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<Edit3 className="w-4 h-4 text-accent" />
|
<Edit3 className="w-4 h-4 text-accent" />
|
||||||
<span className="text-[10px] font-mono text-accent uppercase tracking-wider">Edit Domain</span>
|
<span className="text-[10px] font-mono text-accent uppercase tracking-wider">Edit Domain</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-lg font-bold text-white font-mono">{domain.domain}</h2>
|
<h2 className="text-lg font-bold text-white font-mono">{domain.domain}</h2>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={onClose} className="p-2 text-white/40 hover:text-white">
|
<button onClick={onClose} className="p-2 text-white/40 hover:text-white">
|
||||||
<X className="w-5 h-5" />
|
<X className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
@ -363,8 +363,8 @@ function EditModal({
|
|||||||
className="w-full px-3 py-2 bg-white/5 border border-white/10 text-white text-sm font-mono focus:border-accent/50 focus:outline-none"
|
className="w-full px-3 py-2 bg-white/5 border border-white/10 text-white text-sm font-mono focus:border-accent/50 focus:outline-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
@ -781,7 +781,7 @@ export default function PortfolioPage() {
|
|||||||
setHealthByDomain(prev => ({ ...prev, [key]: report }))
|
setHealthByDomain(prev => ({ ...prev, [key]: report }))
|
||||||
} catch {
|
} catch {
|
||||||
// Silently fail for individual domains
|
// Silently fail for individual domains
|
||||||
} finally {
|
} finally {
|
||||||
setCheckingHealth(prev => {
|
setCheckingHealth(prev => {
|
||||||
const next = new Set(prev)
|
const next = new Set(prev)
|
||||||
next.delete(key)
|
next.delete(key)
|
||||||
@ -1077,8 +1077,8 @@ export default function PortfolioPage() {
|
|||||||
<Plus className="w-3.5 h-3.5" />
|
<Plus className="w-3.5 h-3.5" />
|
||||||
Add
|
Add
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tab Bar - Scrollable */}
|
{/* Tab Bar - Scrollable */}
|
||||||
<div className="-mx-4 px-4 overflow-x-auto">
|
<div className="-mx-4 px-4 overflow-x-auto">
|
||||||
<div className="flex gap-1 min-w-max pb-1">
|
<div className="flex gap-1 min-w-max pb-1">
|
||||||
@ -1124,18 +1124,18 @@ export default function PortfolioPage() {
|
|||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<div className="w-1.5 h-1.5 bg-accent animate-pulse" />
|
<div className="w-1.5 h-1.5 bg-accent animate-pulse" />
|
||||||
<span className="text-[10px] font-mono tracking-[0.2em] text-accent uppercase">Portfolio Manager</span>
|
<span className="text-[10px] font-mono tracking-[0.2em] text-accent uppercase">Portfolio Manager</span>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="font-display text-[2.5rem] leading-[1] tracking-[-0.02em] text-white">
|
<h1 className="font-display text-[2.5rem] leading-[1] tracking-[-0.02em] text-white">
|
||||||
My Portfolio
|
My Portfolio
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-white/40 font-mono max-w-lg mt-2">
|
<p className="text-sm text-white/40 font-mono max-w-lg mt-2">
|
||||||
Track your domain investments. Add purchase details, monitor values, verify ownership, and list for sale.
|
Track your domain investments. Add purchase details, monitor values, verify ownership, and list for sale.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* TABS - Directly under subtitle */}
|
{/* TABS - Directly under subtitle */}
|
||||||
<div className="flex gap-2 pt-2">
|
<div className="flex gap-2 pt-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('assets')}
|
onClick={() => setActiveTab('assets')}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'flex items-center gap-2 px-4 py-2.5 border transition-all',
|
'flex items-center gap-2 px-4 py-2.5 border transition-all',
|
||||||
@ -1146,7 +1146,7 @@ export default function PortfolioPage() {
|
|||||||
>
|
>
|
||||||
<Briefcase className="w-4 h-4" />
|
<Briefcase className="w-4 h-4" />
|
||||||
<span className="text-xs font-bold uppercase tracking-wider">Assets</span>
|
<span className="text-xs font-bold uppercase tracking-wider">Assets</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('financials')}
|
onClick={() => setActiveTab('financials')}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@ -1165,13 +1165,13 @@ export default function PortfolioPage() {
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-8">
|
<div className="flex items-center gap-8">
|
||||||
<div className="text-right" title="Total invested">
|
<div className="text-right" title="Total invested">
|
||||||
<div className="text-2xl font-bold text-white font-mono">{formatCurrency(summary?.total_invested || 0)}</div>
|
<div className="text-2xl font-bold text-white font-mono">{formatCurrency(summary?.total_invested || 0)}</div>
|
||||||
<div className="text-[9px] font-mono text-white/30 uppercase">Invested</div>
|
<div className="text-[9px] font-mono text-white/30 uppercase">Invested</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right" title="Current market value">
|
<div className="text-right" title="Current market value">
|
||||||
<div className="text-2xl font-bold text-accent font-mono">{formatCurrency(summary?.total_value || 0)}</div>
|
<div className="text-2xl font-bold text-accent font-mono">{formatCurrency(summary?.total_value || 0)}</div>
|
||||||
<div className="text-[9px] font-mono text-white/30 uppercase">Value</div>
|
<div className="text-[9px] font-mono text-white/30 uppercase">Value</div>
|
||||||
@ -1247,7 +1247,7 @@ export default function PortfolioPage() {
|
|||||||
>
|
>
|
||||||
<RefreshCw className="w-4 h-4" />Refresh
|
<RefreshCw className="w-4 h-4" />Refresh
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Summary Cards */}
|
{/* Summary Cards */}
|
||||||
@ -1256,7 +1256,7 @@ export default function PortfolioPage() {
|
|||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<Wallet className="w-4 h-4 text-orange-400" />
|
<Wallet className="w-4 h-4 text-orange-400" />
|
||||||
<span className="text-[10px] font-mono text-orange-400/60 uppercase tracking-wider">Next 30 Days</span>
|
<span className="text-[10px] font-mono text-orange-400/60 uppercase tracking-wider">Next 30 Days</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-2xl font-bold text-orange-400 font-mono">${Math.round(cfoData.upcoming_30d_total_usd)}</div>
|
<div className="text-2xl font-bold text-orange-400 font-mono">${Math.round(cfoData.upcoming_30d_total_usd)}</div>
|
||||||
<div className="text-[10px] font-mono text-white/30 mt-1">{cfoData.upcoming_30d_rows.length} renewals due</div>
|
<div className="text-[10px] font-mono text-white/30 mt-1">{cfoData.upcoming_30d_rows.length} renewals due</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1299,8 +1299,8 @@ export default function PortfolioPage() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Burn Rate Timeline */}
|
{/* Burn Rate Timeline */}
|
||||||
<BurnRateTimeline monthly={cfoData.monthly} />
|
<BurnRateTimeline monthly={cfoData.monthly} />
|
||||||
@ -1315,7 +1315,7 @@ export default function PortfolioPage() {
|
|||||||
<div>- If a renewal cost is missing, fill it in on the domain in <span className="text-white/70">Assets → Edit</span>.</div>
|
<div>- If a renewal cost is missing, fill it in on the domain in <span className="text-white/70">Assets → Edit</span>.</div>
|
||||||
<div>- "Set to Drop" is a local flag — you still need to disable auto-renew at your registrar.</div>
|
<div>- "Set to Drop" is a local flag — you still need to disable auto-renew at your registrar.</div>
|
||||||
<div>- Want to cover costs? Activate Yield only for <span className="text-white/70">DNS‑verified</span> domains.</div>
|
<div>- Want to cover costs? Activate Yield only for <span className="text-white/70">DNS‑verified</span> domains.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -1325,23 +1325,23 @@ export default function PortfolioPage() {
|
|||||||
{/* ASSETS TAB (Domain List) */}
|
{/* ASSETS TAB (Domain List) */}
|
||||||
{activeTab === 'assets' && (
|
{activeTab === 'assets' && (
|
||||||
<>
|
<>
|
||||||
{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>
|
||||||
) : !filteredDomains.length ? (
|
) : !filteredDomains.length ? (
|
||||||
<div className="text-center py-16 border border-dashed border-white/[0.08]">
|
<div className="text-center py-16 border border-dashed border-white/[0.08]">
|
||||||
<Briefcase className="w-10 h-10 text-white/10 mx-auto mb-4" />
|
<Briefcase className="w-10 h-10 text-white/10 mx-auto mb-4" />
|
||||||
<p className="text-white/40 text-sm font-mono mb-1">No domains found</p>
|
<p className="text-white/40 text-sm font-mono mb-1">No domains found</p>
|
||||||
<p className="text-white/25 text-xs font-mono mb-4">Add your first domain to start tracking</p>
|
<p className="text-white/25 text-xs font-mono mb-4">Add your first domain to start tracking</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowAddModal(true)}
|
onClick={() => setShowAddModal(true)}
|
||||||
className="inline-flex items-center gap-2 px-4 py-2 bg-accent text-black text-xs font-bold uppercase"
|
className="inline-flex items-center gap-2 px-4 py-2 bg-accent text-black text-xs font-bold uppercase"
|
||||||
>
|
>
|
||||||
<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">
|
<div className="space-y-px">
|
||||||
{/* Desktop Table Header */}
|
{/* Desktop Table Header */}
|
||||||
<div className="hidden lg:grid grid-cols-[1.5fr_1fr_100px_100px_100px_100px_90px_60px_140px] gap-3 px-4 py-2.5 text-[10px] font-mono text-white/40 uppercase tracking-wider border-b border-white/[0.08] bg-white/[0.02]">
|
<div className="hidden lg:grid grid-cols-[1.5fr_1fr_100px_100px_100px_100px_90px_60px_140px] gap-3 px-4 py-2.5 text-[10px] font-mono text-white/40 uppercase tracking-wider border-b border-white/[0.08] bg-white/[0.02]">
|
||||||
@ -1386,12 +1386,12 @@ export default function PortfolioPage() {
|
|||||||
<div className="hidden lg:grid grid-cols-[1.5fr_1fr_100px_100px_100px_100px_90px_60px_140px] gap-3 px-4 py-3 items-center">
|
<div className="hidden lg:grid grid-cols-[1.5fr_1fr_100px_100px_100px_100px_90px_60px_140px] gap-3 px-4 py-3 items-center">
|
||||||
{/* Domain */}
|
{/* Domain */}
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
<div className={clsx(
|
<div className={clsx(
|
||||||
"w-10 h-10 flex items-center justify-center border shrink-0",
|
"w-10 h-10 flex items-center justify-center border shrink-0",
|
||||||
domain.is_sold ? "bg-white/[0.02] border-white/[0.06]" : "bg-accent/10 border-accent/20"
|
domain.is_sold ? "bg-white/[0.02] border-white/[0.06]" : "bg-accent/10 border-accent/20"
|
||||||
)}>
|
)}>
|
||||||
{domain.is_sold ? <CheckCircle className="w-4 h-4 text-white/30" /> : <Briefcase className="w-4 h-4 text-accent" />}
|
{domain.is_sold ? <CheckCircle className="w-4 h-4 text-white/30" /> : <Briefcase className="w-4 h-4 text-accent" />}
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<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-1.5 mt-0.5">
|
<div className="flex items-center gap-1.5 mt-0.5">
|
||||||
@ -1404,7 +1404,7 @@ export default function PortfolioPage() {
|
|||||||
Verify
|
Verify
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -1422,18 +1422,18 @@ export default function PortfolioPage() {
|
|||||||
<span className="text-[8px] font-mono text-white/30">+{tags.length - 2}</span>
|
<span className="text-[8px] font-mono text-white/30">+{tags.length - 2}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Purchased */}
|
{/* Purchased */}
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<div className="text-xs font-mono text-white/60">{formatShortDate(domain.purchase_date)}</div>
|
<div className="text-xs font-mono text-white/60">{formatShortDate(domain.purchase_date)}</div>
|
||||||
<div className="text-[10px] font-mono text-white/30">{formatCurrency(domain.purchase_price)}</div>
|
<div className="text-[10px] font-mono text-white/30">{formatCurrency(domain.purchase_price)}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Expires */}
|
{/* Expires */}
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
{domain.is_sold ? (
|
{domain.is_sold ? (
|
||||||
<div className="text-xs font-mono text-white/30">Sold</div>
|
<div className="text-xs font-mono text-white/30">Sold</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@ -1445,7 +1445,7 @@ export default function PortfolioPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="text-[10px] font-mono text-white/30">{formatShortDate(domain.renewal_date)}</div>
|
<div className="text-[10px] font-mono text-white/30">{formatShortDate(domain.renewal_date)}</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Value */}
|
{/* Value */}
|
||||||
@ -1457,7 +1457,7 @@ export default function PortfolioPage() {
|
|||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<div className={clsx("text-sm font-bold font-mono", roiPositive ? "text-accent" : "text-rose-400")}>
|
<div className={clsx("text-sm font-bold font-mono", roiPositive ? "text-accent" : "text-rose-400")}>
|
||||||
{formatROI(domain.roi)}
|
{formatROI(domain.roi)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Yield */}
|
{/* Yield */}
|
||||||
@ -1502,7 +1502,7 @@ export default function PortfolioPage() {
|
|||||||
>
|
>
|
||||||
<Tag className="w-4 h-4" />
|
<Tag className="w-4 h-4" />
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDelete(domain.id, domain.domain)}
|
onClick={() => handleDelete(domain.id, domain.domain)}
|
||||||
disabled={deletingId === domain.id}
|
disabled={deletingId === domain.id}
|
||||||
@ -1512,7 +1512,7 @@ export default function PortfolioPage() {
|
|||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* MOBILE ROW */}
|
{/* MOBILE ROW */}
|
||||||
<div className="lg:hidden">
|
<div className="lg:hidden">
|
||||||
@ -1527,7 +1527,7 @@ export default function PortfolioPage() {
|
|||||||
domain.is_sold ? "bg-white/[0.02] border-white/[0.06]" : "bg-accent/10 border-accent/20"
|
domain.is_sold ? "bg-white/[0.02] border-white/[0.06]" : "bg-accent/10 border-accent/20"
|
||||||
)}>
|
)}>
|
||||||
{domain.is_sold ? <CheckCircle className="w-4 h-4 text-white/30" /> : <Briefcase className="w-4 h-4 text-accent" />}
|
{domain.is_sold ? <CheckCircle className="w-4 h-4 text-white/30" /> : <Briefcase className="w-4 h-4 text-accent" />}
|
||||||
</div>
|
</div>
|
||||||
<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-1.5 mt-0.5 flex-wrap">
|
<div className="flex items-center gap-1.5 mt-0.5 flex-wrap">
|
||||||
@ -1535,16 +1535,16 @@ export default function PortfolioPage() {
|
|||||||
<span className="text-[10px] font-mono text-white/30">{domain.registrar}</span>
|
<span className="text-[10px] font-mono text-white/30">{domain.registrar}</span>
|
||||||
)}
|
)}
|
||||||
{renderStatusBadges(domain)}
|
{renderStatusBadges(domain)}
|
||||||
</div>
|
</div>
|
||||||
</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">{formatCurrency(domain.estimated_value)}</div>
|
<div className="text-sm font-bold text-accent font-mono">{formatCurrency(domain.estimated_value)}</div>
|
||||||
<div className={clsx("text-[10px] font-mono", roiPositive ? "text-accent" : "text-rose-400")}>
|
<div className={clsx("text-[10px] font-mono", roiPositive ? "text-accent" : "text-rose-400")}>
|
||||||
{formatROI(domain.roi)}
|
{formatROI(domain.roi)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Quick info row */}
|
{/* Quick info row */}
|
||||||
<div className="flex items-center justify-between mt-2 pt-2 border-t border-white/[0.05] text-[10px] font-mono text-white/40">
|
<div className="flex items-center justify-between mt-2 pt-2 border-t border-white/[0.05] text-[10px] font-mono text-white/40">
|
||||||
@ -1557,42 +1557,42 @@ export default function PortfolioPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ChevronDown className={clsx("w-4 h-4 transition-transform", isExpanded && "rotate-180")} />
|
<ChevronDown className={clsx("w-4 h-4 transition-transform", isExpanded && "rotate-180")} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Expanded details */}
|
{/* Expanded details */}
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<div className="px-3 pb-3 space-y-3 border-t border-white/[0.05]">
|
<div className="px-3 pb-3 space-y-3 border-t border-white/[0.05]">
|
||||||
<div className="grid grid-cols-2 gap-3 pt-3 text-[10px] font-mono">
|
<div className="grid grid-cols-2 gap-3 pt-3 text-[10px] font-mono">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-white/30 uppercase mb-0.5">Purchased</div>
|
<div className="text-white/30 uppercase mb-0.5">Purchased</div>
|
||||||
<div className="text-white/60">{formatDate(domain.purchase_date)}</div>
|
<div className="text-white/60">{formatDate(domain.purchase_date)}</div>
|
||||||
<div className="text-white/40">{formatCurrency(domain.purchase_price)}</div>
|
<div className="text-white/40">{formatCurrency(domain.purchase_price)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-white/30 uppercase mb-0.5">Renewal</div>
|
<div className="text-white/30 uppercase mb-0.5">Renewal</div>
|
||||||
<div className={clsx(isExpiringSoon ? "text-orange-400" : "text-white/60")}>{formatDate(domain.renewal_date)}</div>
|
<div className={clsx(isExpiringSoon ? "text-orange-400" : "text-white/60")}>{formatDate(domain.renewal_date)}</div>
|
||||||
<div className="text-white/40">{formatCurrency(domain.renewal_cost)}/yr</div>
|
<div className="text-white/40">{formatCurrency(domain.renewal_cost)}/yr</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{domain.notes && (
|
{domain.notes && (
|
||||||
<div className="text-[10px] font-mono">
|
<div className="text-[10px] font-mono">
|
||||||
<div className="text-white/30 uppercase mb-0.5">Notes</div>
|
<div className="text-white/30 uppercase mb-0.5">Notes</div>
|
||||||
<div className="text-white/50">{domain.notes}</div>
|
<div className="text-white/50">{domain.notes}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex items-center gap-2 pt-2">
|
<div className="flex items-center gap-2 pt-2">
|
||||||
{renderHealth(domain)}
|
{renderHealth(domain)}
|
||||||
<button
|
<button
|
||||||
onClick={() => openAnalyze(domain.domain)}
|
onClick={() => openAnalyze(domain.domain)}
|
||||||
className="flex items-center gap-1 px-2 py-1.5 text-[9px] font-mono uppercase border border-white/10 text-white/60 bg-white/5 hover:text-accent hover:border-accent/20 hover:bg-accent/10"
|
className="flex items-center gap-1 px-2 py-1.5 text-[9px] font-mono uppercase border border-white/10 text-white/60 bg-white/5 hover:text-accent hover:border-accent/20 hover:bg-accent/10"
|
||||||
>
|
>
|
||||||
<Shield className="w-3 h-3" />
|
<Shield className="w-3 h-3" />
|
||||||
Analyze
|
Analyze
|
||||||
</button>
|
</button>
|
||||||
<Link
|
<Link
|
||||||
href={
|
href={
|
||||||
domain.is_dns_verified && !domain.is_sold
|
domain.is_dns_verified && !domain.is_sold
|
||||||
@ -1611,7 +1611,7 @@ export default function PortfolioPage() {
|
|||||||
Yield
|
Yield
|
||||||
</Link>
|
</Link>
|
||||||
{!domain.is_dns_verified && !domain.is_sold && (
|
{!domain.is_dns_verified && !domain.is_sold && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setVerifyingDomain(domain)}
|
onClick={() => setVerifyingDomain(domain)}
|
||||||
className="flex items-center gap-1 px-2 py-1.5 text-[9px] font-mono uppercase border border-amber-400/30 text-amber-400 bg-amber-400/5"
|
className="flex items-center gap-1 px-2 py-1.5 text-[9px] font-mono uppercase border border-amber-400/30 text-amber-400 bg-amber-400/5"
|
||||||
>
|
>
|
||||||
@ -1636,16 +1636,16 @@ export default function PortfolioPage() {
|
|||||||
className="p-1.5 text-white/30 hover:text-rose-400 ml-auto"
|
className="p-1.5 text-white/30 hover:text-rose-400 ml-auto"
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
@ -1673,7 +1673,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 */}
|
||||||
@ -1688,7 +1688,7 @@ export default function PortfolioPage() {
|
|||||||
<button onClick={() => setNavDrawerOpen(false)} className="p-2 text-white/40">
|
<button onClick={() => setNavDrawerOpen(false)} className="p-2 text-white/40">
|
||||||
<X className="w-5 h-5" />
|
<X className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 space-y-6">
|
<div className="p-4 space-y-6">
|
||||||
{drawerNavSections.map(section => (
|
{drawerNavSections.map(section => (
|
||||||
<div key={section.title}>
|
<div key={section.title}>
|
||||||
@ -1708,21 +1708,21 @@ export default function PortfolioPage() {
|
|||||||
<span className="text-sm font-mono">{item.label}</span>
|
<span className="text-sm font-mono">{item.label}</span>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{user && (
|
{user && (
|
||||||
<div className="p-4 border-t border-white/10 mt-4">
|
<div className="p-4 border-t border-white/10 mt-4">
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<div className="flex items-center gap-3 mb-4">
|
||||||
<div className="w-10 h-10 bg-accent/10 border border-accent/20 flex items-center justify-center">
|
<div className="w-10 h-10 bg-accent/10 border border-accent/20 flex items-center justify-center">
|
||||||
<TierIcon className="w-5 h-5 text-accent" />
|
<TierIcon className="w-5 h-5 text-accent" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-mono text-white">{user.email?.split('@')[0]}</div>
|
<div className="text-sm font-mono text-white">{user.email?.split('@')[0]}</div>
|
||||||
<div className="text-[10px] font-mono text-accent uppercase">{tierName}</div>
|
<div className="text-[10px] font-mono text-accent uppercase">{tierName}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Link href="/terminal/settings" className="flex items-center gap-3 px-3 py-2 text-white/60 hover:bg-white/5">
|
<Link href="/terminal/settings" className="flex items-center gap-3 px-3 py-2 text-white/60 hover:bg-white/5">
|
||||||
<Settings className="w-4 h-4" /><span className="text-sm font-mono">Settings</span>
|
<Settings className="w-4 h-4" /><span className="text-sm font-mono">Settings</span>
|
||||||
@ -1776,7 +1776,7 @@ export default function PortfolioPage() {
|
|||||||
<div className="text-[10px] font-mono text-white/40 uppercase tracking-wider mb-1">Health Check</div>
|
<div className="text-[10px] font-mono text-white/40 uppercase tracking-wider mb-1">Health Check</div>
|
||||||
<h3 className="text-lg font-bold text-white font-mono">{selectedHealthDomain.domain}</h3>
|
<h3 className="text-lg font-bold text-white font-mono">{selectedHealthDomain.domain}</h3>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedHealthDomain(null)}
|
onClick={() => setSelectedHealthDomain(null)}
|
||||||
className="p-2 text-white/40 hover:text-white transition-colors"
|
className="p-2 text-white/40 hover:text-white transition-colors"
|
||||||
>
|
>
|
||||||
@ -1801,12 +1801,12 @@ export default function PortfolioPage() {
|
|||||||
cfg.bg, cfg.color, cfg.border
|
cfg.bg, cfg.color, cfg.border
|
||||||
)}>
|
)}>
|
||||||
{cfg.label}
|
{cfg.label}
|
||||||
</div>
|
</div>
|
||||||
{health?.score !== undefined && (
|
{health?.score !== undefined && (
|
||||||
<span className="text-sm font-mono text-white/60">Score: {health.score}/100</span>
|
<span className="text-sm font-mono text-white/60">Score: {health.score}/100</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Checks */}
|
{/* Checks */}
|
||||||
{health ? (
|
{health ? (
|
||||||
<div className="space-y-px mb-5 border border-white/[0.08]">
|
<div className="space-y-px mb-5 border border-white/[0.08]">
|
||||||
@ -1824,8 +1824,8 @@ export default function PortfolioPage() {
|
|||||||
<span className="text-sm font-mono text-rose-400 font-bold">FAIL</span>
|
<span className="text-sm font-mono text-rose-400 font-bold">FAIL</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* HTTP */}
|
{/* HTTP */}
|
||||||
<div className="flex items-center justify-between px-4 py-3 bg-[#020202]">
|
<div className="flex items-center justify-between px-4 py-3 bg-[#020202]">
|
||||||
<span className="text-sm font-mono text-white/70">HTTP Access</span>
|
<span className="text-sm font-mono text-white/70">HTTP Access</span>
|
||||||
@ -1833,15 +1833,15 @@ export default function PortfolioPage() {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<CheckCircle2 className="w-5 h-5 text-accent" />
|
<CheckCircle2 className="w-5 h-5 text-accent" />
|
||||||
<span className="text-sm font-mono text-accent font-bold">OK</span>
|
<span className="text-sm font-mono text-accent font-bold">OK</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<XCircle className="w-5 h-5 text-rose-400" />
|
<XCircle className="w-5 h-5 text-rose-400" />
|
||||||
<span className="text-sm font-mono text-rose-400 font-bold">{health.http?.error?.substring(0, 15) || 'FAIL'}</span>
|
<span className="text-sm font-mono text-rose-400 font-bold">{health.http?.error?.substring(0, 15) || 'FAIL'}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* SSL */}
|
{/* SSL */}
|
||||||
<div className="flex items-center justify-between px-4 py-3 bg-[#020202]">
|
<div className="flex items-center justify-between px-4 py-3 bg-[#020202]">
|
||||||
<span className="text-sm font-mono text-white/70">SSL Certificate</span>
|
<span className="text-sm font-mono text-white/70">SSL Certificate</span>
|
||||||
@ -1849,15 +1849,15 @@ export default function PortfolioPage() {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<CheckCircle2 className="w-5 h-5 text-accent" />
|
<CheckCircle2 className="w-5 h-5 text-accent" />
|
||||||
<span className="text-sm font-mono text-accent font-bold">VALID</span>
|
<span className="text-sm font-mono text-accent font-bold">VALID</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<XCircle className="w-5 h-5 text-rose-400" />
|
<XCircle className="w-5 h-5 text-rose-400" />
|
||||||
<span className="text-sm font-mono text-rose-400 font-bold">MISSING</span>
|
<span className="text-sm font-mono text-rose-400 font-bold">MISSING</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Parked Status */}
|
{/* Parked Status */}
|
||||||
<div className="flex items-center justify-between px-4 py-3 bg-[#020202]">
|
<div className="flex items-center justify-between px-4 py-3 bg-[#020202]">
|
||||||
<span className="text-sm font-mono text-white/70">Not Parked</span>
|
<span className="text-sm font-mono text-white/70">Not Parked</span>
|
||||||
@ -1865,23 +1865,23 @@ export default function PortfolioPage() {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<CheckCircle2 className="w-5 h-5 text-accent" />
|
<CheckCircle2 className="w-5 h-5 text-accent" />
|
||||||
<span className="text-sm font-mono text-accent font-bold">OK</span>
|
<span className="text-sm font-mono text-accent font-bold">OK</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<AlertTriangle className="w-5 h-5 text-amber-400" />
|
<AlertTriangle className="w-5 h-5 text-amber-400" />
|
||||||
<span className="text-sm font-mono text-amber-400 font-bold">PARKED</span>
|
<span className="text-sm font-mono text-amber-400 font-bold">PARKED</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="py-10 text-center text-white/40 text-sm font-mono mb-5 border border-white/[0.08] bg-[#020202]">
|
<div className="py-10 text-center text-white/40 text-sm font-mono mb-5 border border-white/[0.08] bg-[#020202]">
|
||||||
{isLoading ? 'Running health check...' : 'Click below to run health check'}
|
{isLoading ? 'Running health check...' : 'Click below to run health check'}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Run Check Button */}
|
{/* Run Check Button */}
|
||||||
<button
|
<button
|
||||||
onClick={() => handleHealthCheck(selectedHealthDomain.domain)}
|
onClick={() => handleHealthCheck(selectedHealthDomain.domain)}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="w-full py-3.5 bg-accent text-black text-sm font-bold uppercase tracking-wider hover:bg-white transition-colors flex items-center justify-center gap-2 disabled:opacity-50"
|
className="w-full py-3.5 bg-accent text-black text-sm font-bold uppercase tracking-wider hover:bg-white transition-colors flex items-center justify-center gap-2 disabled:opacity-50"
|
||||||
@ -1894,14 +1894,14 @@ export default function PortfolioPage() {
|
|||||||
{health ? 'Refresh Check' : 'Run Check'}
|
{health ? 'Refresh Check' : 'Run Check'}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
})()}
|
})()}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{toast && <Toast message={toast.message} type={toast.type} onClose={hideToast} />}
|
{toast && <Toast message={toast.message} type={toast.type} onClose={hideToast} />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -66,7 +66,7 @@ export function Footer() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs sm:text-sm font-mono text-white/50 mb-6 sm:mb-8 max-w-sm leading-relaxed">
|
<p className="text-xs sm:text-sm font-mono text-white/50 mb-6 sm:mb-8 max-w-sm leading-relaxed">
|
||||||
Global domain intelligence for serious investors. Scan. Acquire. Route. Yield.
|
High-density domain intelligence for serious investors. Scan. Track. Trade.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Newsletter - Hidden on Mobile */}
|
{/* Newsletter - Hidden on Mobile */}
|
||||||
@ -144,7 +144,7 @@ export function Footer() {
|
|||||||
{[
|
{[
|
||||||
{ href: '/acquire', label: 'Acquire' },
|
{ href: '/acquire', label: 'Acquire' },
|
||||||
{ href: '/discover', label: 'Discover' },
|
{ href: '/discover', label: 'Discover' },
|
||||||
{ href: '/yield', label: 'Yield' },
|
{ href: '/intelligence', label: 'Intel' },
|
||||||
{ href: '/pricing', label: 'Pricing' },
|
{ href: '/pricing', label: 'Pricing' },
|
||||||
].map((link) => (
|
].map((link) => (
|
||||||
<li key={link.href}>
|
<li key={link.href}>
|
||||||
|
|||||||
@ -35,7 +35,7 @@ export function Header() {
|
|||||||
const publicNavItems = [
|
const publicNavItems = [
|
||||||
{ href: '/discover', label: 'Discover', icon: TrendingUp },
|
{ href: '/discover', label: 'Discover', icon: TrendingUp },
|
||||||
{ href: '/acquire', label: 'Acquire', icon: Gavel },
|
{ href: '/acquire', label: 'Acquire', icon: Gavel },
|
||||||
{ href: '/yield', label: 'Yield', icon: Coins },
|
{ href: '/intelligence', label: 'Intel', icon: TrendingUp },
|
||||||
{ href: '/pricing', label: 'Pricing', icon: CreditCard },
|
{ href: '/pricing', label: 'Pricing', icon: CreditCard },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user