From b2c773b94ccf69ca3fc42c7f1e46af9797f755ca Mon Sep 17 00:00:00 2001 From: "yves.gugger" Date: Tue, 9 Dec 2025 09:13:51 +0100 Subject: [PATCH] feat: Complete Command Center redesign + fix notify API DASHBOARD REDESIGN (Award-winning UI): - New hero section with gradient icon and tier badge - Modern stats grid with gradient backgrounds - Redesigned domain cards with improved spacing - Better visual hierarchy and typography - Smooth animations and transitions - Quick links section at bottom - Redesigned all modals with rounded corners - Better color system for ROI indicators - Improved mobile responsiveness API FIX: - Fixed PATCH /domains/{id}/notify endpoint - Now accepts body with 'notify' field instead of query param - Resolves 422 Unprocessable Entity error UI IMPROVEMENTS: - Added BellOff icon for disabled notifications - Better loading states with descriptive text - Improved empty states with larger icons - Gradient backgrounds for positive/negative values - Better button hover states --- backend/app/api/domains.py | 10 +- frontend/src/app/dashboard/page.tsx | 882 ++++++++++++++++------------ 2 files changed, 509 insertions(+), 383 deletions(-) diff --git a/backend/app/api/domains.py b/backend/app/api/domains.py index dba383c..2f9f8ea 100644 --- a/backend/app/api/domains.py +++ b/backend/app/api/domains.py @@ -3,6 +3,7 @@ from datetime import datetime from math import ceil from fastapi import APIRouter, HTTPException, status, Query +from pydantic import BaseModel from sqlalchemy import select, func from app.api.deps import Database, CurrentUser @@ -212,10 +213,15 @@ async def refresh_domain( return domain +class NotifyUpdate(BaseModel): + """Schema for updating notification settings.""" + notify: bool + + @router.patch("/{domain_id}/notify", response_model=DomainResponse) async def update_notification_settings( domain_id: int, - notify_on_available: bool, + data: NotifyUpdate, current_user: CurrentUser, db: Database, ): @@ -234,7 +240,7 @@ async def update_notification_settings( detail="Domain not found", ) - domain.notify_on_available = notify_on_available + domain.notify_on_available = data.notify await db.commit() await db.refresh(domain) diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx index c307944..94d540d 100644 --- a/frontend/src/app/dashboard/page.tsx +++ b/frontend/src/app/dashboard/page.tsx @@ -18,6 +18,7 @@ import { History, ChevronRight, Bell, + BellOff, Check, X, Zap, @@ -28,13 +29,18 @@ import { Eye, DollarSign, Tag, - MoreVertical, Edit2, ExternalLink, Sparkles, BarChart3, CreditCard, - Settings, + Target, + Crosshair, + Shield, + Activity, + ArrowUpRight, + ArrowDownRight, + Globe, } from 'lucide-react' import clsx from 'clsx' import Link from 'next/link' @@ -195,7 +201,7 @@ export default function DashboardPage() { const loadDomainHistory = async (domainId: number) => { if (!subscription?.features?.expiration_tracking) { - setError('Check history requires Professional or Enterprise plan') + setError('Check history requires Trader or Tycoon plan') return } @@ -380,9 +386,9 @@ export default function DashboardPage() { const daysUntil = Math.ceil((date.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)) if (daysUntil < 0) return { text: 'Expired', urgent: true } - if (daysUntil <= 7) return { text: `${daysUntil}d left`, urgent: true } - if (daysUntil <= 30) return { text: `${daysUntil}d left`, urgent: false } - return { text: date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }), urgent: false } + if (daysUntil <= 7) return { text: `${daysUntil}d`, urgent: true } + if (daysUntil <= 30) return { text: `${daysUntil}d`, urgent: false } + return { text: date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }), urgent: false } } const formatCurrency = (value: number | null) => { @@ -397,8 +403,11 @@ export default function DashboardPage() { if (isLoading) { return ( -
-
+
+
+
+

Loading your command center...

+
) } @@ -412,107 +421,126 @@ export default function DashboardPage() { : true const availableCount = domains.filter(d => d.is_available).length + const takenCount = domains.filter(d => !d.is_available).length const expiringCount = domains.filter(d => { if (!d.expiration_date) return false const daysUntil = Math.ceil((new Date(d.expiration_date).getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24)) return daysUntil <= 30 && daysUntil > 0 }).length - const tierName = subscription?.tier_name || subscription?.tier || 'Starter' + const tierName = subscription?.tier_name || subscription?.tier || 'Scout' const isProOrHigher = tierName === 'Professional' || tierName === 'Enterprise' || tierName === 'Trader' || tierName === 'Tycoon' const isEnterprise = tierName === 'Enterprise' || tierName === 'Tycoon' return (
- {/* Ambient glow */} -
-
+ {/* Background elements */} +
+
+
-
-
- {/* Header with Tabs */} -
-
+
+
+ {/* Hero Section */} +
+
-

+
+
+ +
+ + {isEnterprise && } + {tierName} + +
+

Command Center

-

+

Your domains. Your intel. Your edge.

- - {isEnterprise && } - {tierName} Plan - - - {/* Billing/Upgrade Button */} {isProOrHigher ? ( ) : ( - + Upgrade )}
+
- {/* Tabs */} -
+ {/* Tab Navigation */} +
+
{error && ( -
+

{error}

-
@@ -520,62 +548,83 @@ export default function DashboardPage() { {/* Watchlist Tab */} {activeTab === 'watchlist' && ( -
- {/* Stats */} - {isProOrHigher && ( -
-
-
- - Tracked +
+ {/* Stats Grid */} +
+
+
+
+
-

{domains.length}

-
-
-
- - Available -
-

{availableCount}

-
-
-
- - Expiring -
-

0 ? "text-warning" : "text-foreground" - )}>{expiringCount}

-
-
-
- - Frequency -
-

- {subscription?.check_frequency || 'Daily'} -

+ Total
+

{domains.length}

+

targets tracked

- )} + +
+
+
+ +
+ Hunt Ready +
+

{availableCount}

+

available now

+
+ +
+
+
+ +
+ Claimed +
+

{takenCount}

+

registered

+
+ +
0 + ? "bg-gradient-to-br from-warning/10 to-warning/5 border-warning/20 hover:border-warning/30" + : "bg-gradient-to-br from-background-secondary/80 to-background-secondary/40 border-border hover:border-border-hover" + )}> +
+
0 ? "bg-warning/10" : "bg-foreground/5" + )}> + 0 ? "text-warning" : "text-foreground-muted")} /> +
+ 0 ? "text-warning/70" : "text-foreground-subtle")}>Expiring +
+

0 ? "text-warning" : "text-foreground" + )}>{expiringCount}

+

0 ? "text-warning/70" : "text-foreground-muted")}> + in 30 days +

+
+
{/* Add Domain Form */} -
+
-
- +
+
setNewDomain(e.target.value)} - placeholder="Add domain to watchlist (e.g., example.com)" + placeholder="Add domain to hunt (e.g., dream.io)" disabled={!canAddMore} - className="w-full pl-12 pr-4 py-3.5 bg-background-secondary border border-border rounded-xl + className="w-full pl-12 pr-4 py-4 bg-background-secondary/50 border border-border rounded-2xl text-body text-foreground placeholder:text-foreground-subtle - focus:outline-none focus:border-border-hover + focus:outline-none focus:border-accent/50 focus:ring-2 focus:ring-accent/10 disabled:opacity-50 disabled:cursor-not-allowed transition-all" /> @@ -583,139 +632,143 @@ export default function DashboardPage() {
+ {!canAddMore && ( +

+ + Watchlist limit reached. Upgrade for more. +

+ )} {/* Domain List */} {domains.length === 0 ? ( -
-
- +
+
+
-

No targets yet

-

- Add a domain above to start hunting +

No targets yet

+

+ Add your first domain above to start tracking availability

) : ( -
- - - - - - {isProOrHigher && ( - - )} - - - - - - {domains.map((domain) => { - const expiration = formatExpirationDate(domain.expiration_date) - return ( - - - - {isProOrHigher && ( - - )} - - - - ) - })} - -
DomainStatusExpirationLast CheckActions
-
-
- {domain.name} -
-
- - {domain.is_available ? 'Available' : 'Registered'} - - - {expiration ? ( +
+ {domains.map((domain, index) => { + const expiration = formatExpirationDate(domain.expiration_date) + return ( +
+
+
+ {/* Status indicator */} +
+ + {/* Domain info */} +
+
+ + {domain.name} + + + {domain.is_available ? 'Available' : 'Registered'} + + {expiration && ( - + {expiration.text} - ) : ( - )} -
- - - {formatDate(domain.last_checked)} - - -
- {isProOrHigher && ( - - )} - - - -
-
+
+ + {formatDate(domain.last_checked)} +
+
+
+ + {/* Actions */} +
+ {isProOrHigher && ( + + )} + + + + +
+
+
+ ) + })}
)}
@@ -723,61 +776,101 @@ export default function DashboardPage() { {/* Portfolio Tab */} {activeTab === 'portfolio' && ( -
+
{/* Portfolio Summary */} {portfolioSummary && ( -
-
-
- - Total Value +
+
+
+
+ +
-

+

{formatCurrency(portfolioSummary.total_value)}

+

total value

-
-
- - Invested + +
+
+
+ +
-

+

{formatCurrency(portfolioSummary.total_invested)}

+

invested

-
-
- - Unrealized P/L + +
= 0 + ? "bg-gradient-to-br from-accent/10 to-accent/5 border-accent/20" + : "bg-gradient-to-br from-danger/10 to-danger/5 border-danger/20" + )}> +
+
= 0 ? "bg-accent/10" : "bg-danger/10" + )}> + {portfolioSummary.unrealized_profit >= 0 ? ( + + ) : ( + + )} +

= 0 ? "text-accent" : "text-danger" )}> {portfolioSummary.unrealized_profit >= 0 ? '+' : ''}{formatCurrency(portfolioSummary.unrealized_profit)}

+

= 0 ? "text-accent/70" : "text-danger/70" + )}>unrealized P/L

-
-
- - ROI + +
= 0 + ? "bg-gradient-to-br from-accent/10 to-accent/5 border-accent/20" + : "bg-gradient-to-br from-danger/10 to-danger/5 border-danger/20" + )}> +
+
= 0 ? "bg-accent/10" : "bg-danger/10" + )}> + = 0 ? "text-accent" : "text-danger" + )} /> +

= 0 ? "text-accent" : "text-danger" )}> {portfolioSummary.overall_roi >= 0 ? '+' : ''}{portfolioSummary.overall_roi.toFixed(1)}%

+

= 0 ? "text-accent/70" : "text-danger/70" + )}>overall ROI

)} {/* Add Domain Button */} -
+
{domain.notes && ( -

+

{domain.notes}

)} @@ -912,44 +1008,64 @@ export default function DashboardPage() {
)} - {/* CTA */} -
-
-
- -
-
-

TLD Price Intelligence

-

Track domain extension pricing trends

-
-
+ {/* Quick Links */} +
- Explore Pricing - +
+
+
+ +
+
+

TLD Price Intelligence

+

Track 886+ domain extensions

+
+
+ +
+ + + +
+
+
+ +
+
+

Smart Pounce Auctions

+

Find undervalued domains

+
+
+ +
+ {/* Modals */} {/* Add Portfolio Domain Modal */} {showAddPortfolioModal && ( -
-
-
-
-

Add Domain to Portfolio

+
+
+
+
+

Add to Portfolio

-
+
setPortfolioForm({ ...portfolioForm, domain: e.target.value })} placeholder="example.com" required - className="w-full px-4 py-3 bg-background border border-border rounded-xl text-body text-foreground - placeholder:text-foreground-subtle focus:outline-none focus:border-border-hover transition-all" + 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:border-accent/50 focus:ring-2 focus:ring-accent/10 transition-all" />
@@ -972,9 +1088,9 @@ export default function DashboardPage() { min="0" value={portfolioForm.purchase_price} onChange={(e) => setPortfolioForm({ ...portfolioForm, purchase_price: e.target.value })} - placeholder="$0.00" - className="w-full px-4 py-3 bg-background border border-border rounded-xl text-body text-foreground - placeholder:text-foreground-subtle focus:outline-none focus:border-border-hover transition-all" + placeholder="0.00" + 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:border-accent/50 focus:ring-2 focus:ring-accent/10 transition-all" />
@@ -983,8 +1099,8 @@ export default function DashboardPage() { type="date" value={portfolioForm.purchase_date} onChange={(e) => setPortfolioForm({ ...portfolioForm, purchase_date: e.target.value })} - className="w-full px-4 py-3 bg-background border border-border rounded-xl text-body text-foreground - focus:outline-none focus:border-border-hover transition-all" + className="w-full px-4 py-3.5 bg-background border border-border rounded-xl text-body text-foreground + focus:outline-none focus:border-accent/50 focus:ring-2 focus:ring-accent/10 transition-all" />
@@ -995,9 +1111,9 @@ export default function DashboardPage() { type="text" value={portfolioForm.registrar} onChange={(e) => setPortfolioForm({ ...portfolioForm, registrar: e.target.value })} - placeholder="e.g., Porkbun, Namecheap" - className="w-full px-4 py-3 bg-background border border-border rounded-xl text-body text-foreground - placeholder:text-foreground-subtle focus:outline-none focus:border-border-hover transition-all" + placeholder="e.g., Porkbun" + 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:border-accent/50 focus:ring-2 focus:ring-accent/10 transition-all" />
@@ -1008,8 +1124,8 @@ export default function DashboardPage() { type="date" value={portfolioForm.renewal_date} onChange={(e) => setPortfolioForm({ ...portfolioForm, renewal_date: e.target.value })} - className="w-full px-4 py-3 bg-background border border-border rounded-xl text-body text-foreground - focus:outline-none focus:border-border-hover transition-all" + className="w-full px-4 py-3.5 bg-background border border-border rounded-xl text-body text-foreground + focus:outline-none focus:border-accent/50 focus:ring-2 focus:ring-accent/10 transition-all" />
@@ -1020,9 +1136,9 @@ export default function DashboardPage() { min="0" value={portfolioForm.renewal_cost} onChange={(e) => setPortfolioForm({ ...portfolioForm, renewal_cost: e.target.value })} - placeholder="$0.00" - className="w-full px-4 py-3 bg-background border border-border rounded-xl text-body text-foreground - placeholder:text-foreground-subtle focus:outline-none focus:border-border-hover transition-all" + placeholder="0.00" + 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:border-accent/50 focus:ring-2 focus:ring-accent/10 transition-all" />
@@ -1032,26 +1148,26 @@ export default function DashboardPage() {