first
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
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/applications">
|
||||
<Package className="mr-2 h-4 w-4" />
|
||||
Applications
|
||||
</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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user