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