Files
Neon-Desk/components/shell/app-shell.tsx

155 lines
6.8 KiB
TypeScript

'use client';
import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
import { useState } from 'react';
import { Activity, BookOpenText, ChartCandlestick, Eye, LineChart, LogOut } from 'lucide-react';
import { authClient } from '@/lib/auth-client';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
type AppShellProps = {
title: string;
subtitle?: string;
actions?: React.ReactNode;
children: React.ReactNode;
};
const NAV_ITEMS = [
{ href: '/', label: 'Command Center', icon: Activity },
{ href: '/analysis', label: 'Company Analysis', icon: LineChart },
{ href: '/filings', label: 'Filings Stream', icon: BookOpenText },
{ href: '/portfolio', label: 'Portfolio Matrix', icon: ChartCandlestick },
{ href: '/watchlist', label: 'Watchlist', icon: Eye }
];
export function AppShell({ title, subtitle, actions, children }: AppShellProps) {
const pathname = usePathname();
const router = useRouter();
const [isSigningOut, setIsSigningOut] = useState(false);
const { data: session } = authClient.useSession();
const sessionUser = (session?.user ?? null) as { name?: string | null; email?: string | null; role?: unknown } | null;
const role = typeof sessionUser?.role === 'string'
? sessionUser.role
: Array.isArray(sessionUser?.role)
? sessionUser.role.filter((entry): entry is string => typeof entry === 'string').join(', ')
: null;
const displayName = sessionUser?.name || sessionUser?.email || 'Authenticated user';
const signOut = async () => {
if (isSigningOut) {
return;
}
setIsSigningOut(true);
try {
await authClient.signOut();
router.replace('/auth/signin');
} finally {
setIsSigningOut(false);
}
};
return (
<div className="app-surface">
<div className="ambient-grid" aria-hidden="true" />
<div className="noise-layer" aria-hidden="true" />
<div className="relative z-10 mx-auto flex min-h-screen w-full max-w-[1300px] gap-6 px-4 pb-12 pt-6 md:px-8">
<aside className="hidden w-72 shrink-0 flex-col gap-6 rounded-2xl border border-[color:var(--line-weak)] bg-[color:var(--panel)] p-5 shadow-[0_0_0_1px_rgba(0,255,180,0.06),0_20px_60px_rgba(1,4,10,0.55)] lg:flex">
<div>
<p className="terminal-caption text-xs uppercase tracking-[0.25em] text-[color:var(--terminal-muted)]">Fiscal Clone</p>
<h1 className="mt-2 text-2xl font-semibold text-[color:var(--terminal-bright)]">Neon Desk</h1>
<p className="mt-2 text-sm text-[color:var(--terminal-muted)]">
Financial intelligence cockpit with durable AI workflows.
</p>
</div>
<nav className="space-y-2">
{NAV_ITEMS.map((item) => {
const Icon = item.icon;
const isActive = pathname === item.href;
return (
<Link
key={item.href}
href={item.href}
className={cn(
'flex items-center gap-3 rounded-xl border px-3 py-2 text-sm transition-all duration-200',
isActive
? 'border-[color:var(--line-strong)] bg-[color:var(--panel-bright)] text-[color:var(--terminal-bright)] shadow-[0_0_18px_rgba(0,255,180,0.16)]'
: 'border-transparent text-[color:var(--terminal-muted)] hover:border-[color:var(--line-weak)] hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--terminal-bright)]'
)}
>
<Icon className="size-4" />
{item.label}
</Link>
);
})}
</nav>
<div className="mt-auto rounded-xl border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] p-3">
<p className="text-xs uppercase tracking-[0.2em] text-[color:var(--terminal-muted)]">Runtime</p>
<p className="mt-1 truncate text-sm text-[color:var(--terminal-bright)]">{displayName}</p>
{role ? <p className="mt-1 text-xs uppercase tracking-[0.16em] text-[color:var(--terminal-muted)]">Role: {role}</p> : null}
<p className="mt-2 text-xs text-[color:var(--terminal-muted)]">
AI and market data are driven by environment configuration and live API tasks.
</p>
<Button className="mt-3 w-full" variant="ghost" onClick={() => void signOut()} disabled={isSigningOut}>
<LogOut className="size-4" />
{isSigningOut ? 'Signing out...' : 'Sign out'}
</Button>
</div>
</aside>
<div className="flex-1">
<header className="mb-6 rounded-2xl border border-[color:var(--line-weak)] bg-[color:var(--panel)] px-6 py-5 shadow-[0_0_0_1px_rgba(0,255,180,0.05),0_14px_40px_rgba(1,4,10,0.5)]">
<div className="flex flex-wrap items-start justify-between gap-4">
<div>
<p className="terminal-caption text-xs uppercase tracking-[0.3em] text-[color:var(--terminal-muted)]">Live System</p>
<h2 className="mt-2 text-2xl font-semibold text-[color:var(--terminal-bright)] md:text-3xl">{title}</h2>
{subtitle ? (
<p className="mt-1 text-sm text-[color:var(--terminal-muted)]">{subtitle}</p>
) : null}
</div>
<div className="flex flex-wrap items-center gap-2">
{actions}
<Button variant="ghost" onClick={() => void signOut()} disabled={isSigningOut}>
<LogOut className="size-4" />
{isSigningOut ? 'Signing out...' : 'Sign out'}
</Button>
</div>
</div>
</header>
<nav className="mb-6 flex gap-2 overflow-x-auto rounded-xl border border-[color:var(--line-weak)] bg-[color:var(--panel)] p-2 lg:hidden">
{NAV_ITEMS.map((item) => {
const Icon = item.icon;
const isActive = pathname === item.href;
return (
<Link
key={item.href}
href={item.href}
className={cn(
'inline-flex min-w-fit items-center gap-2 rounded-lg border px-3 py-2 text-xs transition',
isActive
? 'border-[color:var(--line-strong)] bg-[color:var(--panel-bright)] text-[color:var(--terminal-bright)]'
: 'border-transparent text-[color:var(--terminal-muted)] hover:border-[color:var(--line-weak)] hover:bg-[color:var(--panel-soft)]'
)}
>
<Icon className="size-3.5" />
{item.label}
</Link>
);
})}
</nav>
<main className="space-y-6">{children}</main>
</div>
</div>
</div>
);
}