Sidebar Double-Sided

PreviousNext

A sidebar double-sided block.

Docs
blocksblock

Preview

Loading preview…
content/components/sidebar/sidebar-05/app-sidebar.tsx
"use client";

import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupContent,
  SidebarHeader,
  SidebarMenu,
  SidebarMenuBadge,
  SidebarMenuButton,
  SidebarMenuItem,
} from "@/components/ui/sidebar";
import { cn } from "@/lib/utils";
import {
  IconActivityHeartbeat,
  IconArchive,
  IconBackground,
  IconBellRinging,
  IconBrandGoogle,
  IconBrandMeta,
  IconBrandNpm,
  IconBrandOpenai,
  IconBug,
  IconChartBar,
  IconChevronRight,
  IconCloud,
  IconDatabase,
  IconFileText,
  IconFolder,
  IconFolders,
  IconGitCommit,
  IconGitMerge,
  IconGitPullRequest,
  IconHome,
  IconKey,
  IconLockExclamation,
  IconLockPassword,
  IconLogout,
  IconNorthStar,
  IconPackageExport,
  IconPackages,
  IconPasswordFingerprint,
  IconPlayerPlay,
  IconScanEye,
  IconSettings,
  IconShieldLock,
  IconStar,
  IconTarget,
  IconTerminal2,
  IconUser,
  IconUserPlus,
  IconWebhook,
  IconX,
} from "@tabler/icons-react";
import type React from "react";
import { useState } from "react";
import { TeamSwitcher } from "@/components/sidebar-05/team-switcher";

const data = {
  teams: [
    {
      name: "OpenAI",
      logo: IconBrandOpenai,
      plan: "Enterprise",
    },
    {
      name: "Anthropic",
      logo: IconNorthStar,
      plan: "Pro",
    },
    {
      name: "Google",
      logo: IconBrandGoogle,
      plan: "Free",
    },
    {
      name: "Meta",
      logo: IconBrandMeta,
      plan: "Free",
    },
  ],
};

interface SidebarItem {
  id: string;
  label: string;
  icon: React.ComponentType<{ className?: string }>;
  badge?: string;
  hasSubItems?: boolean;
  route?: string;
  subItems?: {
    id: string;
    label: string;
    icon: React.ComponentType<{ className?: string }>;
    description?: string;
    route?: string;
  }[];
}

const sidebarItems: SidebarItem[] = [
  {
    id: "overview",
    label: "Overview",
    icon: IconHome,
    hasSubItems: true,
    subItems: [
      {
        id: "dashboard",
        label: "Dashboard",
        icon: IconChartBar,
        description: "Project overview and activity",
        route: "/overview/dashboard",
      },
      {
        id: "activity",
        label: "Activity",
        icon: IconActivityHeartbeat,
        description: "Recent commits and changes",
        route: "/overview/activity",
      },
      {
        id: "insights",
        label: "Insights",
        icon: IconTarget,
        description: "Code analytics and metrics",
        route: "/overview/insights",
      },
    ],
  },
  {
    id: "repositories",
    label: "Repositories",
    icon: IconFolders,
    badge: "12",
    hasSubItems: true,
    subItems: [
      {
        id: "all-repos",
        label: "All Repositories",
        icon: IconFolder,
        description: "Browse all your repositories",
        route: "/repositories",
      },
      {
        id: "starred",
        label: "Starred",
        icon: IconStar,
        description: "Your starred repositories",
        route: "/repositories/starred",
      },
      {
        id: "archived",
        label: "Archived",
        icon: IconArchive,
        description: "Archived repositories",
        route: "/repositories/archived",
      },
    ],
  },
  {
    id: "pull-requests",
    label: "Pull Requests",
    icon: IconGitPullRequest,
    badge: "3",
    hasSubItems: true,
    subItems: [
      {
        id: "open-prs",
        label: "Open",
        icon: IconGitPullRequest,
        description: "Open pull requests",
        route: "/pull-requests/open",
      },
      {
        id: "review-requests",
        label: "Review Requests",
        icon: IconScanEye,
        description: "PRs awaiting your review",
        route: "/pull-requests/review",
      },
      {
        id: "merged",
        label: "Merged",
        icon: IconGitMerge,
        description: "Recently merged PRs",
        route: "/pull-requests/merged",
      },
    ],
  },
  {
    id: "issues",
    label: "Issues",
    icon: IconBug,
    badge: "7",
    hasSubItems: true,
    subItems: [
      {
        id: "open-issues",
        label: "Open Issues",
        icon: IconBug,
        description: "Active issues and bugs",
        route: "/issues/open",
      },
      {
        id: "assigned",
        label: "Assigned to Me",
        icon: IconUserPlus,
        description: "Issues assigned to you",
        route: "/issues/assigned",
      },
      {
        id: "created",
        label: "Created by Me",
        icon: IconGitCommit,
        description: "Issues you've created",
        route: "/issues/created",
      },
    ],
  },
  {
    id: "actions",
    label: "Actions",
    icon: IconBackground,
    hasSubItems: true,
    subItems: [
      {
        id: "workflows",
        label: "Workflows",
        icon: IconPlayerPlay,
        description: "CI/CD workflows and pipelines",
        route: "/actions/workflows",
      },
      {
        id: "runners",
        label: "Runners",
        icon: IconTerminal2,
        description: "Self-hosted runners",
        route: "/actions/runners",
      },
      {
        id: "deployments",
        label: "Deployments",
        icon: IconCloud,
        description: "Deployment history",
        route: "/actions/deployments",
      },
    ],
  },
  {
    id: "packages",
    label: "Packages",
    icon: IconPackages,
    hasSubItems: true,
    subItems: [
      {
        id: "published",
        label: "Published",
        icon: IconPackageExport,
        description: "Your published packages",
        route: "/packages/published",
      },
      {
        id: "container-registry",
        label: "Container Registry",
        icon: IconDatabase,
        description: "Docker images and containers",
        route: "/packages/containers",
      },
      {
        id: "npm-packages",
        label: "npm Packages",
        icon: IconBrandNpm,
        description: "Node.js packages",
        route: "/packages/npm",
      },
    ],
  },
  {
    id: "security",
    label: "Security",
    icon: IconLockPassword,
    badge: "2",
    hasSubItems: true,
    subItems: [
      {
        id: "alerts",
        label: "Security Alerts",
        icon: IconLockExclamation,
        description: "Vulnerability alerts",
        route: "/security/alerts",
      },
      {
        id: "advisories",
        label: "Advisories",
        icon: IconShieldLock,
        description: "Security advisories",
        route: "/security/advisories",
      },
      {
        id: "secrets",
        label: "Secrets",
        icon: IconPasswordFingerprint,
        description: "Repository secrets",
        route: "/security/secrets",
      },
    ],
  },
  {
    id: "settings",
    label: "Settings",
    icon: IconSettings,
    hasSubItems: true,
    subItems: [
      {
        id: "profile",
        label: "Profile",
        icon: IconUser,
        description: "Your profile settings",
        route: "/settings/profile",
      },
      {
        id: "notifications",
        label: "Notifications",
        icon: IconBellRinging,
        description: "Notification preferences",
        route: "/settings/notifications",
      },
      {
        id: "webhooks",
        label: "Webhooks",
        icon: IconWebhook,
        description: "Webhook configurations",
        route: "/settings/webhooks",
      },
      {
        id: "api-keys",
        label: "API Keys",
        icon: IconKey,
        description: "Personal access tokens",
        route: "/settings/api-keys",
      },
    ],
  },
  {
    id: "docs",
    label: "Documentation",
    icon: IconFileText,
    hasSubItems: false,
    route: "/docs",
  },
];

export function AppSidebar() {
  const [activeItem, setActiveItem] = useState<string | null>("overview");
  const [selectedSubItem, setSelectedSubItem] = useState<string | null>(null);

  const activeItemData = sidebarItems.find((item) => item.id === activeItem);

  const handleItemClick = (item: SidebarItem) => {
    if (item.hasSubItems) {
      const isActive = activeItem === item.id;
      setActiveItem(isActive ? null : item.id);
      if (isActive) {
        setSelectedSubItem(null);
      }
    } else {
      if (activeItem) {
        setActiveItem(null);
        setSelectedSubItem(null);
      }
      console.log(`[v0] Navigating to: ${item.route}`);
    }
  };

  const handleSubItemClick = (subItem: { id: string; route?: string }) => {
    setSelectedSubItem(selectedSubItem === subItem.id ? null : subItem.id);
    if (subItem.route) {
      console.log(`[v0] Navigating to: ${subItem.route}`);
    }
  };

  return (
    <div className="flex h-screen bg-background">
      <Sidebar
        side="left"
        variant="sidebar"
        collapsible="none"
        className="w-64 border-r"
      >
        <SidebarHeader>
          <TeamSwitcher teams={data.teams} />
        </SidebarHeader>

        <SidebarContent>
          <SidebarGroup>
            <SidebarGroupContent>
              <SidebarMenu>
                {sidebarItems.map((item) => {
                  const Icon = item.icon;
                  const isActive = activeItem === item.id;
                  const chevronIndicator = (
                    <IconChevronRight
                      className={cn(
                        "h-4 w-4 transition-transform shrink-0",
                        isActive && "rotate-90"
                      )}
                    />
                  );

                  return (
                    <SidebarMenuItem key={item.id}>
                      <SidebarMenuButton
                        isActive={isActive}
                        className="w-full h-10 px-3"
                        onClick={() => handleItemClick(item)}
                      >
                        <div className="flex items-center gap-3 flex-1 min-w-0">
                          <Icon className="h-4 w-4 shrink-0" />
                          <span className="truncate">{item.label}</span>
                        </div>
                        <div className="flex items-center gap-1 shrink-0 ml-auto min-w-fit">
                          {(item.badge || item.hasSubItems) &&
                            (item.badge ? (
                              <SidebarMenuBadge
                                className={cn(
                                  "min-w-fit",
                                  item.hasSubItems && "gap-x-3"
                                )}
                              >
                                {item.badge}
                                {item.hasSubItems && chevronIndicator}
                              </SidebarMenuBadge>
                            ) : (
                              chevronIndicator
                            ))}
                        </div>
                      </SidebarMenuButton>
                    </SidebarMenuItem>
                  );
                })}
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
        </SidebarContent>

        <SidebarFooter>
          <SidebarMenu>
            <SidebarMenuItem>
              <SidebarMenuButton className="w-full h-12 px-3">
                <div className="flex items-center gap-3 flex-1 min-w-0">
                  <Avatar className="h-8 w-8 rounded-full">
                    <AvatarImage src="/avatar-01.png" alt="ephraim" />
                    <AvatarFallback className="rounded-full">E</AvatarFallback>
                  </Avatar>
                  <div className="flex-1 text-left min-w-0">
                    <div className="text-sm font-medium truncate">ephraim</div>
                    <div className="text-xs text-muted-foreground truncate">
                      ephraim@blocks.so
                    </div>
                  </div>
                </div>
                <IconLogout className="h-4 w-4 shrink-0" />
              </SidebarMenuButton>
            </SidebarMenuItem>
          </SidebarMenu>
        </SidebarFooter>
      </Sidebar>

      {activeItem && activeItemData?.subItems && (
        <Sidebar
          side="left"
          variant="sidebar"
          collapsible="none"
          className="w-72 animate-in slide-in-from-left-5 duration-200 border-r"
        >
          <SidebarHeader className="flex flex-row items-center justify-between border-b px-4">
            <h3 className="font-medium">{activeItemData.label}</h3>
            <button
              onClick={() => setActiveItem(null)}
              className="h-6 w-6 p-0 rounded-md hover:bg-sidebar-accent flex items-center justify-center"
            >
              <IconX className="h-4 w-4" />
            </button>
          </SidebarHeader>

          <SidebarContent>
            <SidebarGroup>
              <SidebarGroupContent>
                <SidebarMenu>
                  {activeItemData.subItems.map((subItem) => {
                    const SubIcon = subItem.icon;
                    const isSelected = selectedSubItem === subItem.id;

                    return (
                      <SidebarMenuItem key={subItem.id}>
                        <SidebarMenuButton
                          isActive={isSelected}
                          className="w-full justify-start gap-3 h-auto py-2 px-3"
                          onClick={() => handleSubItemClick(subItem)}
                        >
                          <SubIcon className="h-5 w-5 shrink-0 self-start mt-0.5" />

                          <div className="flex-1 text-left min-w-0">
                            <div className="font-medium">{subItem.label}</div>
                            {subItem.description && (
                              <div className="text-xs text-muted-foreground mt-0.5">
                                {subItem.description}
                              </div>
                            )}
                          </div>
                        </SidebarMenuButton>
                      </SidebarMenuItem>
                    );
                  })}
                </SidebarMenu>
              </SidebarGroupContent>
            </SidebarGroup>
          </SidebarContent>
        </Sidebar>
      )}
    </div>
  );
}

Installation

npx shadcn@latest add @blocks/sidebar-05

Usage

import { Sidebar05 } from "@/components/sidebar-05"
<Sidebar05 />