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
- New Sidebar component with collapsible navigation - New CommandCenterLayout for logged-in users - Separate routes: /watchlist, /portfolio, /market, /intelligence - Dashboard with Activity Feed and Market Pulse - Traffic light status indicators for domain status - Updated Header for public/logged-in state separation - Settings page uses new Command Center layout
281 lines
9.0 KiB
TypeScript
281 lines
9.0 KiB
TypeScript
'use client'
|
|
|
|
import Link from 'next/link'
|
|
import { usePathname } from 'next/navigation'
|
|
import { useStore } from '@/lib/store'
|
|
import {
|
|
LayoutDashboard,
|
|
Eye,
|
|
Briefcase,
|
|
Gavel,
|
|
TrendingUp,
|
|
Settings,
|
|
ChevronLeft,
|
|
ChevronRight,
|
|
LogOut,
|
|
Crown,
|
|
Zap,
|
|
Shield,
|
|
CreditCard,
|
|
} 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)
|
|
|
|
// 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')
|
|
}
|
|
}, [])
|
|
|
|
// 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
|
|
|
|
// Navigation items
|
|
const navItems = [
|
|
{
|
|
href: '/dashboard',
|
|
label: 'Dashboard',
|
|
icon: LayoutDashboard,
|
|
badge: null,
|
|
},
|
|
{
|
|
href: '/watchlist',
|
|
label: 'Watchlist',
|
|
icon: Eye,
|
|
badge: domains?.filter(d => d.is_available).length || null,
|
|
},
|
|
{
|
|
href: '/portfolio',
|
|
label: 'Portfolio',
|
|
icon: Briefcase,
|
|
badge: null,
|
|
},
|
|
{
|
|
href: '/market',
|
|
label: 'Market',
|
|
icon: Gavel,
|
|
badge: null,
|
|
},
|
|
{
|
|
href: '/intelligence',
|
|
label: 'Intelligence',
|
|
icon: TrendingUp,
|
|
badge: null,
|
|
},
|
|
]
|
|
|
|
const bottomItems = [
|
|
{ href: '/settings', label: 'Settings', icon: Settings },
|
|
]
|
|
|
|
const isActive = (href: string) => {
|
|
if (href === '/dashboard') return pathname === '/dashboard'
|
|
return pathname.startsWith(href)
|
|
}
|
|
|
|
return (
|
|
<aside
|
|
className={clsx(
|
|
"fixed left-0 top-0 bottom-0 z-40 flex flex-col",
|
|
"bg-background-secondary/50 backdrop-blur-xl border-r border-border",
|
|
"transition-all duration-300 ease-in-out",
|
|
collapsed ? "w-[72px]" : "w-[240px]"
|
|
)}
|
|
>
|
|
{/* Logo */}
|
|
<div className={clsx(
|
|
"h-16 sm:h-20 flex items-center border-b border-border/50",
|
|
collapsed ? "justify-center px-2" : "px-5"
|
|
)}>
|
|
<Link href="/" className="flex items-center gap-3 group">
|
|
<div className="w-9 h-9 bg-accent/10 rounded-xl flex items-center justify-center border border-accent/20
|
|
group-hover:bg-accent/20 transition-colors">
|
|
<span className="font-display text-accent text-lg font-bold">P</span>
|
|
</div>
|
|
{!collapsed && (
|
|
<span
|
|
className="text-lg font-bold tracking-[0.1em] text-foreground"
|
|
style={{ fontFamily: 'var(--font-display), Playfair Display, Georgia, serif' }}
|
|
>
|
|
POUNCE
|
|
</span>
|
|
)}
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Main Navigation */}
|
|
<nav className="flex-1 py-4 px-3 space-y-1 overflow-y-auto">
|
|
{navItems.map((item) => (
|
|
<Link
|
|
key={item.href}
|
|
href={item.href}
|
|
className={clsx(
|
|
"group flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all duration-200",
|
|
isActive(item.href)
|
|
? "bg-accent/10 text-foreground"
|
|
: "text-foreground-muted hover:text-foreground hover:bg-foreground/5"
|
|
)}
|
|
title={collapsed ? item.label : undefined}
|
|
>
|
|
<div className="relative">
|
|
<item.icon className={clsx(
|
|
"w-5 h-5 transition-colors",
|
|
isActive(item.href) ? "text-accent" : "group-hover:text-foreground"
|
|
)} />
|
|
{/* Badge for notifications */}
|
|
{item.badge && (
|
|
<span className="absolute -top-1.5 -right-1.5 w-4 h-4 bg-accent text-background
|
|
text-[10px] font-bold rounded-full flex items-center justify-center">
|
|
{item.badge > 9 ? '9+' : item.badge}
|
|
</span>
|
|
)}
|
|
</div>
|
|
{!collapsed && (
|
|
<span className={clsx(
|
|
"text-sm font-medium transition-colors",
|
|
isActive(item.href) && "text-foreground"
|
|
)}>
|
|
{item.label}
|
|
</span>
|
|
)}
|
|
</Link>
|
|
))}
|
|
</nav>
|
|
|
|
{/* Bottom Section */}
|
|
<div className="border-t border-border/50 py-4 px-3 space-y-1">
|
|
{/* Admin Link */}
|
|
{user?.is_admin && (
|
|
<Link
|
|
href="/admin"
|
|
className={clsx(
|
|
"group flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all duration-200",
|
|
pathname.startsWith('/admin')
|
|
? "bg-accent/10 text-accent"
|
|
: "text-accent/70 hover:text-accent hover:bg-accent/5"
|
|
)}
|
|
title={collapsed ? "Admin Panel" : undefined}
|
|
>
|
|
<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}
|
|
className={clsx(
|
|
"group flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all duration-200",
|
|
isActive(item.href)
|
|
? "bg-foreground/10 text-foreground"
|
|
: "text-foreground-muted hover:text-foreground hover:bg-foreground/5"
|
|
)}
|
|
title={collapsed ? item.label : undefined}
|
|
>
|
|
<item.icon className="w-5 h-5" />
|
|
{!collapsed && <span className="text-sm font-medium">{item.label}</span>}
|
|
</Link>
|
|
))}
|
|
|
|
{/* User Info */}
|
|
<div className={clsx(
|
|
"mt-4 p-3 bg-foreground/5 rounded-xl",
|
|
collapsed && "p-2"
|
|
)}>
|
|
{collapsed ? (
|
|
<div className="flex justify-center">
|
|
<div className="w-8 h-8 bg-accent/10 rounded-lg flex items-center justify-center">
|
|
<TierIcon className="w-4 h-4 text-accent" />
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="flex items-center gap-3 mb-3">
|
|
<div className="w-9 h-9 bg-accent/10 rounded-lg flex items-center justify-center">
|
|
<TierIcon className="w-4 h-4 text-accent" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm font-medium text-foreground truncate">
|
|
{user?.name || user?.email?.split('@')[0]}
|
|
</p>
|
|
<p className="text-xs text-foreground-muted">{tierName}</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center justify-between text-xs text-foreground-subtle">
|
|
<span>{subscription?.domains_used || 0}/{subscription?.domain_limit || 5} domains</span>
|
|
{tierName === 'Scout' && (
|
|
<Link
|
|
href="/pricing"
|
|
className="text-accent hover:underline flex items-center gap-1"
|
|
>
|
|
<CreditCard className="w-3 h-3" />
|
|
Upgrade
|
|
</Link>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{/* Logout */}
|
|
<button
|
|
onClick={logout}
|
|
className={clsx(
|
|
"w-full flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all duration-200",
|
|
"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 */}
|
|
<button
|
|
onClick={toggleCollapsed}
|
|
className={clsx(
|
|
"absolute -right-3 top-24 w-6 h-6 bg-background-secondary border border-border rounded-full",
|
|
"flex items-center justify-center text-foreground-muted hover:text-foreground",
|
|
"hover:bg-foreground/5 transition-all duration-200 shadow-sm"
|
|
)}
|
|
>
|
|
{collapsed ? (
|
|
<ChevronRight className="w-3.5 h-3.5" />
|
|
) : (
|
|
<ChevronLeft className="w-3.5 h-3.5" />
|
|
)}
|
|
</button>
|
|
</aside>
|
|
)
|
|
}
|
|
|