332 lines
10 KiB
TypeScript
332 lines
10 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import Link from "next/link";
|
|
import { usePathname } from "next/navigation";
|
|
import {
|
|
Search,
|
|
LayoutGrid,
|
|
ClipboardList,
|
|
Database,
|
|
Globe,
|
|
Code2,
|
|
Terminal,
|
|
Settings,
|
|
ChevronDown,
|
|
ChevronRight,
|
|
Users,
|
|
HardDrive,
|
|
Blocks,
|
|
} from "lucide-react";
|
|
|
|
import { useAnatomy } from "@/components/project/use-anatomy";
|
|
|
|
type MenuItem = {
|
|
segment: string;
|
|
label: string;
|
|
Icon: React.ElementType;
|
|
badge?: string;
|
|
hasChildren?: boolean;
|
|
children?: { segment: string; label: string }[];
|
|
};
|
|
|
|
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,
|
|
data: true,
|
|
});
|
|
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
|
|
const { anatomy } = useAnatomy(projectId);
|
|
const databases = anatomy?.infrastructure?.databases ?? [];
|
|
|
|
if (isPreview) {
|
|
return <>{children}</>;
|
|
}
|
|
|
|
const handleSectionClick = (segment: string) => {
|
|
if (!expandedSections[segment]) {
|
|
setExpandedSections((prev) => ({ ...prev, [segment]: true }));
|
|
}
|
|
};
|
|
|
|
const menuItems: MenuItem[] = [
|
|
{ segment: "overview", label: "Overview", Icon: LayoutGrid },
|
|
{ segment: "plan", label: "Plan Docs", Icon: ClipboardList },
|
|
{ segment: "code", label: "Codebase", Icon: Code2 },
|
|
{ segment: "users", label: "Users", Icon: Users },
|
|
{
|
|
segment: "data",
|
|
label: "Databases",
|
|
Icon: Database,
|
|
hasChildren: true,
|
|
children: databases.map((db) => ({
|
|
segment: `data/tables?db=${db.uuid}`,
|
|
label: db.name,
|
|
})),
|
|
},
|
|
{ segment: "storage", label: "File Storage", Icon: HardDrive },
|
|
{ segment: "services", label: "Live Servers", Icon: Blocks },
|
|
{ segment: "domains", label: "Custom Domains", Icon: Globe },
|
|
{ segment: "logs", label: "Server Logs", Icon: Terminal },
|
|
{
|
|
segment: "settings",
|
|
label: "Settings",
|
|
Icon: Settings,
|
|
hasChildren: true,
|
|
children: [
|
|
{ segment: "settings/app", label: "App Settings" },
|
|
{ segment: "settings/auth", label: "Authentication" },
|
|
],
|
|
},
|
|
];
|
|
|
|
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 #e5e7eb",
|
|
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
|
|
? "#f3f4f6"
|
|
: "transparent",
|
|
color:
|
|
isMainActive && !item.hasChildren ? "#111827" : "#4b5563",
|
|
transition: "all 0.1s ease",
|
|
}}
|
|
onClick={() => {
|
|
if (item.hasChildren) {
|
|
setExpandedSections((prev) => ({
|
|
...prev,
|
|
[item.segment]: !prev[item.segment],
|
|
}));
|
|
}
|
|
}}
|
|
>
|
|
{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: "#f3f4f6",
|
|
color: "#4b5563",
|
|
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 href = child.segment.includes("?")
|
|
? `${projectBase}/${child.segment.split("?")[0]}?${child.segment.split("?")[1]}`
|
|
: `${projectBase}/${child.segment}`;
|
|
let isChildActive = false;
|
|
if (child.segment.includes("?")) {
|
|
const [basePath, searchStr] = child.segment.split("?");
|
|
isChildActive =
|
|
pathname === `${projectBase}/${basePath}` &&
|
|
(typeof window !== "undefined"
|
|
? window.location.search.includes(searchStr)
|
|
: false);
|
|
} else {
|
|
isChildActive =
|
|
pathname === `${projectBase}/${child.segment}`;
|
|
}
|
|
|
|
return (
|
|
<Link
|
|
key={child.segment}
|
|
href={href}
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
padding: "6px 10px 6px 14px",
|
|
marginLeft: "18px",
|
|
borderRadius: "0 8px 8px 0",
|
|
fontSize: "0.8rem",
|
|
fontWeight: 500,
|
|
textDecoration: "none",
|
|
color: isChildActive ? "#18181b" : "#52525b",
|
|
background: "transparent",
|
|
transition: "all 0.1s ease",
|
|
borderLeft: isChildActive
|
|
? "2px solid #18181b"
|
|
: "2px solid transparent",
|
|
}}
|
|
>
|
|
{child.label}
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
<div
|
|
style={{
|
|
flex: 1,
|
|
minWidth: 0,
|
|
overflow: "auto",
|
|
background:
|
|
"radial-gradient(120% 80% at 50% 0%, #ffffff 0%, #f9fafb 52%, #f3f4f6 100%)",
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
}}
|
|
>
|
|
{children}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|