mirror of
https://github.com/hanzoai/bootnode.git
synced 2026-10-25 02:31:30 +00:00
Deploy the web3 console as lux.cloud (Lux brand). Convert from SSR (output: standalone) to static export so it serves as static assets on Cloudflare Pages with no edge/Node runtime. - next.config.ts: output "export" + images.unoptimized; drop headers()/ redirects() (unsupported in export) — moved to public/_headers and public/_redirects for Pages. - app/layout.tsx: bake brand from NEXT_PUBLIC_BRAND/BRAND; remove force-dynamic + headers() host detection (client still auto-detects brand from window.location.hostname via lib/brand.ts). - app/api/health, app/llms.txt: mark route handlers force-static. - Remove app/dashboard/api/keys route handler (backend proxy unsupported in export; the dashboard talks to brand.apiUrl directly). - nodes/[id]: split into a server page.tsx (generateStaticParams) + client node-detail-client.tsx so the dynamic segment is export-compatible. - unified-navigation.tsx: fix API Keys link to the real /dashboard/api-keys page (was pointing at the removed route handler). - lib/brand.ts: point lux IAM clientId at the real Casdoor app "lux-cloud" (clientId lux-app-client-id does not exist in IAM). Built with NEXT_PUBLIC_BRAND=lux, IAM lux.id / client lux-cloud; deployed to CF Pages project lux-cloud, custom domain lux.cloud. Co-authored-by: Hanzo Dev <[email protected]>
466 lines
16 KiB
TypeScript
466 lines
16 KiB
TypeScript
"use client"
|
|
|
|
// Unified Dashboard Navigation - All features merged
|
|
// Complete platform navigation with organizations, teams, and all features
|
|
|
|
import { useState, useEffect } from "react"
|
|
import Link from "next/link"
|
|
import { usePathname } from "next/navigation"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger
|
|
} from "@/components/ui/dropdown-menu"
|
|
import { useAuth, OrgBadge } from "@/lib/auth"
|
|
import { getBrand } from "@/lib/brand"
|
|
import {
|
|
Activity,
|
|
Server,
|
|
Network,
|
|
Database,
|
|
Users,
|
|
Settings,
|
|
CreditCard,
|
|
Key,
|
|
Webhook,
|
|
BarChart3,
|
|
Coins,
|
|
Shield,
|
|
Building,
|
|
UserPlus,
|
|
ChevronDown,
|
|
Menu,
|
|
X,
|
|
Home,
|
|
Zap,
|
|
Globe,
|
|
Code,
|
|
FileText,
|
|
Bell,
|
|
Boxes,
|
|
Eye,
|
|
ExternalLink,
|
|
Search,
|
|
ArrowLeftRight,
|
|
ArrowUpDown,
|
|
ShoppingBag,
|
|
Wallet,
|
|
Droplets,
|
|
Vote,
|
|
Bot,
|
|
Brain,
|
|
MessageCircle,
|
|
MessageSquare,
|
|
Sparkles,
|
|
} from "lucide-react"
|
|
import { CommandMenu } from "@/components/command-menu"
|
|
|
|
interface NavigationItem {
|
|
name: string
|
|
href: string
|
|
icon: any
|
|
current?: boolean
|
|
badge?: string
|
|
description?: string
|
|
children?: NavigationItem[]
|
|
}
|
|
|
|
export function UnifiedNavigation() {
|
|
const { user, logout, isAdmin } = useAuth()
|
|
const pathname = usePathname()
|
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
|
const [selectedOrg, setSelectedOrg] = useState<string>(user?.org || "hanzo")
|
|
|
|
// Unified navigation structure
|
|
const navigation: NavigationItem[] = [
|
|
{
|
|
name: "Overview",
|
|
href: "/dashboard",
|
|
icon: Home,
|
|
description: "Platform overview and quick actions"
|
|
},
|
|
{
|
|
name: "Infrastructure",
|
|
href: "/dashboard/infrastructure",
|
|
icon: Server,
|
|
description: "Manage nodes, networks, services, and infrastructure",
|
|
children: [
|
|
{ name: "Nodes", href: "/dashboard/infrastructure/nodes", icon: Server },
|
|
{ name: "Networks", href: "/dashboard/infrastructure/networks", icon: Network },
|
|
{ name: "Services", href: "/dashboard/infrastructure/services", icon: Boxes },
|
|
{ name: "Observability", href: "/dashboard/infrastructure/observability", icon: Eye },
|
|
{ name: "Monitoring", href: "/dashboard/infrastructure/monitoring", icon: Activity },
|
|
]
|
|
},
|
|
{
|
|
name: "API & Development",
|
|
href: "/dashboard/api",
|
|
icon: Code,
|
|
description: "API keys, webhooks, and developer tools",
|
|
children: [
|
|
{ name: "API Keys", href: "/dashboard/api-keys", icon: Key },
|
|
{ name: "Webhooks", href: "/dashboard/api/webhooks", icon: Webhook },
|
|
{ name: "Documentation", href: "/dashboard/api/docs", icon: FileText },
|
|
{ name: "Playground", href: "/dashboard/api/playground", icon: Zap },
|
|
]
|
|
},
|
|
{
|
|
name: "Analytics",
|
|
href: "/dashboard/analytics",
|
|
icon: BarChart3,
|
|
description: "Usage metrics, performance, and insights",
|
|
children: [
|
|
{ name: "Usage", href: "/dashboard/analytics/usage", icon: Activity },
|
|
{ name: "Performance", href: "/dashboard/analytics/performance", icon: Zap },
|
|
{ name: "Billing", href: "/dashboard/organization/billing", icon: CreditCard },
|
|
]
|
|
},
|
|
{
|
|
name: "Assets",
|
|
href: "/dashboard/assets",
|
|
icon: Coins,
|
|
description: "Token balances, NFTs, and portfolio",
|
|
children: [
|
|
{ name: "Tokens", href: "/dashboard/assets/tokens", icon: Coins },
|
|
{ name: "NFTs", href: "/dashboard/assets/nfts", icon: Globe },
|
|
{ name: "Transfers", href: "/dashboard/assets/transfers", icon: Activity },
|
|
]
|
|
},
|
|
{
|
|
name: "Organization",
|
|
href: "/dashboard/organization",
|
|
icon: Building,
|
|
description: "Team, members, and organization settings",
|
|
children: [
|
|
{ name: "Team", href: "/dashboard/organization/team", icon: Users },
|
|
{ name: "Settings", href: "/dashboard/organization/settings", icon: Settings },
|
|
{ name: "Billing", href: "/dashboard/organization/billing", icon: CreditCard },
|
|
{ name: "Security", href: "/dashboard/organization/security", icon: Shield },
|
|
]
|
|
}
|
|
]
|
|
|
|
// Ecosystem apps from brand config
|
|
const brand = getBrand()
|
|
const ecosystemNavigation: NavigationItem[] = brand.ecosystemApps.length > 0
|
|
? [{
|
|
name: "Ecosystem",
|
|
href: "/dashboard/ecosystem",
|
|
icon: Globe,
|
|
description: "Ecosystem apps and services",
|
|
children: brand.ecosystemApps.map(app => ({
|
|
name: app.name,
|
|
href: app.href,
|
|
icon: ExternalLink,
|
|
})),
|
|
}]
|
|
: []
|
|
|
|
// Admin-only navigation
|
|
const adminNavigation: NavigationItem[] = [
|
|
{
|
|
name: "Admin",
|
|
href: "/admin",
|
|
icon: Shield,
|
|
description: "Platform administration and management",
|
|
children: [
|
|
{ name: "Overview", href: "/admin", icon: Activity },
|
|
{ name: "All Nodes", href: "/admin/nodes", icon: Server },
|
|
{ name: "All Networks", href: "/admin/networks", icon: Network },
|
|
{ name: "All Users", href: "/admin/users", icon: Users },
|
|
{ name: "System Health", href: "/admin/health", icon: Shield },
|
|
]
|
|
}
|
|
]
|
|
|
|
const isActive = (href: string) => {
|
|
if (href === "/dashboard") return pathname === href
|
|
return pathname.startsWith(href)
|
|
}
|
|
|
|
const organizations = [
|
|
{ id: "hanzo", name: "Hanzo", domain: "hanzo.id", color: "#0066ff" },
|
|
{ id: "zoo", name: "Zoo Labs", domain: "id.zoo.network", color: "#00cc66" },
|
|
{ id: "lux", name: "Lux Network", domain: "lux.id", color: "#8b5cf6" },
|
|
{ id: "pars", name: "Pars", domain: "pars.id", color: "#f59e0b" },
|
|
]
|
|
|
|
const currentOrg = organizations.find(org => org.id === selectedOrg) || organizations[0]
|
|
|
|
return (
|
|
<>
|
|
{/* Top Navigation Bar */}
|
|
<nav className="bg-background border-b border-border fixed w-full top-0 z-50">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div className="flex justify-between h-16">
|
|
<div className="flex items-center">
|
|
{/* Logo */}
|
|
<Link href="/dashboard" className="flex items-center space-x-3">
|
|
<img src={getBrand().logoIcon} alt={getBrand().name} className="w-8 h-8" />
|
|
<span className="text-xl font-bold">{getBrand().name}</span>
|
|
</Link>
|
|
|
|
{/* Organization Selector */}
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" className="ml-6 flex items-center space-x-2">
|
|
<div
|
|
className="w-4 h-4 rounded-full"
|
|
style={{ backgroundColor: currentOrg.color }}
|
|
/>
|
|
<span>{currentOrg.name}</span>
|
|
<ChevronDown className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="start" className="w-56">
|
|
<DropdownMenuLabel>Switch Organization</DropdownMenuLabel>
|
|
<DropdownMenuSeparator />
|
|
{organizations.map((org) => (
|
|
<DropdownMenuItem
|
|
key={org.id}
|
|
onClick={() => setSelectedOrg(org.id)}
|
|
className="flex items-center space-x-2"
|
|
>
|
|
<div
|
|
className="w-3 h-3 rounded-full"
|
|
style={{ backgroundColor: org.color }}
|
|
/>
|
|
<div>
|
|
<div className="font-medium">{org.name}</div>
|
|
<div className="text-xs text-muted-foreground">{org.domain}</div>
|
|
</div>
|
|
{org.id === selectedOrg && <Badge variant="secondary">Current</Badge>}
|
|
</DropdownMenuItem>
|
|
))}
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem>
|
|
<UserPlus className="mr-2 h-4 w-4" />
|
|
Create Organization
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
|
|
{/* Right side */}
|
|
<div className="flex items-center space-x-4">
|
|
{/* Command Menu + AI Chat */}
|
|
<CommandMenu />
|
|
|
|
{/* Notifications */}
|
|
<Button variant="ghost" size="sm">
|
|
<Bell className="h-5 w-5" />
|
|
</Button>
|
|
|
|
{/* User Menu */}
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" className="flex items-center space-x-2">
|
|
<Avatar className="h-8 w-8">
|
|
<AvatarImage src={user?.avatar} />
|
|
<AvatarFallback>{user?.name?.charAt(0)}</AvatarFallback>
|
|
</Avatar>
|
|
<span className="hidden md:block">{user?.name}</span>
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-56">
|
|
<DropdownMenuLabel>
|
|
<div>{user?.name}</div>
|
|
<div className="text-xs text-muted-foreground">{user?.email}</div>
|
|
</DropdownMenuLabel>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem>
|
|
<Settings className="mr-2 h-4 w-4" />
|
|
Profile Settings
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem>
|
|
<Key className="mr-2 h-4 w-4" />
|
|
API Keys
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem onClick={logout}>
|
|
Logout
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
|
|
{/* Mobile menu button */}
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="md:hidden"
|
|
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
|
>
|
|
{mobileMenuOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
{/* Side Navigation */}
|
|
<div className="fixed inset-y-0 left-0 z-40 w-64 bg-sidebar border-r border-border pt-16 overflow-y-auto">
|
|
<nav className="p-4 space-y-2">
|
|
{/* Main Navigation */}
|
|
{navigation.map((item) => (
|
|
<NavItem key={item.name} item={item} isActive={isActive} />
|
|
))}
|
|
|
|
{/* Ecosystem Apps */}
|
|
{ecosystemNavigation.length > 0 && (
|
|
<>
|
|
<div className="pt-4 mt-4 border-t border-border">
|
|
<h3 className="px-3 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
|
Ecosystem
|
|
</h3>
|
|
</div>
|
|
{ecosystemNavigation.map((item) => (
|
|
<NavItem key={item.name} item={item} isActive={isActive} />
|
|
))}
|
|
</>
|
|
)}
|
|
|
|
{/* Admin Navigation (if admin) */}
|
|
{isAdmin && (
|
|
<>
|
|
<div className="pt-4 mt-4 border-t border-border">
|
|
<h3 className="px-3 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
|
Administration
|
|
</h3>
|
|
</div>
|
|
{adminNavigation.map((item) => (
|
|
<NavItem key={item.name} item={item} isActive={isActive} />
|
|
))}
|
|
</>
|
|
)}
|
|
</nav>
|
|
|
|
{/* Organization Info */}
|
|
<div className="absolute bottom-0 left-0 right-0 p-4 bg-background border-t border-border">
|
|
<div className="flex items-center space-x-3">
|
|
<div
|
|
className="w-8 h-8 rounded-lg flex items-center justify-center text-white text-sm font-bold"
|
|
style={{ backgroundColor: currentOrg.color }}
|
|
>
|
|
{currentOrg.name.charAt(0)}
|
|
</div>
|
|
<div>
|
|
<div className="font-medium text-sm">{currentOrg.name}</div>
|
|
<div className="text-xs text-muted-foreground">{currentOrg.domain}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Mobile Navigation Overlay */}
|
|
{mobileMenuOpen && (
|
|
<div className="fixed inset-0 z-30 md:hidden">
|
|
<div className="absolute inset-0 bg-black opacity-25" onClick={() => setMobileMenuOpen(false)} />
|
|
<div className="absolute left-0 top-16 w-64 h-full bg-background border-r border-border overflow-y-auto">
|
|
<nav className="p-4 space-y-2">
|
|
{navigation.concat(ecosystemNavigation).concat(isAdmin ? adminNavigation : []).map((item) => (
|
|
<NavItem
|
|
key={item.name}
|
|
item={item}
|
|
isActive={isActive}
|
|
onClick={() => setMobileMenuOpen(false)}
|
|
/>
|
|
))}
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Page Content Wrapper */}
|
|
<div className="pl-64 pt-16">
|
|
{/* This will wrap the page content */}
|
|
</div>
|
|
</>
|
|
)
|
|
}
|
|
|
|
// Navigation Item Component
|
|
function NavItem({
|
|
item,
|
|
isActive,
|
|
onClick
|
|
}: {
|
|
item: NavigationItem
|
|
isActive: (href: string) => boolean
|
|
onClick?: () => void
|
|
}) {
|
|
const [isExpanded, setIsExpanded] = useState(isActive(item.href))
|
|
const hasChildren = item.children && item.children.length > 0
|
|
|
|
return (
|
|
<div>
|
|
<Link
|
|
href={item.href}
|
|
onClick={onClick}
|
|
className={`
|
|
flex items-center justify-between w-full px-3 py-2 text-sm font-medium rounded-md transition-colors
|
|
${isActive(item.href)
|
|
? 'bg-primary text-primary-foreground'
|
|
: 'text-foreground/80 hover:bg-accent'
|
|
}
|
|
`}
|
|
>
|
|
<div className="flex items-center space-x-3">
|
|
<item.icon className="h-5 w-5" />
|
|
<span>{item.name}</span>
|
|
{item.badge && (
|
|
<Badge variant="secondary" className="ml-auto">
|
|
{item.badge}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
{hasChildren && (
|
|
<ChevronDown
|
|
className={`h-4 w-4 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
|
|
onClick={(e) => {
|
|
e.preventDefault()
|
|
setIsExpanded(!isExpanded)
|
|
}}
|
|
/>
|
|
)}
|
|
</Link>
|
|
|
|
{/* Sub-navigation */}
|
|
{hasChildren && isExpanded && (
|
|
<div className="ml-8 mt-1 space-y-1">
|
|
{item.children!.map((child) => {
|
|
const isExternal = child.href.startsWith("http")
|
|
const linkProps = isExternal
|
|
? { target: "_blank" as const, rel: "noopener noreferrer" }
|
|
: {}
|
|
return (
|
|
<Link
|
|
key={child.name}
|
|
href={child.href}
|
|
onClick={onClick}
|
|
{...linkProps}
|
|
className={`
|
|
flex items-center space-x-2 px-3 py-2 text-sm rounded-md transition-colors
|
|
${!isExternal && isActive(child.href)
|
|
? 'bg-primary/10 text-primary'
|
|
: 'text-muted-foreground hover:bg-accent'
|
|
}
|
|
`}
|
|
>
|
|
<child.icon className="h-4 w-4" />
|
|
<span>{child.name}</span>
|
|
{isExternal && <ExternalLink className="h-3 w-3 ml-auto opacity-50" />}
|
|
</Link>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|