diff --git a/backend/app/api/admin.py b/backend/app/api/admin.py index ccda790..21298b1 100644 --- a/backend/app/api/admin.py +++ b/backend/app/api/admin.py @@ -811,7 +811,7 @@ async def test_email( """Send a test email to the admin user.""" from app.services.email_service import email_service - if not email_service.is_configured: + if not email_service.is_configured(): raise HTTPException( status_code=400, detail="Email service is not configured. Check SMTP settings." diff --git a/backend/app/api/auth.py b/backend/app/api/auth.py index 467e59e..946b9c1 100644 --- a/backend/app/api/auth.py +++ b/backend/app/api/auth.py @@ -160,7 +160,7 @@ async def register( await db.commit() # Send verification email in background - if email_service.is_configured: + if email_service.is_configured(): site_url = os.getenv("SITE_URL", "http://localhost:3000") verify_url = f"{site_url}/verify-email?token={verification_token}" @@ -312,7 +312,7 @@ async def forgot_password( await db.commit() # Send reset email in background - if email_service.is_configured: + if email_service.is_configured(): site_url = os.getenv("SITE_URL", "http://localhost:3000") reset_url = f"{site_url}/reset-password?token={reset_token}" @@ -440,7 +440,7 @@ async def resend_verification( await db.commit() # Send verification email - if email_service.is_configured: + if email_service.is_configured(): site_url = os.getenv("SITE_URL", "http://localhost:3000") verify_url = f"{site_url}/verify-email?token={verification_token}" diff --git a/frontend/src/app/terminal/intel/[tld]/page.tsx b/frontend/src/app/terminal/intel/[tld]/page.tsx index a720dc0..e2509e4 100644 --- a/frontend/src/app/terminal/intel/[tld]/page.tsx +++ b/frontend/src/app/terminal/intel/[tld]/page.tsx @@ -1,8 +1,8 @@ 'use client' -import { useEffect, useState, useMemo, useRef, useCallback, memo } from 'react' +import { useEffect, useState, useMemo, useRef, useCallback } from 'react' import { useParams, useRouter } from 'next/navigation' -import { CommandCenterLayout } from '@/components/CommandCenterLayout' +import { Sidebar } from '@/components/Sidebar' import { useStore } from '@/lib/store' import { api } from '@/lib/api' import { @@ -10,7 +10,6 @@ import { TrendingUp, TrendingDown, Minus, - Calendar, Globe, Building, ExternalLink, @@ -19,20 +18,26 @@ import { Check, X, RefreshCw, - AlertTriangle, DollarSign, BarChart3, - Shield, - ShieldCheck, Loader2, - Info, Lock, Sparkles, Diamond, - Activity, - Zap + Eye, + Gavel, + Target, + Menu, + Settings, + Shield, + LogOut, + Crown, + Zap, + Coins, + Tag } from 'lucide-react' import Link from 'next/link' +import Image from 'next/image' import clsx from 'clsx' // ============================================================================ @@ -120,7 +125,7 @@ function PriceChart({ if (data.length === 0) { return ( -
+
No price history available
@@ -146,7 +151,7 @@ function PriceChart({ return (
setHoveredIndex(null)} > (null) + + // Mobile Menu + const [menuOpen, setMenuOpen] = useState(false) useEffect(() => { + checkAuth() fetchSubscription() if (tld) loadData() - }, [tld, fetchSubscription]) + }, [tld, fetchSubscription, checkAuth]) const loadData = async () => { try { @@ -355,402 +364,566 @@ export default function TldDetailPage() { } const renewalInfo = getRenewalInfo() + + // Mobile Nav + const mobileNavItems = [ + { href: '/terminal/radar', label: 'Radar', icon: Target, active: false }, + { href: '/terminal/market', label: 'Market', icon: Gavel, active: false }, + { href: '/terminal/watchlist', label: 'Watch', icon: Eye, active: false }, + { href: '/terminal/intel', label: 'Intel', icon: TrendingUp, active: true }, + ] + + const tierName = subscription?.tier_name || subscription?.tier || 'Scout' + const TierIcon = tierName === 'Tycoon' ? Crown : tierName === 'Trader' ? TrendingUp : Zap + + const drawerNavSections = [ + { + title: 'Discover', + items: [ + { href: '/terminal/radar', label: 'Radar', icon: Target }, + { href: '/terminal/market', label: 'Market', icon: Gavel }, + { href: '/terminal/intel', label: 'Intel', icon: TrendingUp }, + ] + }, + { + title: 'Manage', + items: [ + { href: '/terminal/watchlist', label: 'Watchlist', icon: Eye }, + { href: '/terminal/sniper', label: 'Sniper', icon: Target }, + ] + }, + { + title: 'Monetize', + items: [ + { href: '/terminal/yield', label: 'Yield', icon: Coins, isNew: true }, + { href: '/terminal/listing', label: 'For Sale', icon: Tag }, + ] + } + ] if (loading) { return ( - -
- -
- +
+ +
) } if (error || !details) { return ( - -
- -

TLD Not Found

-

The extension .{tld} is not currently tracked.

- - Back to Intel - -
-
+
+ +

TLD Not Found

+

The extension .{tld} is not currently tracked.

+ + Back to Intel + +
) } return ( - - {/* ═══════════════════════════════════════════════════════════════════════ */} - {/* HEADER */} - {/* ═══════════════════════════════════════════════════════════════════════ */} -
-
-
- {/* Breadcrumb */} - - -
-
-
-
-

- .{details.tld} -

- {/* Risk Badge */} - - {details.risk_level} risk - -
-

{details.description}

-
-
-
- -
-
- - {userTier} -
-
-
-
- - {/* ═══════════════════════════════════════════════════════════════════════ */} - {/* STATS GRID */} - {/* ═══════════════════════════════════════════════════════════════════════ */} -
-
- {/* Registration */} -
-
- - Registration -
-
${details.pricing.min.toFixed(2)}
-
at {details.cheapest_registrar}
-
- - {/* Renewal */} -
-
- - Renewal -
- {canSeeRenewal ? ( - <> -
+ {/* Desktop Sidebar */} +
+ +
+ + {/* Main Content */} +
+ + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* MOBILE HEADER */} + {/* ═══════════════════════════════════════════════════════════════════════ */} +
+
+ {/* Back + Title */} +
+ + + +
+
+ .{details.tld} + - ${details.min_renewal_price.toFixed(2)} -
- {renewalInfo?.isTrap && ( -
{renewalInfo.ratio.toFixed(1)}x markup
- )} - - ) : ( -
- - + {details.risk_level} +
- )} -
- - {/* 1Y Trend */} -
-
- {details.price_change_1y > 0 ? : } - 1Y Trend
-
5 ? "text-orange-400" : - details.price_change_1y < -5 ? "text-accent" : - "text-white/40" - )}> - {details.price_change_1y > 0 ? '+' : ''}{details.price_change_1y.toFixed(0)}% -
-
- - {/* 3Y Trend */} -
-
- - 3Y Trend -
- {canSeeFullHistory ? ( -
10 ? "text-orange-400" : - details.price_change_3y < -10 ? "text-accent" : - "text-white/40" - )}> - {details.price_change_3y > 0 ? '+' : ''}{details.price_change_3y.toFixed(0)}% -
- ) : ( -
- - -
- )} -
-
-
- - {/* ═══════════════════════════════════════════════════════════════════════ */} - {/* MAIN CONTENT */} - {/* ═══════════════════════════════════════════════════════════════════════ */} -
-
- - {/* Left Column: Chart + Search */} -
- {/* Price Chart */} -
- {/* Lock overlay for Scout */} - {!canAccessDetailPage && ( -
-
- + {/* Stats Grid */} +
+
+
${details.pricing.min.toFixed(0)}
+
Reg
+
+
+
+ {canSeeRenewal ? `$${details.min_renewal_price.toFixed(0)}` : '—'} +
+
Renew
+
+
+
5 ? "text-orange-400" : details.price_change_1y < -5 ? "text-accent" : "text-white/40" + )}> + {details.price_change_1y > 0 ? '+' : ''}{details.price_change_1y.toFixed(0)}% +
+
1Y
+
+
+
10 ? "text-orange-400" : details.price_change_3y < -10 ? "text-accent" : "text-white/40") + : "text-white/20" + )}> + {canSeeFullHistory ? `${details.price_change_3y > 0 ? '+' : ''}${details.price_change_3y.toFixed(0)}%` : '—'} +
+
3Y
+
+
+
+ + + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* DESKTOP HEADER */} + {/* ═══════════════════════════════════════════════════════════════════════ */} +
+
+
+ {/* Breadcrumb */} + + +
+
+
+
+

+ .{details.tld} +

+ + {details.risk_level} risk +
-

Charts Locked

-

- Upgrade to Trader for detailed price history. -

- {details.description}

+
+
+
+ +
+
+
${details.pricing.min.toFixed(2)}
+
Registration
+
+ {canSeeRenewal && ( +
+
+ ${details.min_renewal_price.toFixed(2)} +
+
Renewal
+
+ )} +
+
+
+ + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* CONTENT */} + {/* ═══════════════════════════════════════════════════════════════════════ */} +
+
+ + {/* Left Column: Chart + Search */} +
+ + {/* Price Chart */} +
+ {/* Lock overlay for Scout */} + {!canAccessDetailPage && ( +
+
+ +
+

Charts Locked

+

+ Upgrade for detailed price history. +

+ + + Unlock + +
+ )} + +
+

Price History

+
+ {(['1M', '3M', '1Y', 'ALL'] as ChartPeriod[]).map((period) => { + const isAvailable = availablePeriods.includes(period) + const isActive = chartPeriod === period && isAvailable + return ( + + ) + })} +
+
+ +
+ +
+ +
+
+
High
+
${chartStats.high.toFixed(2)}
+
+
+
Average
+
${chartStats.avg.toFixed(2)}
+
+
+
Low
+
${chartStats.low.toFixed(2)}
+
+
+
+ + {/* Domain Check */} +
+
+
+

+ + Check Availability +

+

Check if your .{details.tld} domain is available

+
+ +
+ setDomainSearch(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleDomainCheck()} + placeholder={`example.${details.tld}`} + className="flex-1 h-10 bg-white/5 border border-white/10 px-4 text-white placeholder:text-white/25 outline-none focus:border-accent/50 font-mono text-sm" + /> + +
+
+ + {domainResult && ( +
+
+
+ {domainResult.is_available ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+
{domainResult.domain}
+
+ {domainResult.is_available ? 'Available' : 'Registered'} +
+
+
+ + {domainResult.is_available && ( + + Register + + + )} +
+
+ )} +
+ + {/* Info Cards */} +
+
+
+ + Type +
+
{details.type}
+
+
+
+ + Registry +
+
{details.registry}
+
+
+
+ + {/* Right Column: Registrars */} +
+
+

Registrar Prices

+
+ +
+ {details.registrars.slice(0, 5).map((registrar, idx) => { + const hasRenewalTrap = registrar.renewal_price / registrar.registration_price > 1.5 + const isBest = idx === 0 && !hasRenewalTrap + + return ( +
+
+
+
{registrar.name}
+ {isBest && Best Value} + {idx === 0 && hasRenewalTrap && canSeeRenewal && ( + Renewal Trap + )} +
+
+
+ ${registrar.registration_price.toFixed(2)} +
+ {canSeeRenewal && ( +
+ ${registrar.renewal_price.toFixed(2)}/yr +
+ )} +
+
+ + Visit + + +
+ ) + })} +
+ + {userTier === 'scout' && ( +
+ - - Unlock + + Upgrade for renewal prices
)} - -
-
-

Price History

-
-
- {(['1M', '3M', '1Y', 'ALL'] as ChartPeriod[]).map((period) => { - const isAvailable = availablePeriods.includes(period) - const isActive = chartPeriod === period && isAvailable - return ( - - ) - })} -
-
- -
- -
- -
-
-
High
-
${chartStats.high.toFixed(2)}
-
-
-
Average
-
${chartStats.avg.toFixed(2)}
-
-
-
Low
-
${chartStats.low.toFixed(2)}
-
-
-
- - {/* Domain Check */} -
-
-
-

- - Check Availability -

-

Check if your .{details.tld} domain is available

-
- -
- setDomainSearch(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && handleDomainCheck()} - placeholder={`example.${details.tld}`} - className="flex-1 h-10 bg-white/5 border border-white/10 px-4 text-white placeholder:text-white/25 outline-none focus:border-accent/50 font-mono text-sm" - /> - -
-
- - {domainResult && ( -
-
-
- {domainResult.is_available ? ( -
- -
- ) : ( -
- -
- )} -
-
{domainResult.domain}
-
- {domainResult.is_available ? 'Available' : 'Registered'} -
-
-
- - {domainResult.is_available && ( - - Buy at {details.cheapest_registrar} - - - )} -
-
- )} -
- - {/* Info Cards */} -
-
-
- - Type -
-
{details.type}
-
-
-
- - Registry -
-
{details.registry}
-
+
- {/* Right Column: Registrars */} -
-
-

Registrar Prices

-
+ {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* MOBILE BOTTOM NAV */} + {/* ═══════════════════════════════════════════════════════════════════════ */} + + + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* MOBILE DRAWER */} + {/* ═══════════════════════════════════════════════════════════════════════ */} + {menuOpen && ( +
+
setMenuOpen(false)} + /> + +
+ +
+
+ Pounce +
+

POUNCE

+

Terminal v1.0

- ) - })} -
- - {userTier === 'scout' && ( -
- +
- )} + +
+ {drawerNavSections.map((section) => ( +
+
+
+ {section.title} +
+
+ {section.items.map((item: any) => ( + setMenuOpen(false)} + className="flex items-center gap-3 px-4 py-2.5 text-white/60 active:text-white active:bg-white/[0.03] transition-colors border-l-2 border-transparent active:border-accent" + > + + {item.label} + {item.isNew && ( + NEW + )} + + ))} +
+
+ ))} + +
+ setMenuOpen(false)} + className="flex items-center gap-3 py-2.5 text-white/50 active:text-white transition-colors" + > + + Settings + + + {user?.is_admin && ( + setMenuOpen(false)} + className="flex items-center gap-3 py-2.5 text-amber-500/70 active:text-amber-400 transition-colors" + > + + Admin + + )} +
+
+ +
+
+
+ +
+
+

+ {user?.name || user?.email?.split('@')[0] || 'User'} +

+

{tierName}

+
+
+ + {tierName === 'Scout' && ( + setMenuOpen(false)} + className="flex items-center justify-center gap-2 w-full py-2.5 bg-accent text-black text-xs font-bold uppercase tracking-wider active:scale-[0.98] transition-all mb-2" + > + + Upgrade + + )} + + +
+
-
-
-
+ )} + +
) } diff --git a/frontend/src/app/terminal/watchlist/page.tsx b/frontend/src/app/terminal/watchlist/page.tsx index 94fbe53..d6c4408 100755 --- a/frontend/src/app/terminal/watchlist/page.tsx +++ b/frontend/src/app/terminal/watchlist/page.tsx @@ -327,10 +327,41 @@ export default function WatchlistPage() { {/* ═══════════════════════════════════════════════════════════════════════ */} - {/* ADD DOMAIN */} + {/* DESKTOP HEADER */} {/* ═══════════════════════════════════════════════════════════════════════ */} -
-
+
+
+
+
+
+ Domain Surveillance +
+ +

+ Watchlist + {stats.total} +

+
+ +
+
+
{stats.available}
+
Available
+
+
+
{stats.expiring}
+
Expiring
+
+
+
+
+ + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* ADD DOMAIN + FILTERS */} + {/* ═══════════════════════════════════════════════════════════════════════ */} +
+ {/* Add Domain Form */} +
-
- - {/* ═══════════════════════════════════════════════════════════════════════ */} - {/* FILTERS */} - {/* ═══════════════════════════════════════════════════════════════════════ */} -
+ + {/* Filters */}
{[ { value: 'all', label: 'All', count: stats.total }, @@ -387,36 +414,6 @@ export default function WatchlistPage() {
- {/* ═══════════════════════════════════════════════════════════════════════ */} - {/* DESKTOP HEADER */} - {/* ═══════════════════════════════════════════════════════════════════════ */} -
-
-
-
-
- Domain Surveillance -
- -

- Watchlist - {stats.total} -

-
- -
-
-
{stats.available}
-
Available
-
-
-
{stats.expiring}
-
Expiring
-
-
-
-
- {/* ═══════════════════════════════════════════════════════════════════════ */} {/* DOMAIN LIST */} {/* ═══════════════════════════════════════════════════════════════════════ */}