yves.gugger ae1416bd34
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
Major navigation overhaul: Add Command Center with Sidebar
- 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
2025-12-10 08:37:29 +01:00

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>
)
}