141 lines
5.3 KiB
TypeScript
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>
|
|
);
|
|
}
|