design(dashboard): rebuild sidebar to match Base44 navigation hierarchy and aesthetic
This commit is contained in:
310
vibn-frontend/components/project/dashboard-sidebar.tsx
Normal file
310
vibn-frontend/components/project/dashboard-sidebar.tsx
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
import {
|
||||||
|
Search,
|
||||||
|
LayoutGrid,
|
||||||
|
Users,
|
||||||
|
Database,
|
||||||
|
BarChart2,
|
||||||
|
TrendingUp,
|
||||||
|
Globe,
|
||||||
|
Plug,
|
||||||
|
ShieldCheck,
|
||||||
|
Code2,
|
||||||
|
Bot,
|
||||||
|
Zap,
|
||||||
|
Terminal,
|
||||||
|
FileJson,
|
||||||
|
Settings,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronRight,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
export function DashboardSidebar({
|
||||||
|
workspace,
|
||||||
|
projectId,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
workspace: string;
|
||||||
|
projectId: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const pathname = usePathname() ?? "";
|
||||||
|
const projectBase = `/${workspace}/project/${projectId}`;
|
||||||
|
const isPreview =
|
||||||
|
pathname === `${projectBase}/preview` ||
|
||||||
|
pathname.startsWith(`${projectBase}/preview/`);
|
||||||
|
|
||||||
|
const [expandedSections, setExpandedSections] = useState<
|
||||||
|
Record<string, boolean>
|
||||||
|
>({
|
||||||
|
settings: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
|
if (isPreview) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleSection = (section: string) => {
|
||||||
|
setExpandedSections((prev) => ({ ...prev, [section]: !prev[section] }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const menuItems = [
|
||||||
|
{ segment: "overview", label: "Overview", Icon: LayoutGrid },
|
||||||
|
{ segment: "users", label: "Users", Icon: Users },
|
||||||
|
{
|
||||||
|
segment: "data",
|
||||||
|
label: "Data",
|
||||||
|
Icon: Database,
|
||||||
|
hasChildren: true,
|
||||||
|
},
|
||||||
|
{ segment: "analytics", label: "Analytics", Icon: BarChart2 },
|
||||||
|
{
|
||||||
|
segment: "marketing",
|
||||||
|
label: "Marketing",
|
||||||
|
Icon: TrendingUp,
|
||||||
|
badge: "New",
|
||||||
|
hasChildren: true,
|
||||||
|
},
|
||||||
|
{ segment: "domains", label: "Domains", Icon: Globe },
|
||||||
|
{ segment: "integrations", label: "Integrations", Icon: Plug },
|
||||||
|
{ segment: "security", label: "Security", Icon: ShieldCheck },
|
||||||
|
{ segment: "code", label: "Code", Icon: Code2 },
|
||||||
|
{ segment: "agents", label: "Agents", Icon: Bot, badge: "New" },
|
||||||
|
{ segment: "automations", label: "Automations", Icon: Zap },
|
||||||
|
{ segment: "logs", label: "Logs", Icon: Terminal },
|
||||||
|
{ segment: "api", label: "API", Icon: FileJson },
|
||||||
|
{
|
||||||
|
segment: "settings",
|
||||||
|
label: "Settings",
|
||||||
|
Icon: Settings,
|
||||||
|
hasChildren: true,
|
||||||
|
children: [
|
||||||
|
{ segment: "settings/app", label: "App Settings" },
|
||||||
|
{ segment: "settings/auth", label: "Authentication" },
|
||||||
|
{ segment: "settings/template", label: "App Template" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const filteredItems = menuItems.filter(
|
||||||
|
(item) =>
|
||||||
|
item.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
(item.children &&
|
||||||
|
item.children.some((child) =>
|
||||||
|
child.label.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: "flex", flex: 1, minHeight: 0, minWidth: 0 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 250,
|
||||||
|
borderRight: "1px solid #e4e4e7",
|
||||||
|
background: "#ffffff",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
padding: "16px 12px",
|
||||||
|
gap: 4,
|
||||||
|
overflowY: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "0.85rem",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "#18181b",
|
||||||
|
padding: "0 8px 12px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Dashboard
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search Bar */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
background: "#f4f4f5",
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: "6px 10px",
|
||||||
|
marginBottom: 12,
|
||||||
|
margin: "0 4px 12px 4px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Search size={14} color="#a1a1aa" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
style={{
|
||||||
|
border: "none",
|
||||||
|
background: "transparent",
|
||||||
|
outline: "none",
|
||||||
|
width: "100%",
|
||||||
|
fontSize: "0.8rem",
|
||||||
|
color: "#18181b",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||||
|
{filteredItems.map((item) => {
|
||||||
|
const isMainActive =
|
||||||
|
pathname === `${projectBase}/${item.segment}` ||
|
||||||
|
pathname.startsWith(`${projectBase}/${item.segment}/`);
|
||||||
|
const isExpanded = expandedSections[item.segment];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={item.segment}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
padding: "8px 10px",
|
||||||
|
borderRadius: 8,
|
||||||
|
cursor: "pointer",
|
||||||
|
background:
|
||||||
|
isMainActive && !item.hasChildren
|
||||||
|
? "#eff6ff"
|
||||||
|
: "transparent",
|
||||||
|
color:
|
||||||
|
isMainActive && !item.hasChildren ? "#1d4ed8" : "#52525b",
|
||||||
|
transition: "all 0.1s ease",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
if (item.hasChildren) {
|
||||||
|
toggleSection(item.segment);
|
||||||
|
} else {
|
||||||
|
// Navigate via link logic would happen here, we'll wrap the icon/label in a link
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.hasChildren ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 10,
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<item.Icon size={16} />
|
||||||
|
<span style={{ fontSize: "0.85rem", fontWeight: 500 }}>
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Link
|
||||||
|
href={`${projectBase}/${item.segment}`}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 10,
|
||||||
|
flex: 1,
|
||||||
|
textDecoration: "none",
|
||||||
|
color: "inherit",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<item.Icon size={16} />
|
||||||
|
<span style={{ fontSize: "0.85rem", fontWeight: 500 }}>
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{ display: "flex", alignItems: "center", gap: 6 }}
|
||||||
|
>
|
||||||
|
{item.badge && (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
background: "#eef2ff",
|
||||||
|
color: "#4f46e5",
|
||||||
|
fontSize: "0.65rem",
|
||||||
|
fontWeight: 600,
|
||||||
|
padding: "2px 6px",
|
||||||
|
borderRadius: 999,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.badge}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{item.hasChildren &&
|
||||||
|
(isExpanded ? (
|
||||||
|
<ChevronDown size={14} color="#a1a1aa" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight size={14} color="#a1a1aa" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Render Children if expanded */}
|
||||||
|
{item.hasChildren && isExpanded && item.children && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
marginTop: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.children.map((child) => {
|
||||||
|
const isChildActive =
|
||||||
|
pathname === `${projectBase}/${child.segment}`;
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={child.segment}
|
||||||
|
href={`${projectBase}/${child.segment}`}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "6px 10px 6px 36px",
|
||||||
|
borderRadius: 8,
|
||||||
|
fontSize: "0.8rem",
|
||||||
|
fontWeight: 500,
|
||||||
|
textDecoration: "none",
|
||||||
|
color: isChildActive ? "#18181b" : "#52525b",
|
||||||
|
background: isChildActive
|
||||||
|
? "#f4f4f5"
|
||||||
|
: "transparent",
|
||||||
|
transition: "all 0.1s ease",
|
||||||
|
border: isChildActive
|
||||||
|
? "1px solid #e4e4e7"
|
||||||
|
: "1px solid transparent",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{child.label}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 0,
|
||||||
|
overflow: "auto",
|
||||||
|
background: "#fff",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user