EMOJI REMOVAL: - Replaced 🟢🟡🔴 emojis with CSS circles (bg-accent, bg-amber-400, bg-red-400) - Updated TLD Pricing pages (public + command) - Updated Landing Page feature pills - Updated Admin panel feature checklist PORTFOLIO FEATURE: - Added 'Portfolio Health' section to Landing Page under 'Beyond Hunting' - Highlights: SSL Monitor, Expiry Alerts, Valuation, P&L Tracking - Links to /command/portfolio - Uses 'Your Domain Insurance' tagline Portfolio Status: - Public Page: N/A (personal feature, no public page needed) - Command Center: ✅ Fully implemented with Add/Edit/Sell/Valuation - Admin Panel: ✅ Stats visible in Overview - Landing Page: ✅ Now advertised in 'Beyond Hunting' section
523 lines
19 KiB
TypeScript
Executable File
523 lines
19 KiB
TypeScript
Executable File
'use client'
|
|
|
|
import Link from 'next/link'
|
|
import Image from 'next/image'
|
|
import { usePathname } from 'next/navigation'
|
|
import { useStore } from '@/lib/store'
|
|
import {
|
|
LayoutDashboard,
|
|
Eye,
|
|
Briefcase,
|
|
Gavel,
|
|
TrendingUp,
|
|
Settings,
|
|
ChevronLeft,
|
|
ChevronRight,
|
|
LogOut,
|
|
Crown,
|
|
Zap,
|
|
Shield,
|
|
CreditCard,
|
|
Menu,
|
|
X,
|
|
Sparkles,
|
|
Tag,
|
|
Target,
|
|
Link2,
|
|
} from 'lucide-react'
|
|
import { useState, useEffect } from 'react'
|
|
import clsx from 'clsx'
|
|
|
|
interface SidebarProps {
|
|
collapsed?: boolean
|
|
onCollapsedChange?: (collapsed: boolean) => void
|
|
}
|
|
|
|
export function Sidebar({ collapsed: controlledCollapsed, onCollapsedChange }: SidebarProps) {
|
|
const pathname = usePathname()
|
|
const { user, logout, subscription, domains } = useStore()
|
|
|
|
// Internal state for uncontrolled mode
|
|
const [internalCollapsed, setInternalCollapsed] = useState(false)
|
|
const [mobileOpen, setMobileOpen] = useState(false)
|
|
|
|
// Use controlled or uncontrolled state
|
|
const collapsed = controlledCollapsed ?? internalCollapsed
|
|
const setCollapsed = onCollapsedChange ?? setInternalCollapsed
|
|
|
|
// Load collapsed state from localStorage
|
|
useEffect(() => {
|
|
const saved = localStorage.getItem('sidebar-collapsed')
|
|
if (saved) {
|
|
setCollapsed(saved === 'true')
|
|
}
|
|
}, [])
|
|
|
|
// Close mobile menu on route change
|
|
useEffect(() => {
|
|
setMobileOpen(false)
|
|
}, [pathname])
|
|
|
|
// Save collapsed state
|
|
const toggleCollapsed = () => {
|
|
const newState = !collapsed
|
|
setCollapsed(newState)
|
|
localStorage.setItem('sidebar-collapsed', String(newState))
|
|
}
|
|
|
|
const tierName = subscription?.tier_name || subscription?.tier || 'Scout'
|
|
const tierIcon = tierName === 'Tycoon' ? Crown : tierName === 'Trader' ? TrendingUp : Zap
|
|
const TierIcon = tierIcon
|
|
|
|
// Count available domains for notification badge
|
|
const availableCount = domains?.filter(d => d.is_available).length || 0
|
|
|
|
const isTycoon = tierName.toLowerCase() === 'tycoon'
|
|
|
|
// SECTION 1: Discover - External market data
|
|
const discoverItems = [
|
|
{
|
|
href: '/command/auctions',
|
|
label: 'Auctions',
|
|
icon: Gavel,
|
|
badge: null,
|
|
},
|
|
{
|
|
href: '/command/marketplace',
|
|
label: 'Marketplace',
|
|
icon: Tag,
|
|
badge: null,
|
|
},
|
|
{
|
|
href: '/command/pricing',
|
|
label: 'TLD Pricing',
|
|
icon: TrendingUp,
|
|
badge: null,
|
|
},
|
|
]
|
|
|
|
// SECTION 2: Manage - Your own assets and tools
|
|
const manageItems: Array<{
|
|
href: string
|
|
label: string
|
|
icon: any
|
|
badge: number | null
|
|
tycoonOnly?: boolean
|
|
}> = [
|
|
{
|
|
href: '/command/dashboard',
|
|
label: 'Dashboard',
|
|
icon: LayoutDashboard,
|
|
badge: null,
|
|
},
|
|
{
|
|
href: '/command/watchlist',
|
|
label: 'Watchlist',
|
|
icon: Eye,
|
|
badge: availableCount || null,
|
|
},
|
|
{
|
|
href: '/command/portfolio',
|
|
label: 'Portfolio',
|
|
icon: Briefcase,
|
|
badge: null,
|
|
},
|
|
{
|
|
href: '/command/listings',
|
|
label: 'My Listings',
|
|
icon: Tag,
|
|
badge: null,
|
|
},
|
|
{
|
|
href: '/command/alerts',
|
|
label: 'Sniper Alerts',
|
|
icon: Target,
|
|
badge: null,
|
|
},
|
|
{
|
|
href: '/command/seo',
|
|
label: 'SEO Juice',
|
|
icon: Link2,
|
|
badge: null,
|
|
tycoonOnly: true,
|
|
},
|
|
]
|
|
|
|
const bottomItems = [
|
|
{ href: '/command/settings', label: 'Settings', icon: Settings },
|
|
]
|
|
|
|
const isActive = (href: string) => {
|
|
if (href === '/command/dashboard') return pathname === '/command/dashboard' || pathname === '/command'
|
|
return pathname.startsWith(href)
|
|
}
|
|
|
|
const SidebarContent = () => (
|
|
<>
|
|
{/* Logo Section */}
|
|
<div className={clsx(
|
|
"relative h-20 flex items-center border-b border-border/30",
|
|
collapsed ? "justify-center px-2" : "px-4"
|
|
)}>
|
|
<Link href="/" className="flex items-center gap-3 group">
|
|
<div className={clsx(
|
|
"relative flex items-center justify-center transition-all duration-300",
|
|
collapsed ? "w-10 h-10" : "w-12 h-12"
|
|
)}>
|
|
{/* Glow effect behind logo */}
|
|
<div className="absolute inset-0 bg-accent/20 blur-xl rounded-full scale-150 opacity-50 group-hover:opacity-80 transition-opacity" />
|
|
<Image
|
|
src="/pounce-puma.png"
|
|
alt="pounce"
|
|
width={48}
|
|
height={48}
|
|
className={clsx(
|
|
"relative object-contain drop-shadow-[0_0_20px_rgba(16,185,129,0.3)] group-hover:drop-shadow-[0_0_30px_rgba(16,185,129,0.5)] transition-all",
|
|
collapsed ? "w-9 h-9" : "w-12 h-12"
|
|
)}
|
|
/>
|
|
</div>
|
|
{!collapsed && (
|
|
<div className="flex flex-col">
|
|
<span
|
|
className="text-lg font-bold tracking-[0.12em] text-foreground group-hover:text-accent transition-colors"
|
|
style={{ fontFamily: 'var(--font-display), Georgia, serif' }}
|
|
>
|
|
POUNCE
|
|
</span>
|
|
<span className="text-[10px] text-foreground-subtle tracking-wider uppercase">
|
|
Command Center
|
|
</span>
|
|
</div>
|
|
)}
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Main Navigation */}
|
|
<nav className="flex-1 py-6 px-3 overflow-y-auto">
|
|
{/* SECTION 1: Discover */}
|
|
<div className={clsx("mb-6", collapsed ? "px-1" : "px-2")}>
|
|
{!collapsed && (
|
|
<p className="text-[10px] font-semibold text-foreground-subtle/60 uppercase tracking-[0.15em] mb-3">
|
|
Discover
|
|
</p>
|
|
)}
|
|
{collapsed && <div className="h-px bg-border/50 mb-3" />}
|
|
|
|
<div className="space-y-1.5">
|
|
{discoverItems.map((item) => (
|
|
<Link
|
|
key={item.href}
|
|
href={item.href}
|
|
onClick={() => setMobileOpen(false)}
|
|
className={clsx(
|
|
"group relative flex items-center gap-3 px-3 py-3 rounded-xl transition-all duration-300",
|
|
isActive(item.href)
|
|
? "bg-gradient-to-r from-accent/20 to-accent/5 text-foreground border border-accent/20 shadow-[0_0_20px_-5px_rgba(16,185,129,0.2)]"
|
|
: "text-foreground-muted hover:text-foreground hover:bg-foreground/5 border border-transparent"
|
|
)}
|
|
title={collapsed ? item.label : undefined}
|
|
>
|
|
{isActive(item.href) && (
|
|
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-8 bg-accent rounded-r-full shadow-[0_0_10px_rgba(16,185,129,0.5)]" />
|
|
)}
|
|
<div className="relative">
|
|
<item.icon className={clsx(
|
|
"w-5 h-5 transition-all duration-300",
|
|
isActive(item.href)
|
|
? "text-accent drop-shadow-[0_0_8px_rgba(16,185,129,0.5)]"
|
|
: "group-hover:text-foreground"
|
|
)} />
|
|
</div>
|
|
{!collapsed && (
|
|
<span className={clsx(
|
|
"text-sm font-medium transition-colors",
|
|
isActive(item.href) && "text-foreground"
|
|
)}>
|
|
{item.label}
|
|
</span>
|
|
)}
|
|
{!isActive(item.href) && (
|
|
<div className="absolute inset-0 rounded-xl bg-accent/5 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" />
|
|
)}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* SECTION 2: Manage */}
|
|
<div className={clsx("", collapsed ? "px-1" : "px-2")}>
|
|
{!collapsed && (
|
|
<p className="text-[10px] font-semibold text-foreground-subtle/60 uppercase tracking-[0.15em] mb-3">
|
|
Manage
|
|
</p>
|
|
)}
|
|
{collapsed && <div className="h-px bg-border/50 mb-3" />}
|
|
|
|
<div className="space-y-1.5">
|
|
{manageItems.map((item) => {
|
|
const isDisabled = item.tycoonOnly && !isTycoon
|
|
const ItemWrapper = isDisabled ? 'div' : Link
|
|
|
|
return (
|
|
<ItemWrapper
|
|
key={item.href}
|
|
{...(!isDisabled && { href: item.href })}
|
|
onClick={() => !isDisabled && setMobileOpen(false)}
|
|
className={clsx(
|
|
"group relative flex items-center gap-3 px-3 py-3 rounded-xl transition-all duration-300",
|
|
isDisabled
|
|
? "opacity-50 cursor-not-allowed border border-transparent"
|
|
: isActive(item.href)
|
|
? "bg-gradient-to-r from-accent/20 to-accent/5 text-foreground border border-accent/20 shadow-[0_0_20px_-5px_rgba(16,185,129,0.2)]"
|
|
: "text-foreground-muted hover:text-foreground hover:bg-foreground/5 border border-transparent"
|
|
)}
|
|
title={
|
|
isDisabled
|
|
? "SEO Juice Detector: Analyze backlinks, domain authority & find hidden SEO value. Upgrade to Tycoon to unlock."
|
|
: collapsed ? item.label : undefined
|
|
}
|
|
>
|
|
{!isDisabled && isActive(item.href) && (
|
|
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-8 bg-accent rounded-r-full shadow-[0_0_10px_rgba(16,185,129,0.5)]" />
|
|
)}
|
|
<div className="relative">
|
|
<item.icon className={clsx(
|
|
"w-5 h-5 transition-all duration-300",
|
|
isDisabled
|
|
? "text-foreground-subtle"
|
|
: isActive(item.href)
|
|
? "text-accent drop-shadow-[0_0_8px_rgba(16,185,129,0.5)]"
|
|
: "group-hover:text-foreground"
|
|
)} />
|
|
{item.badge && typeof item.badge === 'number' && !isDisabled && (
|
|
<span className="absolute -top-2 -right-2 w-5 h-5 bg-accent text-background
|
|
text-[10px] font-bold rounded-full flex items-center justify-center
|
|
shadow-[0_0_10px_rgba(16,185,129,0.4)] animate-pulse">
|
|
{item.badge > 9 ? '9+' : item.badge}
|
|
</span>
|
|
)}
|
|
</div>
|
|
{!collapsed && (
|
|
<span className={clsx(
|
|
"text-sm font-medium transition-colors flex-1",
|
|
isDisabled ? "text-foreground-subtle" : isActive(item.href) && "text-foreground"
|
|
)}>
|
|
{item.label}
|
|
</span>
|
|
)}
|
|
{/* Lock icon for disabled items */}
|
|
{isDisabled && !collapsed && (
|
|
<Crown className="w-4 h-4 text-amber-400/60" />
|
|
)}
|
|
{!isDisabled && !isActive(item.href) && (
|
|
<div className="absolute inset-0 rounded-xl bg-accent/5 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" />
|
|
)}
|
|
</ItemWrapper>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
{/* Bottom Section */}
|
|
<div className="border-t border-border/30 py-4 px-3 space-y-1.5">
|
|
{/* Admin Link */}
|
|
{user?.is_admin && (
|
|
<Link
|
|
href="/admin"
|
|
onClick={() => setMobileOpen(false)}
|
|
className={clsx(
|
|
"group relative flex items-center gap-3 px-3 py-3 rounded-xl transition-all duration-300",
|
|
pathname.startsWith('/admin')
|
|
? "bg-gradient-to-r from-accent/20 to-accent/5 text-accent border border-accent/30"
|
|
: "text-accent/70 hover:text-accent hover:bg-accent/5 border border-transparent"
|
|
)}
|
|
title={collapsed ? "Admin Panel" : undefined}
|
|
>
|
|
{pathname.startsWith('/admin') && (
|
|
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-8 bg-accent rounded-r-full" />
|
|
)}
|
|
<Shield className="w-5 h-5" />
|
|
{!collapsed && <span className="text-sm font-medium">Admin Panel</span>}
|
|
</Link>
|
|
)}
|
|
|
|
{/* Settings */}
|
|
{bottomItems.map((item) => (
|
|
<Link
|
|
key={item.href}
|
|
href={item.href}
|
|
onClick={() => setMobileOpen(false)}
|
|
className={clsx(
|
|
"group relative flex items-center gap-3 px-3 py-3 rounded-xl transition-all duration-300",
|
|
isActive(item.href)
|
|
? "bg-foreground/10 text-foreground border border-foreground/10"
|
|
: "text-foreground-muted hover:text-foreground hover:bg-foreground/5 border border-transparent"
|
|
)}
|
|
title={collapsed ? item.label : undefined}
|
|
>
|
|
<item.icon className="w-5 h-5" />
|
|
{!collapsed && <span className="text-sm font-medium">{item.label}</span>}
|
|
</Link>
|
|
))}
|
|
|
|
{/* User Card */}
|
|
<div className={clsx(
|
|
"mt-4 p-4 bg-gradient-to-br from-foreground/[0.03] to-transparent border border-border/50 rounded-2xl",
|
|
collapsed && "p-3"
|
|
)}>
|
|
{collapsed ? (
|
|
<div className="flex justify-center">
|
|
<div className="w-10 h-10 bg-gradient-to-br from-accent/20 to-accent/5 rounded-xl flex items-center justify-center border border-accent/20">
|
|
<TierIcon className="w-5 h-5 text-accent" />
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="flex items-center gap-3 mb-4">
|
|
<div className="w-11 h-11 bg-gradient-to-br from-accent/20 to-accent/5 rounded-xl flex items-center justify-center border border-accent/20 shadow-[0_0_20px_-5px_rgba(16,185,129,0.3)]">
|
|
<TierIcon className="w-5 h-5 text-accent" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm font-semibold text-foreground truncate">
|
|
{user?.name || user?.email?.split('@')[0]}
|
|
</p>
|
|
<div className="flex items-center gap-1.5">
|
|
<span className={clsx(
|
|
"text-xs font-medium",
|
|
tierName === 'Tycoon' ? "text-amber-400" :
|
|
tierName === 'Trader' ? "text-accent" :
|
|
"text-foreground-muted"
|
|
)}>
|
|
{tierName}
|
|
</span>
|
|
{tierName === 'Tycoon' && <Sparkles className="w-3 h-3 text-amber-400" />}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Usage bar */}
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between text-xs">
|
|
<span className="text-foreground-subtle">Domains</span>
|
|
<span className="text-foreground-muted">
|
|
{subscription?.domains_used || 0}/{subscription?.domain_limit || 5}
|
|
</span>
|
|
</div>
|
|
<div className="h-1.5 bg-foreground/5 rounded-full overflow-hidden">
|
|
<div
|
|
className="h-full bg-gradient-to-r from-accent to-accent/60 rounded-full transition-all duration-500"
|
|
style={{
|
|
width: `${Math.min(((subscription?.domains_used || 0) / (subscription?.domain_limit || 5)) * 100, 100)}%`
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{tierName === 'Scout' && (
|
|
<Link
|
|
href="/pricing"
|
|
className="mt-4 flex items-center justify-center gap-2 w-full py-2.5 bg-gradient-to-r from-accent to-accent/80
|
|
text-background text-xs font-semibold rounded-xl
|
|
hover:shadow-[0_0_20px_-5px_rgba(16,185,129,0.5)] transition-all"
|
|
>
|
|
<CreditCard className="w-3.5 h-3.5" />
|
|
Upgrade Plan
|
|
</Link>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{/* Logout */}
|
|
<button
|
|
onClick={() => {
|
|
logout()
|
|
setMobileOpen(false)
|
|
}}
|
|
className={clsx(
|
|
"w-full flex items-center gap-3 px-3 py-3 rounded-xl transition-all duration-300",
|
|
"text-foreground-muted hover:text-foreground hover:bg-foreground/5"
|
|
)}
|
|
title={collapsed ? "Sign out" : undefined}
|
|
>
|
|
<LogOut className="w-5 h-5" />
|
|
{!collapsed && <span className="text-sm font-medium">Sign out</span>}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Collapse Toggle - Desktop only */}
|
|
<button
|
|
onClick={toggleCollapsed}
|
|
className={clsx(
|
|
"hidden lg:flex absolute -right-3 top-24 w-6 h-6 bg-background border border-border rounded-full",
|
|
"items-center justify-center text-foreground-muted hover:text-foreground",
|
|
"hover:bg-accent/10 hover:border-accent/30 transition-all duration-300 shadow-lg"
|
|
)}
|
|
>
|
|
{collapsed ? (
|
|
<ChevronRight className="w-3.5 h-3.5" />
|
|
) : (
|
|
<ChevronLeft className="w-3.5 h-3.5" />
|
|
)}
|
|
</button>
|
|
</>
|
|
)
|
|
|
|
return (
|
|
<>
|
|
{/* Mobile Menu Button */}
|
|
<button
|
|
onClick={() => setMobileOpen(true)}
|
|
className="lg:hidden fixed top-4 left-4 z-50 w-11 h-11 bg-background/80 backdrop-blur-xl border border-border
|
|
rounded-xl flex items-center justify-center text-foreground-muted hover:text-foreground
|
|
transition-all shadow-lg hover:shadow-xl hover:border-accent/30"
|
|
>
|
|
<Menu className="w-5 h-5" />
|
|
</button>
|
|
|
|
{/* Mobile Overlay */}
|
|
{mobileOpen && (
|
|
<div
|
|
className="lg:hidden fixed inset-0 z-40 bg-background/80 backdrop-blur-sm"
|
|
onClick={() => setMobileOpen(false)}
|
|
/>
|
|
)}
|
|
|
|
{/* Mobile Sidebar */}
|
|
<aside
|
|
className={clsx(
|
|
"lg:hidden fixed left-0 top-0 bottom-0 z-50 w-[280px] flex flex-col",
|
|
"bg-background/95 backdrop-blur-xl border-r border-border/50",
|
|
"transition-transform duration-300 ease-out",
|
|
mobileOpen ? "translate-x-0" : "-translate-x-full"
|
|
)}
|
|
>
|
|
{/* Close button */}
|
|
<button
|
|
onClick={() => setMobileOpen(false)}
|
|
className="absolute top-5 right-4 w-8 h-8 flex items-center justify-center
|
|
text-foreground-muted hover:text-foreground transition-colors"
|
|
>
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
<SidebarContent />
|
|
</aside>
|
|
|
|
{/* Desktop Sidebar */}
|
|
<aside
|
|
className={clsx(
|
|
"hidden lg:flex fixed left-0 top-0 bottom-0 z-40 flex-col",
|
|
"bg-gradient-to-b from-background/95 via-background/90 to-background/95 backdrop-blur-xl",
|
|
"border-r border-border/30",
|
|
"transition-all duration-300 ease-out",
|
|
collapsed ? "w-[72px]" : "w-[260px]"
|
|
)}
|
|
>
|
|
<SidebarContent />
|
|
</aside>
|
|
</>
|
|
)
|
|
}
|