refactor: Dashboard & Navigation overhaul
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
DASHBOARD CHANGES: - Stats cards now all neutral (no colored backgrounds) - Added limit warning with upgrade CTA when domain limit reached - Removed TLD Price Intelligence section - Portfolio now uses table layout like Watchlist - Consistent styling across both tabs NAVIGATION OVERHAUL: - New notification center with dropdown - Shows available domains with pulsing indicator - Empty state when no notifications - Link to manage notifications - New user dropdown menu - Shows user info and plan tier - Domain usage (X/Y domains) - Quick links to Dashboard, TLD Pricing, Settings - Clean logout option - Dashboard button more prominent - Plans link hidden when logged in - Click-outside closes dropdowns MOBILE: - User info shown at top of mobile menu - Plan tier and domain usage visible - All navigation items accessible
This commit is contained in:
@ -13,29 +13,22 @@ import {
|
|||||||
Loader2,
|
Loader2,
|
||||||
Clock,
|
Clock,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
Search,
|
|
||||||
Calendar,
|
Calendar,
|
||||||
History,
|
History,
|
||||||
ChevronRight,
|
|
||||||
Bell,
|
Bell,
|
||||||
BellOff,
|
BellOff,
|
||||||
Check,
|
Check,
|
||||||
X,
|
X,
|
||||||
Zap,
|
Zap,
|
||||||
Crown,
|
Crown,
|
||||||
TrendingUp,
|
|
||||||
Briefcase,
|
Briefcase,
|
||||||
Eye,
|
Eye,
|
||||||
DollarSign,
|
DollarSign,
|
||||||
Tag,
|
Tag,
|
||||||
Edit2,
|
Edit2,
|
||||||
ExternalLink,
|
|
||||||
Sparkles,
|
Sparkles,
|
||||||
BarChart3,
|
|
||||||
CreditCard,
|
CreditCard,
|
||||||
Globe,
|
Globe,
|
||||||
ArrowUpRight,
|
|
||||||
ArrowDownRight,
|
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
@ -465,25 +458,37 @@ export default function DashboardPage() {
|
|||||||
<p className="text-ui-xs text-foreground-muted uppercase tracking-wider mb-2">Tracked</p>
|
<p className="text-ui-xs text-foreground-muted uppercase tracking-wider mb-2">Tracked</p>
|
||||||
<p className="text-3xl font-display text-foreground">{domains.length}</p>
|
<p className="text-3xl font-display text-foreground">{domains.length}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-5 bg-accent/5 border border-accent/20 rounded-2xl hover:border-accent/40 transition-colors">
|
<div className="p-5 bg-background-secondary border border-border rounded-2xl hover:border-foreground/20 transition-colors">
|
||||||
<p className="text-ui-xs text-accent/80 uppercase tracking-wider mb-2">Available</p>
|
<p className="text-ui-xs text-foreground-muted uppercase tracking-wider mb-2">Available</p>
|
||||||
<p className="text-3xl font-display text-accent">{availableCount}</p>
|
<p className="text-3xl font-display text-foreground">{availableCount}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-5 bg-accent/5 border border-accent/20 rounded-2xl hover:border-accent/40 transition-colors">
|
<div className="p-5 bg-background-secondary border border-border rounded-2xl hover:border-foreground/20 transition-colors">
|
||||||
<p className="text-ui-xs text-accent/80 uppercase tracking-wider mb-2">Monitoring</p>
|
<p className="text-ui-xs text-foreground-muted uppercase tracking-wider mb-2">Monitoring</p>
|
||||||
<p className="text-3xl font-display text-accent">{domains.filter(d => d.notify_on_available).length}</p>
|
<p className="text-3xl font-display text-foreground">{domains.filter(d => d.notify_on_available).length}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={clsx(
|
<div className="p-5 bg-background-secondary border border-border rounded-2xl hover:border-foreground/20 transition-colors">
|
||||||
"p-5 border rounded-2xl transition-colors",
|
<p className="text-ui-xs text-foreground-muted uppercase tracking-wider mb-2">Expiring</p>
|
||||||
expiringCount > 0
|
<p className="text-3xl font-display text-foreground">{expiringCount}</p>
|
||||||
? "bg-warning/5 border-warning/20 hover:border-warning/40"
|
|
||||||
: "bg-background-secondary border-border hover:border-foreground/20"
|
|
||||||
)}>
|
|
||||||
<p className={clsx("text-ui-xs uppercase tracking-wider mb-2", expiringCount > 0 ? "text-warning/80" : "text-foreground-muted")}>Expiring</p>
|
|
||||||
<p className={clsx("text-3xl font-display", expiringCount > 0 ? "text-warning" : "text-foreground")}>{expiringCount}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Limit Warning */}
|
||||||
|
{!canAddMore && (
|
||||||
|
<div className="p-4 bg-warning/5 border border-warning/20 rounded-xl flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<AlertCircle className="w-5 h-5 text-warning shrink-0" />
|
||||||
|
<div>
|
||||||
|
<p className="text-body-sm font-medium text-foreground">You've reached your domain limit</p>
|
||||||
|
<p className="text-body-xs text-foreground-muted">Upgrade to track more domains and unlock premium features</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Link href="/pricing" className="flex items-center justify-center gap-2 px-4 py-2 bg-accent text-background text-ui-sm font-medium rounded-lg hover:bg-accent-hover transition-all whitespace-nowrap">
|
||||||
|
<Zap className="w-4 h-4" />
|
||||||
|
Upgrade Now
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Add Domain */}
|
{/* Add Domain */}
|
||||||
<form onSubmit={handleAddDomain} className="flex gap-3">
|
<form onSubmit={handleAddDomain} className="flex gap-3">
|
||||||
<div className="flex-1 relative">
|
<div className="flex-1 relative">
|
||||||
@ -626,25 +631,14 @@ export default function DashboardPage() {
|
|||||||
<p className="text-ui-xs text-foreground-muted uppercase tracking-wider mb-2">Invested</p>
|
<p className="text-ui-xs text-foreground-muted uppercase tracking-wider mb-2">Invested</p>
|
||||||
<p className="text-2xl sm:text-3xl font-display text-foreground">{formatCurrency(portfolioSummary.total_invested)}</p>
|
<p className="text-2xl sm:text-3xl font-display text-foreground">{formatCurrency(portfolioSummary.total_invested)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={clsx(
|
<div className="p-5 bg-background-secondary border border-border rounded-2xl hover:border-foreground/20 transition-colors">
|
||||||
"p-5 border rounded-2xl transition-colors",
|
<p className="text-ui-xs text-foreground-muted uppercase tracking-wider mb-2">P/L</p>
|
||||||
portfolioSummary.unrealized_profit >= 0
|
|
||||||
? "bg-accent/5 border-accent/20 hover:border-accent/40"
|
|
||||||
: "bg-danger/5 border-danger/20 hover:border-danger/40"
|
|
||||||
)}>
|
|
||||||
<p className={clsx("text-ui-xs uppercase tracking-wider mb-2", portfolioSummary.unrealized_profit >= 0 ? "text-accent/80" : "text-danger/80")}>P/L</p>
|
|
||||||
<p className={clsx("text-2xl sm:text-3xl font-display flex items-center gap-1", portfolioSummary.unrealized_profit >= 0 ? "text-accent" : "text-danger")}>
|
<p className={clsx("text-2xl sm:text-3xl font-display flex items-center gap-1", portfolioSummary.unrealized_profit >= 0 ? "text-accent" : "text-danger")}>
|
||||||
{portfolioSummary.unrealized_profit >= 0 ? <ArrowUpRight className="w-5 h-5" /> : <ArrowDownRight className="w-5 h-5" />}
|
{portfolioSummary.unrealized_profit >= 0 ? '+' : ''}{formatCurrency(portfolioSummary.unrealized_profit)}
|
||||||
{formatCurrency(Math.abs(portfolioSummary.unrealized_profit))}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={clsx(
|
<div className="p-5 bg-background-secondary border border-border rounded-2xl hover:border-foreground/20 transition-colors">
|
||||||
"p-5 border rounded-2xl transition-colors",
|
<p className="text-ui-xs text-foreground-muted uppercase tracking-wider mb-2">ROI</p>
|
||||||
portfolioSummary.overall_roi >= 0
|
|
||||||
? "bg-accent/5 border-accent/20 hover:border-accent/40"
|
|
||||||
: "bg-danger/5 border-danger/20 hover:border-danger/40"
|
|
||||||
)}>
|
|
||||||
<p className={clsx("text-ui-xs uppercase tracking-wider mb-2", portfolioSummary.overall_roi >= 0 ? "text-accent/80" : "text-danger/80")}>ROI</p>
|
|
||||||
<p className={clsx("text-2xl sm:text-3xl font-display", portfolioSummary.overall_roi >= 0 ? "text-accent" : "text-danger")}>
|
<p className={clsx("text-2xl sm:text-3xl font-display", portfolioSummary.overall_roi >= 0 ? "text-accent" : "text-danger")}>
|
||||||
{portfolioSummary.overall_roi >= 0 ? '+' : ''}{portfolioSummary.overall_roi.toFixed(1)}%
|
{portfolioSummary.overall_roi >= 0 ? '+' : ''}{portfolioSummary.overall_roi.toFixed(1)}%
|
||||||
</p>
|
</p>
|
||||||
@ -652,9 +646,10 @@ export default function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<button onClick={() => setShowAddPortfolioModal(true)} className="flex items-center gap-2.5 px-6 py-3.5 bg-foreground text-background text-ui font-medium rounded-xl hover:bg-foreground/90 transition-all shadow-lg shadow-foreground/10">
|
{/* Add Domain */}
|
||||||
|
<button onClick={() => setShowAddPortfolioModal(true)} className="flex items-center gap-2 px-5 py-3 bg-foreground text-background text-ui-sm font-medium rounded-xl hover:bg-foreground/90 transition-all">
|
||||||
<Plus className="w-4 h-4" />
|
<Plus className="w-4 h-4" />
|
||||||
Add Domain to Portfolio
|
Add Domain
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{loadingPortfolio ? (
|
{loadingPortfolio ? (
|
||||||
@ -668,65 +663,87 @@ export default function DashboardPage() {
|
|||||||
<p className="text-body-sm text-foreground-subtle">Add domains you own to track their value and ROI</p>
|
<p className="text-body-sm text-foreground-subtle">Add domains you own to track their value and ROI</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="border border-border rounded-2xl overflow-hidden bg-background-secondary/30">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead className="bg-background-secondary/80 backdrop-blur-sm">
|
||||||
|
<tr className="border-b border-border">
|
||||||
|
<th className="text-left text-ui-xs font-medium text-foreground-muted uppercase tracking-wider px-5 py-4">Domain</th>
|
||||||
|
<th className="text-left text-ui-xs font-medium text-foreground-muted uppercase tracking-wider px-5 py-4 hidden sm:table-cell">Purchased</th>
|
||||||
|
<th className="text-left text-ui-xs font-medium text-foreground-muted uppercase tracking-wider px-5 py-4 hidden md:table-cell">Value</th>
|
||||||
|
<th className="text-left text-ui-xs font-medium text-foreground-muted uppercase tracking-wider px-5 py-4 hidden lg:table-cell">Renewal</th>
|
||||||
|
<th className="text-right text-ui-xs font-medium text-foreground-muted uppercase tracking-wider px-5 py-4">ROI</th>
|
||||||
|
<th className="text-right text-ui-xs font-medium text-foreground-muted uppercase tracking-wider px-5 py-4">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-border/50">
|
||||||
{portfolio.map((domain) => {
|
{portfolio.map((domain) => {
|
||||||
const roi = domain.roi
|
const roi = domain.roi
|
||||||
const renewal = formatExpirationDate(domain.renewal_date)
|
const renewal = formatExpirationDate(domain.renewal_date)
|
||||||
return (
|
return (
|
||||||
<div key={domain.id} className="group p-4 bg-background-secondary border border-border rounded-xl hover:border-border-hover transition-all">
|
<tr key={domain.id} className="group hover:bg-background-secondary/60 transition-all duration-200">
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
<td className="px-5 py-4">
|
||||||
<div className="flex-1">
|
|
||||||
<div className="flex items-center gap-2 mb-1">
|
|
||||||
<h3 className="font-mono text-body font-medium text-foreground">{domain.domain}</h3>
|
|
||||||
{domain.status === 'sold' && <span className="text-ui-xs px-2 py-0.5 bg-accent/10 text-accent rounded-full">Sold</span>}
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap items-center gap-3 text-body-sm text-foreground-muted">
|
|
||||||
{domain.purchase_price && <span className="flex items-center gap-1"><Tag className="w-3.5 h-3.5" />{formatCurrency(domain.purchase_price)}</span>}
|
|
||||||
{domain.estimated_value && <span className="flex items-center gap-1"><Sparkles className="w-3.5 h-3.5" />{formatCurrency(domain.estimated_value)}</span>}
|
|
||||||
{renewal && <span className={clsx("flex items-center gap-1", renewal.urgent && "text-warning")}><Calendar className="w-3.5 h-3.5" />{renewal.text}</span>}
|
|
||||||
{domain.registrar && <span className="flex items-center gap-1"><Globe className="w-3.5 h-3.5" />{domain.registrar}</span>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{roi !== null && (
|
<div className="relative">
|
||||||
<div className={clsx("text-right px-3 py-1 rounded-lg", roi >= 0 ? "bg-accent/10" : "bg-danger/10")}>
|
<div className={clsx(
|
||||||
<p className="text-ui-xs text-foreground-muted">ROI</p>
|
"w-2.5 h-2.5 rounded-full",
|
||||||
<p className={clsx("text-body font-medium", roi >= 0 ? "text-accent" : "text-danger")}>{roi >= 0 ? '+' : ''}{roi.toFixed(1)}%</p>
|
domain.status === 'sold' ? "bg-accent" : "bg-foreground-subtle"
|
||||||
</div>
|
)} />
|
||||||
)}
|
|
||||||
<div className="flex items-center gap-1 opacity-60 group-hover:opacity-100 transition-opacity">
|
|
||||||
{domain.status !== 'sold' && <button onClick={() => handleOpenSellModal(domain)} className="p-2 text-foreground-subtle hover:text-accent hover:bg-accent/10 rounded-lg transition-all" title="Sell"><DollarSign className="w-4 h-4" /></button>}
|
|
||||||
<button onClick={() => handleOpenEditPortfolio(domain)} className="p-2 text-foreground-subtle hover:text-foreground hover:bg-foreground/5 rounded-lg transition-all" title="Edit"><Edit2 className="w-4 h-4" /></button>
|
|
||||||
<button onClick={() => handleRefreshPortfolioValue(domain.id)} disabled={refreshingPortfolioId === domain.id} className="p-2 text-foreground-subtle hover:text-foreground hover:bg-foreground/5 rounded-lg transition-all" title="Refresh"><RefreshCw className={clsx("w-4 h-4", refreshingPortfolioId === domain.id && "animate-spin")} /></button>
|
|
||||||
<button onClick={() => handleDeletePortfolioDomain(domain.id)} className="p-2 text-foreground-subtle hover:text-danger hover:bg-danger/10 rounded-lg transition-all" title="Remove"><Trash2 className="w-4 h-4" /></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{domain.notes && <p className="mt-3 pt-3 border-t border-border text-body-sm text-foreground-subtle">{domain.notes}</p>}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Quick Links */}
|
|
||||||
<div className="mt-12 p-6 bg-accent/5 border border-accent/20 rounded-2xl flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="w-12 h-12 bg-accent/10 rounded-xl flex items-center justify-center">
|
|
||||||
<TrendingUp className="w-6 h-6 text-accent" />
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-body font-medium text-foreground">TLD Price Intelligence</p>
|
<span className="font-mono text-body-sm text-foreground">{domain.domain}</span>
|
||||||
<p className="text-body-sm text-foreground-muted">Track 886+ domain extensions in real-time</p>
|
{domain.status === 'sold' && <span className="ml-2 text-ui-xs px-1.5 py-0.5 bg-accent/10 text-accent rounded">Sold</span>}
|
||||||
|
{domain.registrar && <p className="text-body-xs text-foreground-subtle">{domain.registrar}</p>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Link href="/tld-pricing" className="text-ui font-medium text-accent hover:text-accent-hover transition-colors flex items-center gap-1.5 group">
|
</td>
|
||||||
Explore Prices
|
<td className="px-5 py-4 hidden sm:table-cell">
|
||||||
<ChevronRight className="w-4 h-4 group-hover:translate-x-0.5 transition-transform" />
|
<span className="text-body-sm text-foreground">{domain.purchase_price ? formatCurrency(domain.purchase_price) : '—'}</span>
|
||||||
</Link>
|
{domain.purchase_date && <p className="text-body-xs text-foreground-subtle">{new Date(domain.purchase_date).toLocaleDateString()}</p>}
|
||||||
|
</td>
|
||||||
|
<td className="px-5 py-4 hidden md:table-cell">
|
||||||
|
<span className="text-body-sm text-foreground">{domain.estimated_value ? formatCurrency(domain.estimated_value) : '—'}</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-5 py-4 hidden lg:table-cell">
|
||||||
|
{renewal ? (
|
||||||
|
<span className={clsx("text-body-sm flex items-center gap-1.5", renewal.urgent ? "text-warning font-medium" : "text-foreground-subtle")}>
|
||||||
|
<Calendar className="w-3.5 h-3.5" />{renewal.text}
|
||||||
|
</span>
|
||||||
|
) : <span className="text-foreground-subtle/50">—</span>}
|
||||||
|
</td>
|
||||||
|
<td className="px-5 py-4 text-right">
|
||||||
|
{roi !== null ? (
|
||||||
|
<span className={clsx("text-body-sm font-medium", roi >= 0 ? "text-accent" : "text-danger")}>
|
||||||
|
{roi >= 0 ? '+' : ''}{roi.toFixed(1)}%
|
||||||
|
</span>
|
||||||
|
) : <span className="text-foreground-subtle/50">—</span>}
|
||||||
|
</td>
|
||||||
|
<td className="px-5 py-4">
|
||||||
|
<div className="flex items-center justify-end gap-0.5 opacity-50 group-hover:opacity-100 transition-opacity">
|
||||||
|
{domain.status !== 'sold' && (
|
||||||
|
<button onClick={() => handleOpenSellModal(domain)} className="p-2 text-foreground-subtle hover:text-accent hover:bg-accent/10 rounded-lg transition-all" title="Mark as Sold">
|
||||||
|
<DollarSign className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button onClick={() => handleOpenEditPortfolio(domain)} className="p-2 text-foreground-subtle hover:text-foreground hover:bg-foreground/10 rounded-lg transition-all" title="Edit">
|
||||||
|
<Edit2 className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button onClick={() => handleRefreshPortfolioValue(domain.id)} disabled={refreshingPortfolioId === domain.id} className="p-2 text-foreground-subtle hover:text-foreground hover:bg-foreground/10 rounded-lg transition-all" title="Refresh Value">
|
||||||
|
<RefreshCw className={clsx("w-4 h-4", refreshingPortfolioId === domain.id && "animate-spin")} />
|
||||||
|
</button>
|
||||||
|
<button onClick={() => handleDeletePortfolioDomain(domain.id)} className="p-2 text-foreground-subtle hover:text-danger hover:bg-danger/10 rounded-lg transition-all" title="Remove">
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
@ -2,12 +2,37 @@
|
|||||||
|
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useStore } from '@/lib/store'
|
import { useStore } from '@/lib/store'
|
||||||
import { LogOut, LayoutDashboard, Menu, X, Settings } from 'lucide-react'
|
import { LogOut, LayoutDashboard, Menu, X, Settings, Bell, User, ChevronDown, TrendingUp, Briefcase, Eye } from 'lucide-react'
|
||||||
import { useState } from 'react'
|
import { useState, useRef, useEffect } from 'react'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
const { isAuthenticated, user, logout } = useStore()
|
const { isAuthenticated, user, logout, domains, subscription } = useStore()
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||||
|
const [userMenuOpen, setUserMenuOpen] = useState(false)
|
||||||
|
const [notificationsOpen, setNotificationsOpen] = useState(false)
|
||||||
|
const userMenuRef = useRef<HTMLDivElement>(null)
|
||||||
|
const notificationsRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
// Close dropdowns when clicking outside
|
||||||
|
useEffect(() => {
|
||||||
|
function handleClickOutside(event: MouseEvent) {
|
||||||
|
if (userMenuRef.current && !userMenuRef.current.contains(event.target as Node)) {
|
||||||
|
setUserMenuOpen(false)
|
||||||
|
}
|
||||||
|
if (notificationsRef.current && !notificationsRef.current.contains(event.target as Node)) {
|
||||||
|
setNotificationsOpen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('mousedown', handleClickOutside)
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Count notifications (available domains, etc.)
|
||||||
|
const availableDomains = domains?.filter(d => d.is_available) || []
|
||||||
|
const hasNotifications = availableDomains.length > 0
|
||||||
|
|
||||||
|
const tierName = subscription?.tier_name || subscription?.tier || 'Scout'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="fixed top-0 left-0 right-0 z-50 bg-background/80 backdrop-blur-xl border-b border-border-subtle">
|
<header className="fixed top-0 left-0 right-0 z-50 bg-background/80 backdrop-blur-xl border-b border-border-subtle">
|
||||||
@ -50,6 +75,7 @@ export function Header() {
|
|||||||
>
|
>
|
||||||
Auctions
|
Auctions
|
||||||
</Link>
|
</Link>
|
||||||
|
{!isAuthenticated && (
|
||||||
<Link
|
<Link
|
||||||
href="/pricing"
|
href="/pricing"
|
||||||
className="flex items-center h-9 px-3 text-[0.8125rem] text-foreground-muted hover:text-foreground
|
className="flex items-center h-9 px-3 text-[0.8125rem] text-foreground-muted hover:text-foreground
|
||||||
@ -57,45 +83,158 @@ export function Header() {
|
|||||||
>
|
>
|
||||||
Plans
|
Plans
|
||||||
</Link>
|
</Link>
|
||||||
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right side: Auth Links - vertically centered */}
|
{/* Right side: Auth Links - vertically centered */}
|
||||||
<nav className="hidden sm:flex items-center h-full gap-1">
|
<nav className="hidden sm:flex items-center h-full gap-2">
|
||||||
{isAuthenticated ? (
|
{isAuthenticated ? (
|
||||||
<>
|
<>
|
||||||
|
{/* Dashboard Link */}
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard"
|
href="/dashboard"
|
||||||
className="flex items-center gap-2 h-9 px-4 text-[0.8125rem] text-foreground-muted
|
className="flex items-center gap-2 h-9 px-4 text-[0.8125rem] font-medium text-foreground
|
||||||
hover:text-foreground hover:bg-background-secondary rounded-lg
|
bg-foreground/5 hover:bg-foreground/10 rounded-lg transition-all duration-300"
|
||||||
transition-all duration-300"
|
|
||||||
>
|
>
|
||||||
<LayoutDashboard className="w-4 h-4" />
|
<LayoutDashboard className="w-4 h-4" />
|
||||||
<span>Dashboard</span>
|
<span>Dashboard</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
{/* Notifications */}
|
||||||
|
<div ref={notificationsRef} className="relative">
|
||||||
|
<button
|
||||||
|
onClick={() => setNotificationsOpen(!notificationsOpen)}
|
||||||
|
className={clsx(
|
||||||
|
"relative flex items-center justify-center w-9 h-9 rounded-lg transition-all duration-300",
|
||||||
|
notificationsOpen ? "bg-foreground/10 text-foreground" : "text-foreground-muted hover:text-foreground hover:bg-foreground/5"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Bell className="w-4 h-4" />
|
||||||
|
{hasNotifications && (
|
||||||
|
<span className="absolute top-1.5 right-1.5 w-2 h-2 bg-accent rounded-full">
|
||||||
|
<span className="absolute inset-0 rounded-full bg-accent animate-ping opacity-50" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Notifications Dropdown */}
|
||||||
|
{notificationsOpen && (
|
||||||
|
<div className="absolute right-0 top-full mt-2 w-80 bg-background-secondary border border-border rounded-xl shadow-2xl overflow-hidden">
|
||||||
|
<div className="p-4 border-b border-border">
|
||||||
|
<h3 className="text-body-sm font-medium text-foreground">Notifications</h3>
|
||||||
|
</div>
|
||||||
|
<div className="max-h-80 overflow-y-auto">
|
||||||
|
{availableDomains.length > 0 ? (
|
||||||
|
<div className="p-2">
|
||||||
|
{availableDomains.map((domain) => (
|
||||||
|
<Link
|
||||||
|
key={domain.id}
|
||||||
|
href="/dashboard"
|
||||||
|
onClick={() => setNotificationsOpen(false)}
|
||||||
|
className="flex items-start gap-3 p-3 hover:bg-foreground/5 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<div className="w-8 h-8 bg-accent/10 rounded-lg flex items-center justify-center shrink-0">
|
||||||
|
<Eye className="w-4 h-4 text-accent" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-body-sm font-medium text-foreground truncate">{domain.name}</p>
|
||||||
|
<p className="text-body-xs text-accent">Domain is available!</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="p-8 text-center">
|
||||||
|
<Bell className="w-8 h-8 text-foreground-subtle mx-auto mb-3" />
|
||||||
|
<p className="text-body-sm text-foreground-muted">No notifications</p>
|
||||||
|
<p className="text-body-xs text-foreground-subtle mt-1">We'll notify you when domains become available</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<Link
|
<Link
|
||||||
href="/settings"
|
href="/settings"
|
||||||
className="flex items-center justify-center w-9 h-9 text-foreground-subtle hover:text-foreground hover:bg-background-secondary rounded-lg transition-all duration-300"
|
onClick={() => setNotificationsOpen(false)}
|
||||||
title="Settings"
|
className="block p-3 text-center text-body-xs text-foreground-muted hover:text-foreground hover:bg-foreground/5 border-t border-border transition-colors"
|
||||||
|
>
|
||||||
|
Manage notifications
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* User Menu */}
|
||||||
|
<div ref={userMenuRef} className="relative">
|
||||||
|
<button
|
||||||
|
onClick={() => setUserMenuOpen(!userMenuOpen)}
|
||||||
|
className={clsx(
|
||||||
|
"flex items-center gap-2 h-9 pl-3 pr-2 rounded-lg transition-all duration-300",
|
||||||
|
userMenuOpen ? "bg-foreground/10" : "hover:bg-foreground/5"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="w-6 h-6 bg-accent/10 rounded-full flex items-center justify-center">
|
||||||
|
<User className="w-3.5 h-3.5 text-accent" />
|
||||||
|
</div>
|
||||||
|
<ChevronDown className={clsx("w-3.5 h-3.5 text-foreground-muted transition-transform", userMenuOpen && "rotate-180")} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* User Dropdown */}
|
||||||
|
{userMenuOpen && (
|
||||||
|
<div className="absolute right-0 top-full mt-2 w-64 bg-background-secondary border border-border rounded-xl shadow-2xl overflow-hidden">
|
||||||
|
{/* User Info */}
|
||||||
|
<div className="p-4 border-b border-border">
|
||||||
|
<p className="text-body-sm font-medium text-foreground truncate">{user?.name || user?.email}</p>
|
||||||
|
<p className="text-body-xs text-foreground-muted truncate">{user?.email}</p>
|
||||||
|
<div className="flex items-center gap-2 mt-2">
|
||||||
|
<span className="text-ui-xs px-2 py-0.5 bg-foreground/5 text-foreground-muted rounded-full">{tierName}</span>
|
||||||
|
<span className="text-ui-xs text-foreground-subtle">{subscription?.domains_used || 0}/{subscription?.domain_limit || 5} domains</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Menu Items */}
|
||||||
|
<div className="p-2">
|
||||||
|
<Link
|
||||||
|
href="/dashboard"
|
||||||
|
onClick={() => setUserMenuOpen(false)}
|
||||||
|
className="flex items-center gap-3 px-3 py-2.5 text-body-sm text-foreground-muted hover:text-foreground hover:bg-foreground/5 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<LayoutDashboard className="w-4 h-4" />
|
||||||
|
Command Center
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/tld-pricing"
|
||||||
|
onClick={() => setUserMenuOpen(false)}
|
||||||
|
className="flex items-center gap-3 px-3 py-2.5 text-body-sm text-foreground-muted hover:text-foreground hover:bg-foreground/5 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<TrendingUp className="w-4 h-4" />
|
||||||
|
TLD Pricing
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/settings"
|
||||||
|
onClick={() => setUserMenuOpen(false)}
|
||||||
|
className="flex items-center gap-3 px-3 py-2.5 text-body-sm text-foreground-muted hover:text-foreground hover:bg-foreground/5 rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
<Settings className="w-4 h-4" />
|
<Settings className="w-4 h-4" />
|
||||||
|
Settings
|
||||||
</Link>
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center h-full gap-3 ml-2 pl-4 border-l border-border">
|
{/* Logout */}
|
||||||
<span className="text-[0.8125rem] text-foreground-subtle hidden md:flex items-center">
|
<div className="p-2 border-t border-border">
|
||||||
{user?.email}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={logout}
|
onClick={() => {
|
||||||
className="flex items-center justify-center w-9 h-9 text-foreground-subtle hover:text-foreground hover:bg-background-secondary
|
logout()
|
||||||
rounded-lg transition-all duration-300"
|
setUserMenuOpen(false)
|
||||||
title="Sign out"
|
}}
|
||||||
|
className="flex items-center gap-3 w-full px-3 py-2.5 text-body-sm text-foreground-muted hover:text-foreground hover:bg-foreground/5 rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
<LogOut className="w-4 h-4" />
|
<LogOut className="w-4 h-4" />
|
||||||
|
Sign out
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@ -132,6 +271,15 @@ export function Header() {
|
|||||||
<nav className="px-4 py-4 space-y-2">
|
<nav className="px-4 py-4 space-y-2">
|
||||||
{isAuthenticated ? (
|
{isAuthenticated ? (
|
||||||
<>
|
<>
|
||||||
|
{/* User Info on Mobile */}
|
||||||
|
<div className="px-4 py-3 mb-2 border-b border-border">
|
||||||
|
<p className="text-body-sm font-medium text-foreground truncate">{user?.name || user?.email}</p>
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
<span className="text-ui-xs px-2 py-0.5 bg-foreground/5 text-foreground-muted rounded-full">{tierName}</span>
|
||||||
|
<span className="text-ui-xs text-foreground-subtle">{subscription?.domains_used || 0}/{subscription?.domain_limit || 5} domains</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard"
|
href="/dashboard"
|
||||||
className="flex items-center gap-3 px-4 py-3 text-body-sm text-foreground-muted
|
className="flex items-center gap-3 px-4 py-3 text-body-sm text-foreground-muted
|
||||||
@ -142,6 +290,26 @@ export function Header() {
|
|||||||
<LayoutDashboard className="w-5 h-5" />
|
<LayoutDashboard className="w-5 h-5" />
|
||||||
<span>Dashboard</span>
|
<span>Dashboard</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/tld-pricing"
|
||||||
|
className="flex items-center gap-3 px-4 py-3 text-body-sm text-foreground-muted
|
||||||
|
hover:text-foreground hover:bg-background-secondary rounded-xl
|
||||||
|
transition-all duration-300"
|
||||||
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
|
>
|
||||||
|
<TrendingUp className="w-5 h-5" />
|
||||||
|
<span>TLD Pricing</span>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/auctions"
|
||||||
|
className="flex items-center gap-3 px-4 py-3 text-body-sm text-foreground-muted
|
||||||
|
hover:text-foreground hover:bg-background-secondary rounded-xl
|
||||||
|
transition-all duration-300"
|
||||||
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
|
>
|
||||||
|
<Briefcase className="w-5 h-5" />
|
||||||
|
<span>Auctions</span>
|
||||||
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="/settings"
|
href="/settings"
|
||||||
className="flex items-center gap-3 px-4 py-3 text-body-sm text-foreground-muted
|
className="flex items-center gap-3 px-4 py-3 text-body-sm text-foreground-muted
|
||||||
|
|||||||
Reference in New Issue
Block a user