156 lines
6.8 KiB
TypeScript
156 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, Landmark, 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: '/financials', label: 'Financials', icon: Landmark },
|
|
{ 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>
|
|
);
|
|
}
|