Yves Gugger fccd88da46
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
feat: merge hunt/market pages, integrate cfo into portfolio
2025-12-15 21:16:09 +01:00

518 lines
18 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 {
Eye,
TrendingUp,
Settings,
ChevronLeft,
ChevronRight,
LogOut,
Crown,
Zap,
Shield,
Crosshair,
Menu,
X,
Tag,
Target,
Coins,
Briefcase,
MessageSquare,
} 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()
const [internalCollapsed, setInternalCollapsed] = useState(false)
const [mobileOpen, setMobileOpen] = useState(false)
const collapsed = controlledCollapsed ?? internalCollapsed
const setCollapsed = onCollapsedChange ?? setInternalCollapsed
useEffect(() => {
const saved = localStorage.getItem('sidebar-collapsed')
if (saved) {
setCollapsed(saved === 'true')
}
}, [])
useEffect(() => {
setMobileOpen(false)
}, [pathname])
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 availableCount = domains?.filter(d => d.is_available).length || 0
const isTycoon = tierName.toLowerCase() === 'tycoon'
// SECTION 1: Discover - Hunt is the main discovery hub
const discoverItems = [
{
href: '/terminal/hunt',
label: 'HUNT',
icon: Crosshair,
badge: null,
},
{
href: '/terminal/intel',
label: 'INTEL',
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: '/terminal/watchlist',
label: 'WATCHLIST',
icon: Eye,
badge: availableCount || null,
},
{
href: '/terminal/portfolio',
label: 'PORTFOLIO',
icon: Briefcase,
badge: null,
},
{
href: '/terminal/inbox',
label: 'INBOX',
icon: MessageSquare,
badge: null,
},
{
href: '/terminal/sniper',
label: 'SNIPER',
icon: Target,
badge: null,
},
]
// SECTION 3: Monetize - Passive income + For Sale
const monetizeItems: Array<{
href: string
label: string
icon: any
badge: number | null
isNew?: boolean
}> = [
{
href: '/terminal/yield',
label: 'YIELD',
icon: Coins,
badge: null,
isNew: true,
},
{
href: '/terminal/listing',
label: 'FOR SALE',
icon: Tag,
badge: null,
},
]
const bottomItems = [
{ href: '/terminal/settings', label: 'Settings', icon: Settings },
]
const isActive = (href: string) => {
if (href === '/terminal/hunt') return pathname === '/terminal/hunt' || pathname === '/terminal' || pathname === '/terminal/dashboard'
return pathname.startsWith(href)
}
const SidebarContent = () => (
<>
{/* Logo Section */}
<div className={clsx(
"relative h-20 flex items-center border-b border-white/[0.08]",
collapsed ? "justify-center px-2" : "px-6"
)}>
<Link href="/" className="flex items-center gap-4 group w-full">
<div className={clsx(
"relative flex items-center justify-center transition-all duration-300",
collapsed ? "w-8 h-8" : "w-10 h-10"
)}>
<div className="absolute inset-0 bg-accent/20 blur-lg rounded-full opacity-0 group-hover:opacity-100 transition-opacity" />
<Image
src="/pounce-puma.png"
alt="pounce"
width={40}
height={40}
className="relative object-contain"
/>
</div>
{!collapsed && (
<div className="flex flex-col">
<span
className="text-lg font-bold tracking-[0.05em] text-white"
style={{ fontFamily: 'var(--font-display), Georgia, serif' }}
>
POUNCE
</span>
<span className="text-[10px] text-white/30 tracking-[0.2em] uppercase font-mono">
Terminal
</span>
</div>
)}
</Link>
</div>
{/* Main Navigation */}
<nav className="flex-1 py-8 px-4 overflow-y-auto scrollbar-hide space-y-8">
{/* SECTION 1: Discover */}
<div>
{!collapsed && (
<div className="flex items-center gap-2 mb-4 px-2">
<div className="w-1 h-1 bg-accent rounded-full" />
<p className="text-[10px] font-mono font-bold text-white/40 uppercase tracking-widest">
Discover
</p>
</div>
)}
<div className="space-y-1">
{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-2 transition-all duration-200",
isActive(item.href)
? "text-white bg-white/[0.03] border-l-2 border-accent"
: "text-white/50 hover:text-white hover:bg-white/[0.02] border-l-2 border-transparent"
)}
title={collapsed ? item.label : undefined}
>
<item.icon className={clsx(
"w-4 h-4 transition-all duration-300",
isActive(item.href) ? "text-accent" : "text-white/40 group-hover:text-white"
)} />
{!collapsed && (
<span className={clsx(
"text-xs font-mono tracking-wide uppercase transition-colors",
isActive(item.href) ? "text-white" : "text-white/50 group-hover:text-white"
)}>
{item.label}
</span>
)}
</Link>
))}
</div>
</div>
{/* SECTION 2: Manage */}
<div>
{!collapsed && (
<div className="flex items-center gap-2 mb-4 px-2">
<div className="w-1 h-1 bg-accent rounded-full" />
<p className="text-[10px] font-mono font-bold text-white/40 uppercase tracking-widest">
Manage
</p>
</div>
)}
<div className="space-y-1">
{manageItems.map((item) => {
const isDisabled = item.tycoonOnly && !isTycoon
const ItemWrapper = (isDisabled ? 'div' : Link) as any
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-2 transition-all duration-200",
isDisabled
? "opacity-40 cursor-not-allowed border-l-2 border-transparent"
: isActive(item.href)
? "text-white bg-white/[0.03] border-l-2 border-accent"
: "text-white/50 hover:text-white hover:bg-white/[0.02] border-l-2 border-transparent"
)}
title={isDisabled ? "Upgrade to Tycoon to unlock" : collapsed ? item.label : undefined}
>
<div className="relative">
<item.icon className={clsx(
"w-4 h-4 transition-all duration-300",
isDisabled ? "text-white/20" : isActive(item.href) ? "text-accent" : "text-white/40 group-hover:text-white"
)} />
{item.badge && typeof item.badge === 'number' && !isDisabled && (
<span className="absolute -top-1 -right-1 w-1.5 h-1.5 bg-accent rounded-full animate-pulse" />
)}
</div>
{!collapsed && (
<span className={clsx(
"text-xs font-mono tracking-wide uppercase transition-colors flex-1",
isDisabled ? "text-white/30" : isActive(item.href) ? "text-white" : "text-white/50 group-hover:text-white"
)}>
{item.label}
</span>
)}
{isDisabled && !collapsed && <Crown className="w-3 h-3 text-amber-500/40" />}
</ItemWrapper>
)
})}
</div>
</div>
{/* SECTION 3: Monetize */}
<div>
{!collapsed && (
<div className="flex items-center gap-2 mb-4 px-2">
<div className="w-1 h-1 bg-accent rounded-full" />
<p className="text-[10px] font-mono font-bold text-white/40 uppercase tracking-widest">
Monetize
</p>
</div>
)}
<div className="space-y-1">
{monetizeItems.map((item) => (
<Link
key={item.href}
href={item.href}
onClick={() => setMobileOpen(false)}
className={clsx(
"group relative flex items-center gap-3 px-3 py-2 transition-all duration-200",
isActive(item.href)
? "text-white bg-white/[0.03] border-l-2 border-accent"
: "text-white/50 hover:text-white hover:bg-white/[0.02] border-l-2 border-transparent"
)}
title={collapsed ? item.label : undefined}
>
<div className="relative">
<item.icon className={clsx(
"w-4 h-4 transition-all duration-300",
isActive(item.href)
? "text-accent"
: "text-white/40 group-hover:text-white"
)} />
</div>
{!collapsed && (
<span className={clsx(
"text-xs font-mono tracking-wide uppercase transition-colors flex-1",
isActive(item.href) ? "text-white" : "text-white/50 group-hover:text-white"
)}>
{item.label}
</span>
)}
{item.isNew && !collapsed && (
<span className="px-1.5 py-0.5 text-[8px] font-mono font-bold uppercase tracking-wider bg-accent/20 text-accent border border-accent/20">
New
</span>
)}
</Link>
))}
</div>
</div>
</nav>
{/* Bottom Section */}
<div className="border-t border-white/[0.08] p-4 space-y-1 bg-[#020202]">
{/* Admin Link */}
{user?.is_admin && (
<Link
href="/admin"
onClick={() => setMobileOpen(false)}
className="group flex items-center gap-3 px-3 py-2 text-amber-500/60 hover:text-amber-400 transition-all"
title={collapsed ? "Admin Panel" : undefined}
>
<Shield className="w-4 h-4" />
{!collapsed && <span className="text-xs font-mono tracking-wide uppercase">Admin</span>}
</Link>
)}
{/* Settings */}
{bottomItems.map((item) => (
<Link
key={item.href}
href={item.href}
onClick={() => setMobileOpen(false)}
className={clsx(
"group flex items-center gap-3 px-3 py-2 transition-all",
isActive(item.href)
? "text-white"
: "text-white/40 hover:text-white"
)}
title={collapsed ? item.label : undefined}
>
<item.icon className="w-4 h-4" />
{!collapsed && <span className="text-xs font-mono tracking-wide uppercase">{item.label}</span>}
</Link>
))}
{/* User Card */}
<div className={clsx(
"mt-6 border border-white/[0.08] bg-white/[0.02]",
collapsed ? "p-2 border-none bg-transparent" : "p-4"
)}>
{collapsed ? (
<div className="flex justify-center">
<div className="w-8 h-8 border border-accent/20 bg-accent/5 flex items-center justify-center">
<TierIcon className="w-4 h-4 text-accent" />
</div>
</div>
) : (
<>
<div className="flex items-center gap-3 mb-4">
<div className="w-8 h-8 border border-accent/20 bg-accent/5 flex items-center justify-center shrink-0">
<TierIcon className="w-4 h-4 text-accent" />
</div>
<div className="flex-1 min-w-0 overflow-hidden">
<p className="text-xs font-bold text-white truncate font-mono uppercase tracking-wider">
{user?.name || user?.email?.split('@')[0]}
</p>
<div className="flex items-center gap-1.5 mt-0.5">
<span className={clsx(
"text-[10px] uppercase tracking-widest font-mono",
tierName === 'Tycoon' ? "text-amber-400" :
tierName === 'Trader' ? "text-accent" :
"text-white/40"
)}>
{tierName}
</span>
</div>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between text-[10px] font-mono uppercase tracking-wider text-white/30">
<span>Usage</span>
<span>{subscription?.domains_used || 0}/{subscription?.domain_limit || 5}</span>
</div>
<div className="h-px bg-white/10 w-full">
<div
className="h-full bg-accent transition-all duration-500 relative"
style={{
width: `${Math.min(((subscription?.domains_used || 0) / (subscription?.domain_limit || 5)) * 100, 100)}%`
}}
>
<div className="absolute right-0 top-1/2 -translate-y-1/2 w-1 h-1 bg-accent shadow-[0_0_5px_rgba(16,185,129,0.8)]" />
</div>
</div>
</div>
{tierName === 'Scout' && (
<Link
href="/pricing"
className="mt-4 flex items-center justify-center gap-2 w-full py-2
border border-accent/20 bg-accent/5 text-accent text-[10px] font-bold uppercase tracking-[0.2em]
hover:bg-accent hover:text-black transition-all"
>
Upgrade
</Link>
)}
</>
)}
</div>
{/* Logout */}
<button
onClick={() => {
logout()
setMobileOpen(false)
}}
className="w-full flex items-center gap-3 px-3 py-2 text-white/30 hover:text-white transition-all group"
title={collapsed ? "Sign out" : undefined}
>
<LogOut className="w-4 h-4 group-hover:text-red-400 transition-colors" />
{!collapsed && <span className="text-xs font-mono tracking-wide uppercase group-hover:text-red-400 transition-colors">Sign out</span>}
</button>
</div>
{/* Collapse Toggle */}
<button
onClick={toggleCollapsed}
className={clsx(
"hidden lg:flex absolute -right-3 top-24 w-6 h-6 bg-[#020202] border border-white/10",
"items-center justify-center text-white/40 hover:text-white",
"hover:border-white/30 transition-all z-50"
)}
>
{collapsed ? <ChevronRight className="w-3 h-3" /> : <ChevronLeft className="w-3 h-3" />}
</button>
</>
)
return (
<>
{/* Mobile Menu Button */}
<button
onClick={() => setMobileOpen(true)}
className="lg:hidden fixed top-4 left-4 z-50 w-10 h-10 bg-[#020202] border border-white/10
flex items-center justify-center text-white/60 hover:text-white
transition-all shadow-lg"
>
<Menu className="w-5 h-5" />
</button>
{/* Mobile Overlay */}
{mobileOpen && (
<div
className="lg:hidden fixed inset-0 z-40 bg-black/90 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-[#020202] border-r border-white/[0.08]",
"transition-transform duration-300 ease-out",
mobileOpen ? "translate-x-0" : "-translate-x-full"
)}
>
<button
onClick={() => setMobileOpen(false)}
className="absolute top-5 right-4 w-8 h-8 flex items-center justify-center
text-white/40 hover:text-white 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-[#020202] backdrop-blur-xl",
"border-r border-white/[0.08]",
"transition-all duration-300 ease-out",
collapsed ? "w-[72px]" : "w-[240px]"
)}
>
<SidebarContent />
</aside>
</>
)
}