Files
platform/src/apps/officer-web/Screens/Dashboard/Layout.tsx
T
2026-02-16 23:39:06 +00:00

141 lines
5.3 KiB
TypeScript

import type { ReactNode } from 'react';
import { Link } from 'react-router';
import {
User,
LogOut,
Terminal,
TerminalSquare,
FileText,
FolderOpen,
Server,
Package,
Bot,
Sparkles,
ClipboardList,
ScrollText,
Workflow,
} from 'lucide-react';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import type { UserWithToken } from 'hooks/useAuth';
import { useAuth } from 'hooks/useAuth';
import { PixelGrid } from '../LandingPage/components/PixelGrid';
import { useIsProduction } from 'hooks/useIsProduction';
import { useServerSettings } from '@/state/useServerSettings';
import { PasskeyGate } from './PasskeyGate';
import { Dock, type DockItem } from './Dock';
type DashboardLayoutProps = {
children?: ReactNode;
mobileFull?: boolean;
};
export function DashboardLayout({ children, mobileFull }: DashboardLayoutProps) {
const { user } = useAuth();
const isProduction = useIsProduction();
const { plugins } = useServerSettings();
const passkeyCount = (user as UserWithToken & { passkeyCount?: number })?.passkeyCount ?? 0;
const dockItems: DockItem[] = [
{ label: 'Chat', to: '/chat', icon: Terminal, color: '#60a5fa' },
...(plugins?.FileBrowser !== false
? [{ label: 'Files', to: '/files', icon: FolderOpen, color: '#fbbf24' } as DockItem]
: []),
...(plugins?.Terminal !== false
? [{ label: 'Terminal', to: '/terminal', icon: TerminalSquare, color: '#34d399' } as DockItem]
: []),
{ label: 'Plans', to: '/plans', icon: FileText, color: '#f472b6' },
{ label: 'Skills', to: '/skills', icon: Sparkles, color: '#c084fc' },
{ label: 'Tasks', to: '/tasks', icon: ClipboardList, color: '#fb923c' },
{ label: 'Processes', to: '/processes', icon: Workflow, color: '#2dd4bf' },
{ label: 'Logs', to: '/task-logs', icon: ScrollText, color: '#94a3b8' },
];
return (
<div className="relative overflow-hidden h-dvh outline-none fixed inset-0">
<PixelGrid />
<section className="relative h-dvh snap-start overflow-hidden">
{/* Background layer */}
<div
className="absolute inset-0 z-0"
style={{
backgroundImage: 'url(/static/landscape1.jpg)',
backgroundSize: 'cover',
backgroundPosition: 'center center',
}}
/>
{/* Content layer - above pixel grid */}
<div className="absolute inset-0 z-[520] flex flex-col">
<header className="shrink-0 border-b border-white/20 bg-white/10 backdrop-blur-xl shadow-lg px-3 py-2 md:px-6 md:py-3 flex items-center justify-between">
<Link to="/">
<img
src="/static/officer-logo.svg"
alt="Officer"
className="h-8 md:h-12 w-auto"
style={{ transform: 'skew(-15deg, -2deg)' }}
/>
</Link>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="rounded-full outline-none focus-visible:ring-2 focus-visible:ring-duck-yellow cursor-pointer">
<Avatar className="h-9 w-9 rounded-full">
<AvatarImage src={user?.avatar ?? undefined} />
<AvatarFallback className="bg-duck-teal text-duck-yellow text-sm font-bold rounded-full">
{user?.name?.charAt(0).toUpperCase() ?? '?'}
</AvatarFallback>
</Avatar>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="z-[600] w-48">
<DropdownMenuItem asChild className="cursor-pointer">
<Link to="/settings/profile">
<User className="mr-2 h-4 w-4" />
Profile
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild className="cursor-pointer">
<Link to="/settings/ai">
<Bot className="mr-2 h-4 w-4" />
AI Settings
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild className="cursor-pointer">
<Link to="/settings/server">
<Server className="mr-2 h-4 w-4" />
Server Settings
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild className="cursor-pointer">
<Link to="/settings/resources">
<Package className="mr-2 h-4 w-4" />
Resources
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild className="cursor-pointer">
<Link to="/auth/signout">
<LogOut className="mr-2 h-4 w-4" />
Sign Out
</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</header>
<div className={`flex-1 min-h-0 ${mobileFull ? 'pb-0 md:pb-20' : 'pb-16 md:pb-20'}`}>
{isProduction && passkeyCount === 0 ? <PasskeyGate /> : children}
</div>
<Dock items={dockItems} className={mobileFull ? 'hidden md:flex' : 'flex'} />
</div>
</section>
</div>
);
}