File size: 6,874 Bytes
89a11b6 a79917c d7fe8fb a79917c 89a11b6 ae6419f a79917c 89a11b6 ae6419f a79917c 19a0889 a79917c ae6419f a79917c 89a11b6 d7fe8fb a79917c 0e29296 19a0889 0e29296 19a0889 0e29296 a79917c bb09573 a79917c 89a11b6 a79917c 89a11b6 a79917c 89a11b6 a79917c 7312b0b 57c6d6d 7312b0b c94d5d9 7312b0b ae6419f a79917c 89a11b6 a79917c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | // frontend/src/Layout.jsx
import React, { useState } from "react";
import { Link } from "react-router-dom";
import { createPageUrl } from "./utils";
import {
LayoutDashboard,
History as HistoryIcon,
Key,
ChevronLeft,
Sparkles,
LogOut,
User,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { useAuth } from "./contexts/AuthContext";
// Import logo - Vite will process this and handle the path correctly
// For production, the logo should be in frontend/public/logo.png
// Vite will copy it to dist/logo.png during build
const logoPath = "/logo.png";
export default function Layout({ children, currentPageName }) {
const [collapsed, setCollapsed] = useState(false);
const { user, logout } = useAuth();
const navItems = [
{ name: "Dashboard", icon: LayoutDashboard, page: "Dashboard" },
{ name: "History", icon: HistoryIcon, page: "History" },
{ name: "API Keys", icon: Key, page: "API Keys" },
];
return (
<div className="min-h-screen bg-[#FAFAFA] flex">
{/* Sidebar */}
<aside
className={cn(
"fixed left-0 top-0 h-screen bg-white border-r border-slate-200/80 z-50 transition-all duration-300 ease-out flex flex-col",
collapsed ? "w-[72px]" : "w-[260px]"
)}
>
{/* Logo */}
<div
className={cn(
"h-16 flex items-center border-b border-slate-100 px-4",
collapsed ? "justify-center" : "justify-between"
)}
>
<Link to={createPageUrl("Dashboard")} className="flex items-center gap-3">
<div className="h-9 w-9 flex items-center justify-center flex-shrink-0">
<img
src={logoPath}
alt="EZOFIS AI Logo"
className="h-full w-full object-contain"
onError={(e) => {
// Fallback: hide image and show placeholder if logo not found
e.target.style.display = 'none';
}}
/>
</div>
{!collapsed && (
<div className="flex flex-col">
<span className="font-semibold text-slate-900 tracking-tight">EZOFIS AI</span>
<span className="text-[10px] text-slate-400 font-medium tracking-wide uppercase">
VRP Intelligence
</span>
</div>
)}
</Link>
{!collapsed && (
<button
onClick={() => setCollapsed(true)}
className="h-7 w-7 rounded-lg hover:bg-slate-100 flex items-center justify-center text-slate-400 hover:text-slate-600 transition-colors"
>
<ChevronLeft className="h-4 w-4" />
</button>
)}
</div>
{/* Navigation */}
<nav className="flex-1 p-3 space-y-1">
{navItems.map((item) => {
const isActive = currentPageName === item.page;
return (
<Link
key={item.name}
to={createPageUrl(item.page)}
className={cn(
"flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all duration-200 group",
isActive
? "bg-gradient-to-r from-indigo-50 to-violet-50 text-indigo-600"
: "text-slate-500 hover:bg-slate-50 hover:text-slate-700"
)}
>
<item.icon
className={cn(
"h-5 w-5 flex-shrink-0",
isActive ? "text-indigo-600" : "text-slate-400 group-hover:text-slate-600"
)}
/>
{!collapsed && (
<span className="font-medium text-sm">{item.name}</span>
)}
</Link>
);
})}
</nav>
{/* Collapse Toggle (when collapsed) */}
{collapsed && (
<button
onClick={() => setCollapsed(false)}
className="m-3 h-10 rounded-xl bg-slate-50 hover:bg-slate-100 flex items-center justify-center text-slate-400 hover:text-slate-600 transition-colors"
>
<ChevronLeft className="h-4 w-4 rotate-180" />
</button>
)}
{/* Pro Badge */}
{!collapsed && (
<div className="p-3">
<div className="p-4 rounded-2xl bg-gradient-to-br from-slate-900 to-slate-800 text-white">
<div className="flex items-center gap-2 mb-2">
<Sparkles className="h-4 w-4 text-amber-400" />
<span className="text-xs font-semibold tracking-wide">DEPLOY CUSTOM AGENT</span>
</div>
<p className="text-xs text-slate-400 mb-3">
Batch extractions, custom model, field mapping, complex lineitems, tables, workflows, & API access
</p>
<button
className="w-full py-2 px-3 rounded-lg bg-white text-slate-900 text-sm font-semibold hover:bg-slate-100 transition-colors"
onClick={() => window.open("https://calendar.app.google/UTx9ZiBXhpMqCVyaA", "_blank", "noopener,noreferrer")}
>
Book a Custom Demo
</button>
</div>
</div>
)}
{/* User Profile */}
{!collapsed && user && (
<div className="p-3 border-t border-slate-200">
<div className="flex items-center gap-3 p-3 rounded-xl bg-slate-50 hover:bg-slate-100 transition-colors">
{user.picture ? (
<img
src={user.picture}
alt={user.name || user.email}
className="h-10 w-10 rounded-lg object-cover"
/>
) : (
<div className="h-10 w-10 rounded-lg bg-indigo-100 flex items-center justify-center">
<User className="h-5 w-5 text-indigo-600" />
</div>
)}
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-slate-900 truncate">
{user.name || "User"}
</p>
<p className="text-xs text-slate-500 truncate">{user.email}</p>
</div>
</div>
<button
onClick={logout}
className="mt-2 w-full flex items-center gap-2 px-3 py-2 rounded-xl text-sm text-slate-600 hover:bg-red-50 hover:text-red-600 transition-colors"
>
<LogOut className="h-4 w-4" />
<span>Sign Out</span>
</button>
</div>
)}
</aside>
{/* Main Content */}
<main
className={cn(
"flex-1 transition-all duration-300",
collapsed ? "ml-[72px]" : "ml-[260px]"
)}
>
{children}
</main>
</div>
);
}
|