diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx index e59d6f7..20200ee 100644 --- a/frontend/src/app/dashboard/page.tsx +++ b/frontend/src/app/dashboard/page.tsx @@ -5,6 +5,7 @@ import { useSearchParams } from 'next/navigation' import { useStore } from '@/lib/store' import { api } from '@/lib/api' import { CommandCenterLayout } from '@/components/CommandCenterLayout' +import { PremiumTable, StatCard, PageContainer, Badge, SectionHeader } from '@/components/PremiumTable' import { Toast, useToast } from '@/components/Toast' import { Eye, @@ -13,7 +14,6 @@ import { Gavel, Clock, Bell, - ArrowRight, ExternalLink, Sparkles, ChevronRight, @@ -59,9 +59,6 @@ export default function DashboardPage() { const [quickDomain, setQuickDomain] = useState('') const [addingDomain, setAddingDomain] = useState(false) - // Note: checkAuth is called in CommandCenterLayout, no need to duplicate here - // Auth redirect is also handled by CommandCenterLayout - // Check for upgrade success useEffect(() => { if (searchParams.get('upgraded') === 'true') { @@ -131,37 +128,39 @@ export default function DashboardPage() { > {toast && } -
+ {/* Quick Add */} -
+
-

+

Quick Add to Watchlist

-
- setQuickDomain(e.target.value)} - placeholder="Enter domain to track (e.g., dream.com)" - className="flex-1 h-12 px-5 bg-background/80 backdrop-blur-sm border border-border/50 rounded-xl - text-foreground placeholder:text-foreground-subtle - focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/30 - shadow-[0_2px_8px_-2px_rgba(0,0,0,0.1)]" - /> + +
+ + setQuickDomain(e.target.value)} + placeholder="Enter domain to track (e.g., dream.com)" + className="w-full h-11 pl-11 pr-4 bg-background/80 backdrop-blur-sm border border-border/50 rounded-xl + text-sm text-foreground placeholder:text-foreground-subtle + focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/30" + /> +
@@ -169,269 +168,219 @@ export default function DashboardPage() { {/* Stats Overview */}
- -
-
-
-
- -
- -
-

{totalDomains}

-

Domains Watched

-
+ + - - 0 - ? "bg-gradient-to-br from-accent/15 to-accent/5 border-accent/30 hover:border-accent/50 shadow-[0_0_30px_-10px_rgba(16,185,129,0.2)]" - : "bg-gradient-to-br from-background-secondary/60 to-background-secondary/30 border-border/50 hover:border-accent/30" - )} - > - {availableDomains.length > 0 &&
} -
-
-
0 ? "bg-accent/20 border-accent/30" : "bg-foreground/5 border-border/30" - )}> - 0 ? "text-accent" : "text-foreground-muted" - )} /> -
- {availableDomains.length > 0 && ( - - POUNCE! - - )} -
-

0 ? "text-accent" : "text-foreground" - )}> - {availableDomains.length} -

-

Available Now

-
+ + 0} + /> - - -
-
-
-
- -
- -
-

0

-

Portfolio Domains

-
+ + - -
-
-
-
-
- -
-
-

{tierName}

-

- {subscription?.domains_used || 0}/{subscription?.domain_limit || 5} slots used -

-
-
+
{/* Activity Feed + Market Pulse */}
{/* Activity Feed */} -
-
-

-
- -
- Activity Feed -

- - View all → - +
+
+ + View all → + + } + />
- - {availableDomains.length > 0 ? ( -
- {availableDomains.slice(0, 4).map((domain) => ( -
-
- - -
-
-

{domain.name}

-

Available for registration!

-
- + {availableDomains.length > 0 ? ( +
+ {availableDomains.slice(0, 4).map((domain) => ( + - ))} - {availableDomains.length > 4 && ( -

- +{availableDomains.length - 4} more available +

+ + +
+
+

{domain.name}

+

Available for registration!

+
+ + Register + +
+ ))} + {availableDomains.length > 4 && ( +

+ +{availableDomains.length - 4} more available +

+ )} +
+ ) : totalDomains > 0 ? ( +
+ +

All domains are still registered

+

+ We're monitoring {totalDomains} domains for you

- )} -
- ) : totalDomains > 0 ? ( -
- -

All domains are still registered

-

- We're monitoring {totalDomains} domains for you -

-
- ) : ( -
- -

No domains tracked yet

-

- Add a domain above to start monitoring -

-
- )} +
+ ) : ( +
+ +

No domains tracked yet

+

+ Add a domain above to start monitoring +

+
+ )} +
{/* Market Pulse */} -
-
-

-
- -
- Market Pulse -

- - View all → - +
+
+ + View all → + + } + /> +
+
+ {loadingAuctions ? ( +
+ {[...Array(4)].map((_, i) => ( +
+ ))} +
+ ) : hotAuctions.length > 0 ? ( + + ) : ( +
+ +

No auctions ending soon

+
+ )}
- - {loadingAuctions ? ( -
- {[...Array(4)].map((_, i) => ( -
- ))} -
- ) : hotAuctions.length > 0 ? ( - - ) : ( -
- -

No auctions ending soon

-
- )}
{/* Trending TLDs */} -
-
-

-
- -
- Trending TLDs -

- - View all → - -
- - {loadingTlds ? ( -
- {[...Array(4)].map((_, i) => ( -
- ))} -
- ) : ( -
- {trendingTlds.map((tld) => ( - -
-
-
- .{tld.tld} - 0 - ? "text-orange-400 bg-orange-400/10 border-orange-400/20" - : "text-accent bg-accent/10 border-accent/20" - )}> - {(tld.price_change || 0) > 0 ? '+' : ''}{(tld.price_change || 0).toFixed(1)}% - -
-

{tld.reason}

-
+
+
+ + View all → - ))} -
- )} + } + /> +
+
+ {loadingTlds ? ( +
+ {[...Array(4)].map((_, i) => ( +
+ ))} +
+ ) : trendingTlds.length > 0 ? ( +
+ {trendingTlds.map((tld) => ( + +
+
+
+ .{tld.tld} + 0 + ? "text-orange-400 bg-orange-400/10 border-orange-400/20" + : "text-accent bg-accent/10 border-accent/20" + )}> + {(tld.price_change || 0) > 0 ? '+' : ''}{(tld.price_change || 0).toFixed(1)}% + +
+

{tld.reason}

+
+ + ))} +
+ ) : ( +
+ +

No trending TLDs available

+
+ )} +
-
+ ) } diff --git a/frontend/src/app/portfolio/page.tsx b/frontend/src/app/portfolio/page.tsx index 454fdaa..a83387c 100644 --- a/frontend/src/app/portfolio/page.tsx +++ b/frontend/src/app/portfolio/page.tsx @@ -4,6 +4,7 @@ import { useEffect, useState } from 'react' import { useStore } from '@/lib/store' import { api, PortfolioDomain, PortfolioSummary, DomainValuation } from '@/lib/api' import { CommandCenterLayout } from '@/components/CommandCenterLayout' +import { PremiumTable, StatCard, PageContainer, TableActionButton } from '@/components/PremiumTable' import { Toast, useToast } from '@/components/Toast' import { Plus, @@ -15,12 +16,12 @@ import { RefreshCw, Loader2, TrendingUp, - TrendingDown, - Tag, - ExternalLink, Sparkles, ArrowUpRight, X, + Briefcase, + PiggyBank, + ShoppingCart, } from 'lucide-react' import clsx from 'clsx' import Link from 'next/link' @@ -230,53 +231,31 @@ export default function PortfolioPage() { } > {toast && } -
+ {/* Summary Stats */} - {summary && ( -
-
-

Total Domains

-

{summary.total_domains}

-
-
-

Total Invested

-

${summary.total_invested?.toLocaleString() || 0}

-
-
-

Est. Value

-

${summary.total_value?.toLocaleString() || 0}

-
-
= 0 - ? "bg-accent/5 border-accent/20" - : "bg-red-500/5 border-red-500/20" - )}> -

Profit/Loss

-

= 0 ? "text-accent" : "text-red-400" - )}> - {(summary.total_profit || 0) >= 0 ? '+' : ''}${summary.total_profit?.toLocaleString() || 0} -

-
-
-

Sold

-

{summary.sold_domains || 0}

-
-
- )} +
+ + + + = 0 ? '+' : ''}$${(summary?.total_profit || 0).toLocaleString()}`} + icon={PiggyBank} + accent={(summary?.total_profit || 0) >= 0} + /> + +
{!canAddMore && (
@@ -292,176 +271,177 @@ export default function PortfolioPage() {
)} - {/* Domain List */} - {loading ? ( -
- {[...Array(3)].map((_, i) => ( -
- ))} -
- ) : portfolio.length === 0 ? ( -
-
- -
-

Your portfolio is empty

-

Add your first domain to start tracking investments

- -
- ) : ( -
- {portfolio.map((domain) => ( -
-
- {/* Domain Info */} -
-

{domain.domain}

-
- {domain.purchase_price && ( - - - Bought: ${domain.purchase_price} - - )} - {domain.registrar && ( - - - {domain.registrar} - - )} - {domain.renewal_date && ( - - - Renews: {new Date(domain.renewal_date).toLocaleDateString()} - - )} -
-
- - {/* Valuation */} - {domain.current_valuation && ( -
-

- ${domain.current_valuation.toLocaleString()} -

-

Est. Value

-
+ {/* Portfolio Table */} + d.id} + loading={loading} + emptyIcon={} + emptyTitle="Your portfolio is empty" + emptyDescription="Add your first domain to start tracking investments" + columns={[ + { + key: 'domain', + header: 'Domain', + render: (domain) => ( +
+ {domain.domain} + {domain.registrar && ( +

+ {domain.registrar} +

)} - - {/* Actions */} -
- - - - - -
-
- ))} -
- )} -
+ ), + }, + { + key: 'purchase', + header: 'Purchase', + hideOnMobile: true, + render: (domain) => ( +
+ {domain.purchase_price && ( + ${domain.purchase_price.toLocaleString()} + )} + {domain.purchase_date && ( +

+ {new Date(domain.purchase_date).toLocaleDateString()} +

+ )} +
+ ), + }, + { + key: 'valuation', + header: 'Est. Value', + align: 'right', + render: (domain) => ( + domain.current_valuation ? ( + ${domain.current_valuation.toLocaleString()} + ) : ( + + ) + ), + }, + { + key: 'renewal', + header: 'Renewal', + hideOnMobile: true, + hideOnTablet: true, + render: (domain) => ( + domain.renewal_date ? ( + + {new Date(domain.renewal_date).toLocaleDateString()} + + ) : ( + + ) + ), + }, + { + key: 'actions', + header: '', + align: 'right', + render: (domain) => ( +
+ handleValuate(domain)} + title="Get valuation" + /> + handleRefresh(domain)} + loading={refreshingId === domain.id} + title="Refresh valuation" + /> + openEditModal(domain)} + title="Edit" + /> + + handleDelete(domain)} + variant="danger" + title="Remove" + /> +
+ ), + }, + ]} + /> + {/* Add Modal */} {showAddModal && ( setShowAddModal(false)}>
- + setAddForm({ ...addForm, domain: e.target.value })} placeholder="example.com" - className="w-full h-10 px-3 bg-background border border-border rounded-lg text-foreground" + className="w-full h-11 px-4 bg-background border border-border/50 rounded-xl text-foreground + focus:outline-none focus:border-accent/50 transition-all" required />
- + setAddForm({ ...addForm, purchase_price: e.target.value })} placeholder="100" - className="w-full h-10 px-3 bg-background border border-border rounded-lg text-foreground" + className="w-full h-11 px-4 bg-background border border-border/50 rounded-xl text-foreground + focus:outline-none focus:border-accent/50" />
- + setAddForm({ ...addForm, purchase_date: e.target.value })} - className="w-full h-10 px-3 bg-background border border-border rounded-lg text-foreground" + className="w-full h-11 px-4 bg-background border border-border/50 rounded-xl text-foreground + focus:outline-none focus:border-accent/50" />
- + setAddForm({ ...addForm, registrar: e.target.value })} placeholder="Namecheap" - className="w-full h-10 px-3 bg-background border border-border rounded-lg text-foreground" + className="w-full h-11 px-4 bg-background border border-border/50 rounded-xl text-foreground + focus:outline-none focus:border-accent/50" />
-
+
+ +
+ + + )} + + {/* Sell Modal */} + {showSellModal && selectedDomain && ( + setShowSellModal(false)}> +
+
+
+ + setSellForm({ ...sellForm, sale_price: e.target.value })} + placeholder="1000" + className="w-full h-11 px-4 bg-background border border-border/50 rounded-xl text-foreground + focus:outline-none focus:border-accent/50" + required + /> +
+
+ + setSellForm({ ...sellForm, sale_date: e.target.value })} + className="w-full h-11 px-4 bg-background border border-border/50 rounded-xl text-foreground + focus:outline-none focus:border-accent/50" + /> +
+
+
+ + +
+
+
+ )} + {/* Valuation Modal */} {showValuationModal && ( { setShowValuationModal(false); setValuation(null); }}> {valuatingDomain ? ( -
- +
+
) : valuation ? (
@@ -485,11 +563,11 @@ export default function PortfolioPage() {

Estimated Value

-
+
Confidence - {valuation.confidence} + {valuation.confidence}
-
+
Formula {valuation.valuation_formula}
@@ -502,7 +580,7 @@ export default function PortfolioPage() { ) } -// Simple Modal Component +// Modal Component function Modal({ title, children, onClose }: { title: string; children: React.ReactNode; onClose: () => void }) { return (
e.stopPropagation()} > -
+

{title}

-
-
+
{children}
) } - diff --git a/frontend/src/app/settings/page.tsx b/frontend/src/app/settings/page.tsx index 92cdfef..c534437 100644 --- a/frontend/src/app/settings/page.tsx +++ b/frontend/src/app/settings/page.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from 'react' import { useRouter } from 'next/navigation' import { CommandCenterLayout } from '@/components/CommandCenterLayout' +import { PageContainer } from '@/components/PremiumTable' import { useStore } from '@/lib/store' import { api, PriceAlert } from '@/lib/api' import { @@ -41,7 +42,7 @@ export default function SettingsPage() { email: '', }) - // Notification preferences (local state - would be persisted via API in production) + // Notification preferences const [notificationPrefs, setNotificationPrefs] = useState({ domain_availability: true, price_alerts: true, @@ -99,7 +100,6 @@ export default function SettingsPage() { try { await api.updateMe({ name: profileForm.name || undefined }) - // Update store with new user info const { checkAuth } = useStore.getState() await checkAuth() setSuccess('Profile updated successfully') @@ -116,7 +116,6 @@ export default function SettingsPage() { setSuccess(null) try { - // Store in localStorage for now (would be API in production) localStorage.setItem('notification_prefs', JSON.stringify(notificationPrefs)) setSuccess('Notification preferences saved') } catch (err) { @@ -126,7 +125,6 @@ export default function SettingsPage() { } } - // Load notification preferences from localStorage useEffect(() => { const saved = localStorage.getItem('notification_prefs') if (saved) { @@ -184,536 +182,382 @@ export default function SettingsPage() { title="Settings" subtitle="Manage your account" > + + {/* Messages */} + {error && ( +
+ +

{error}

+ +
+ )} -
-
+ {success && ( +
+ +

{success}

+ +
+ )} - {/* Messages */} - {error && ( -
- -

{error}

- -
- )} +
+ {/* Sidebar */} +
+ {/* Mobile: Horizontal scroll tabs */} + + + {/* Desktop: Vertical tabs */} + - {success && ( -
- -

{success}

- -
- )} - -
- {/* Sidebar - Horizontal scroll on mobile, vertical on desktop */} -
- {/* Mobile: Horizontal scroll tabs */} - - - {/* Desktop: Vertical tabs */} - - - {/* Plan info - hidden on mobile, shown in content area instead */} -
-
- {isProOrHigher ? : } - {tierName} Plan -
-

- {subscription?.domains_used || 0} / {subscription?.domain_limit || 5} domains tracked -

- {!isProOrHigher && ( - - Upgrade - - - )} + {/* Plan info */} +
+
+ {isProOrHigher ? : } + {tierName} Plan
-
- - {/* Content */} -
- {/* Profile Tab */} - {activeTab === 'profile' && ( -
-

Profile Information

- -
-
- - setProfileForm({ ...profileForm, name: e.target.value })} - placeholder="Your name" - className="w-full px-4 py-3.5 bg-background border border-border rounded-xl text-body text-foreground - placeholder:text-foreground-subtle focus:outline-none focus:ring-1 focus:ring-accent/50 focus:border-accent/50 transition-all" - /> -
- -
- - -

Email cannot be changed

-
- - -
-
- )} - - {/* Notifications Tab */} - {activeTab === 'notifications' && ( -
-
-

Email Preferences

- -
- - - - - -
- - -
- - {/* Active Price Alerts */} -
-

Active Price Alerts

- - {loadingAlerts ? ( -
- -
- ) : priceAlerts.length === 0 ? ( -
- -

No price alerts set

- - Browse TLD prices → - -
- ) : ( -
- {priceAlerts.map((alert) => ( -
-
-
-
- {alert.is_active && ( - - )} -
-
- - .{alert.tld} - -

- Alert on {alert.threshold_percent}% change - {alert.target_price && ` or below $${alert.target_price}`} -

-
-
- -
- ))} -
- )} -
-
- )} - - {/* Billing Tab */} - {activeTab === 'billing' && ( -
- {/* Current Plan */} -
-

Your Current Plan

- -
-
-
- {tierName === 'Tycoon' ? ( - - ) : tierName === 'Trader' ? ( - - ) : ( - - )} -
-

{tierName}

-

- {tierName === 'Scout' ? 'Free forever' : tierName === 'Trader' ? '$9/month' : '$29/month'} -

-
-
- - {isProOrHigher ? 'Active' : 'Free'} - -
- - {/* Plan Stats */} -
-
-

{subscription?.domain_limit || 5}

-

Domains

-
-
-

- {subscription?.check_frequency === 'realtime' ? '10m' : - subscription?.check_frequency === 'hourly' ? '1h' : '24h'} -

-

Check Interval

-
-
-

- {subscription?.portfolio_limit === -1 ? '∞' : subscription?.portfolio_limit || 0} -

-

Portfolio

-
-
- - {isProOrHigher ? ( - - ) : ( - - - Upgrade Plan - - )} -
- - {/* Plan Features */} -

Your Plan Includes

-
    -
  • - - {subscription?.domain_limit || 5} Watchlist Domains -
  • -
  • - - - {subscription?.check_frequency === 'realtime' ? '10-minute' : - subscription?.check_frequency === 'hourly' ? 'Hourly' : 'Daily'} Scans - -
  • -
  • - - Email Alerts -
  • -
  • - - TLD Price Data -
  • - {subscription?.features?.domain_valuation && ( -
  • - - Domain Valuation -
  • - )} - {(subscription?.portfolio_limit ?? 0) !== 0 && ( -
  • - - - {subscription?.portfolio_limit === -1 ? 'Unlimited' : subscription?.portfolio_limit} Portfolio - -
  • - )} - {subscription?.features?.expiration_tracking && ( -
  • - - Expiry Tracking -
  • - )} - {(subscription?.history_days ?? 0) !== 0 && ( -
  • - - - {subscription?.history_days === -1 ? 'Full' : `${subscription?.history_days}-day`} History - -
  • - )} -
-
- - {/* Compare All Plans */} -
-

Compare All Plans

- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FeatureScoutTraderTycoon
PriceFree$9/mo$29/mo
Watchlist Domains550500
Scan FrequencyDailyHourly10 min
Portfolio25Unlimited
Domain Valuation
Price History90 daysUnlimited
Expiry Tracking
-
- - {!isProOrHigher && ( -
- - - Upgrade Now - -
- )} -
-
- )} - - {/* Security Tab */} - {activeTab === 'security' && ( -
-
-

Password

-

- Change your password or reset it if you've forgotten it. -

- - - Change Password - -
- -
-

Account Security

- -
-
-
-

Email Verified

-

Your email address has been verified

-
-
- -
-
- -
-
-

Two-Factor Authentication

-

Coming soon

-
- Soon -
-
-
- -
-

Danger Zone

-

- Permanently delete your account and all associated data. -

- -
-
+

+ {subscription?.domains_used || 0} / {subscription?.domain_limit || 5} domains tracked +

+ {!isProOrHigher && ( + + Upgrade + + )}
+ + {/* Content */} +
+ {/* Profile Tab */} + {activeTab === 'profile' && ( +
+

Profile Information

+ +
+
+ + setProfileForm({ ...profileForm, name: e.target.value })} + placeholder="Your name" + className="w-full h-11 px-4 bg-background border border-border/50 rounded-xl text-foreground + placeholder:text-foreground-subtle focus:outline-none focus:border-accent/50 transition-all" + /> +
+ +
+ + +

Email cannot be changed

+
+ + +
+
+ )} + + {/* Notifications Tab */} + {activeTab === 'notifications' && ( +
+
+

Email Preferences

+ +
+ {[ + { key: 'domain_availability', label: 'Domain Availability', desc: 'Get notified when watched domains become available' }, + { key: 'price_alerts', label: 'Price Alerts', desc: 'Get notified when TLD prices change' }, + { key: 'weekly_digest', label: 'Weekly Digest', desc: 'Receive a weekly summary of your portfolio' }, + ].map((item) => ( + + ))} +
+ + +
+ + {/* Active Price Alerts */} +
+

Active Price Alerts

+ + {loadingAlerts ? ( +
+ +
+ ) : priceAlerts.length === 0 ? ( +
+ +

No price alerts set

+ + Browse TLD prices → + +
+ ) : ( +
+ {priceAlerts.map((alert) => ( +
+
+
+
+ {alert.is_active && ( + + )} +
+
+ + .{alert.tld} + +

+ Alert on {alert.threshold_percent}% change + {alert.target_price && ` or below $${alert.target_price}`} +

+
+
+ +
+ ))} +
+ )} +
+
+ )} + + {/* Billing Tab */} + {activeTab === 'billing' && ( +
+ {/* Current Plan */} +
+

Your Current Plan

+ +
+
+
+ {tierName === 'Tycoon' ? : tierName === 'Trader' ? : } +
+

{tierName}

+

+ {tierName === 'Scout' ? 'Free forever' : tierName === 'Trader' ? '$9/month' : '$29/month'} +

+
+
+ + {isProOrHigher ? 'Active' : 'Free'} + +
+ + {/* Plan Stats */} +
+
+

{subscription?.domain_limit || 5}

+

Domains

+
+
+

+ {subscription?.check_frequency === 'realtime' ? '10m' : + subscription?.check_frequency === 'hourly' ? '1h' : '24h'} +

+

Check Interval

+
+
+

+ {subscription?.portfolio_limit === -1 ? '∞' : subscription?.portfolio_limit || 0} +

+

Portfolio

+
+
+ + {isProOrHigher ? ( + + ) : ( + + + Upgrade Plan + + )} +
+ + {/* Plan Features */} +

Your Plan Includes

+
    + {[ + `${subscription?.domain_limit || 5} Watchlist Domains`, + `${subscription?.check_frequency === 'realtime' ? '10-minute' : subscription?.check_frequency === 'hourly' ? 'Hourly' : 'Daily'} Scans`, + 'Email Alerts', + 'TLD Price Data', + ].map((feature) => ( +
  • + + {feature} +
  • + ))} +
+
+
+ )} + + {/* Security Tab */} + {activeTab === 'security' && ( +
+
+

Password

+

+ Change your password or reset it if you've forgotten it. +

+ + + Change Password + +
+ +
+

Account Security

+ +
+
+
+

Email Verified

+

Your email address has been verified

+
+
+ +
+
+ +
+
+

Two-Factor Authentication

+

Coming soon

+
+ Soon +
+
+
+ +
+

Danger Zone

+

+ Permanently delete your account and all associated data. +

+ +
+
+ )} +
-
+
) } - diff --git a/frontend/src/app/watchlist/page.tsx b/frontend/src/app/watchlist/page.tsx index bfb1172..14d4bd3 100644 --- a/frontend/src/app/watchlist/page.tsx +++ b/frontend/src/app/watchlist/page.tsx @@ -4,6 +4,7 @@ import { useEffect, useState } from 'react' import { useStore } from '@/lib/store' import { api } from '@/lib/api' import { CommandCenterLayout } from '@/components/CommandCenterLayout' +import { PremiumTable, Badge, StatCard, PageContainer, TableActionButton } from '@/components/PremiumTable' import { Toast, useToast } from '@/components/Toast' import { Plus, @@ -14,10 +15,11 @@ import { BellOff, History, ExternalLink, - MoreVertical, Search, - Filter, + Eye, + Sparkles, ArrowUpRight, + X, } from 'lucide-react' import clsx from 'clsx' import Link from 'next/link' @@ -29,57 +31,6 @@ interface DomainHistory { checked_at: string } -// Status indicator component with traffic light system -function StatusIndicator({ domain }: { domain: any }) { - // Determine status based on domain data - let status: 'available' | 'watching' | 'stable' = 'stable' - let label = 'Stable' - let description = 'Domain is registered and active' - - if (domain.is_available) { - status = 'available' - label = 'Available' - description = 'Domain is available for registration!' - } else if (domain.status === 'checking' || domain.status === 'pending') { - status = 'watching' - label = 'Watching' - description = 'Monitoring for changes' - } - - const colors = { - available: 'bg-accent text-accent', - watching: 'bg-amber-400 text-amber-400', - stable: 'bg-foreground-muted text-foreground-muted', - } - - return ( -
-
- - {status === 'available' && ( - - )} -
-
-

- {label} -

-

{description}

-
-
- ) -} - export default function WatchlistPage() { const { domains, addDomain, deleteDomain, refreshDomain, subscription } = useStore() const { toast, showToast, hideToast } = useToast() @@ -97,11 +48,9 @@ export default function WatchlistPage() { // Filter domains const filteredDomains = domains?.filter(domain => { - // Search filter if (searchQuery && !domain.name.toLowerCase().includes(searchQuery.toLowerCase())) { return false } - // Status filter if (filterStatus === 'available' && !domain.is_available) return false if (filterStatus === 'watching' && domain.is_available) return false return true @@ -110,6 +59,9 @@ export default function WatchlistPage() { // Stats const availableCount = domains?.filter(d => d.is_available).length || 0 const watchingCount = domains?.filter(d => !d.is_available).length || 0 + const domainsUsed = domains?.length || 0 + const domainLimit = subscription?.domain_limit || 5 + const canAddMore = domainsUsed < domainLimit const handleAddDomain = async (e: React.FormEvent) => { e.preventDefault() @@ -168,29 +120,6 @@ export default function WatchlistPage() { } } - const loadHistory = async (domainId: number) => { - if (selectedDomainId === domainId) { - setSelectedDomainId(null) - setDomainHistory(null) - return - } - - setSelectedDomainId(domainId) - setLoadingHistory(true) - try { - const history = await api.getDomainHistory(domainId) - setDomainHistory(history) - } catch (err) { - setDomainHistory([]) - } finally { - setLoadingHistory(false) - } - } - - const domainLimit = subscription?.domain_limit || 5 - const domainsUsed = domains?.length || 0 - const canAddMore = domainsUsed < domainLimit - return ( {toast && } -
+ {/* Stats Cards */} -
-
-

Total Watched

-

{domainsUsed}

-
-
-
-
-

Available

-

{availableCount}

-
-
-
-

Watching

-

{watchingCount}

-
-
-

Limit

-

{domainLimit === -1 ? '∞' : domainLimit}

-
+
+ + + +
{/* Add Domain Form */} -
-
+
+
+ setNewDomain(e.target.value)} placeholder="Enter domain to track (e.g., dream.com)" disabled={!canAddMore} - className={clsx( - "w-full h-12 px-5 bg-background-secondary/50 border border-border/50 rounded-xl", - "text-foreground placeholder:text-foreground-subtle", - "focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/30", - "disabled:opacity-50 disabled:cursor-not-allowed", - "shadow-[0_2px_8px_-2px_rgba(0,0,0,0.1)]" - )} + onKeyDown={(e) => e.key === 'Enter' && handleAddDomain(e)} + className="w-full h-11 pl-11 pr-4 bg-background-secondary/50 border border-border/50 rounded-xl + text-sm text-foreground placeholder:text-foreground-subtle + focus:outline-none focus:border-accent/50 transition-all + disabled:opacity-50 disabled:cursor-not-allowed" />
- +
{!canAddMore && (
@@ -274,225 +181,165 @@ export default function WatchlistPage() { {/* Filters */}
-
- - - +
+ {[ + { id: 'all' as const, label: 'All', count: domainsUsed }, + { id: 'available' as const, label: 'Available', count: availableCount, color: 'accent' }, + { id: 'watching' as const, label: 'Watching', count: watchingCount }, + ].map((tab) => ( + + ))}
-
- +
+ setSearchQuery(e.target.value)} - placeholder="Search domains..." - className="w-full sm:w-64 h-10 pl-9 pr-4 bg-background-secondary border border-border rounded-lg + placeholder="Filter domains..." + className="w-full h-10 pl-10 pr-10 bg-background-secondary/50 border border-border/50 rounded-xl text-sm text-foreground placeholder:text-foreground-subtle - focus:outline-none focus:border-accent" + focus:outline-none focus:border-accent/50" /> + {searchQuery && ( + + )}
- {/* Domain List */} -
- {filteredDomains.length === 0 ? ( -
- {domainsUsed === 0 ? ( - <> -
- -
-

Your watchlist is empty

-

Add a domain above to start tracking

- - ) : ( - <> - -

No domains match your filters

- - )} -
- ) : ( - filteredDomains.map((domain) => ( -
-
- {/* Domain Name + Status */} -
-
-

- {domain.name} -

- {domain.is_available && ( - - GRAB IT! - - )} -
- -
- - {/* Actions */} -
- {/* Notify Toggle */} - - - {/* History */} - - - {/* Refresh */} - - - {/* Delete */} - - - {/* External Link (if available) */} + {/* Domain Table */} + d.id} + emptyIcon={} + emptyTitle={domainsUsed === 0 ? "Your watchlist is empty" : "No domains match your filters"} + emptyDescription={domainsUsed === 0 ? "Add a domain above to start tracking" : "Try adjusting your filter criteria"} + columns={[ + { + key: 'domain', + header: 'Domain', + render: (domain) => ( +
+
+ {domain.is_available && ( - - Register - - + + )} +
+
+ {domain.name} + {domain.is_available && ( + AVAILABLE )}
- - {/* History Panel */} - {selectedDomainId === domain.id && ( -
-

Status History

- {loadingHistory ? ( -
- - Acquiring targets... -
- ) : domainHistory && domainHistory.length > 0 ? ( -
- {domainHistory.slice(0, 5).map((entry) => ( -
- - - {new Date(entry.checked_at).toLocaleDateString()} at{' '} - {new Date(entry.checked_at).toLocaleTimeString()} - - - {entry.is_available ? 'Available' : 'Registered'} - -
- ))} -
- ) : ( -

No history available yet

- )} -
- )} -
- )) - )} -
-
+ ), + }, + { + key: 'status', + header: 'Status', + hideOnMobile: true, + render: (domain) => ( + + {domain.is_available ? 'Ready to register!' : 'Monitoring...'} + + ), + }, + { + key: 'notifications', + header: 'Alerts', + align: 'center', + hideOnMobile: true, + render: (domain) => ( + + ), + }, + { + key: 'actions', + header: '', + align: 'right', + render: (domain) => ( +
+ handleRefresh(domain.id)} + loading={refreshingId === domain.id} + title="Refresh status" + /> + handleDelete(domain.id, domain.name)} + variant="danger" + loading={deletingId === domain.id} + title="Remove" + /> + {domain.is_available && ( + e.stopPropagation()} + className="flex items-center gap-1.5 px-3 py-2 bg-accent text-background text-xs font-medium + rounded-lg hover:bg-accent-hover transition-colors ml-1" + > + Register + + )} +
+ ), + }, + ]} + /> + ) } - diff --git a/frontend/src/components/PremiumTable.tsx b/frontend/src/components/PremiumTable.tsx index e4fabc9..89941f9 100644 --- a/frontend/src/components/PremiumTable.tsx +++ b/frontend/src/components/PremiumTable.tsx @@ -397,22 +397,27 @@ export function SectionHeader({ subtitle, icon: Icon, action, + compact = false, }: { title: string subtitle?: string icon?: React.ComponentType<{ className?: string }> action?: ReactNode + compact?: boolean }) { return ( -
+
{Icon && ( -
- +
+
)}
-

{title}

+

{title}

{subtitle &&

{subtitle}

}