Sidebar Double-Sided Mail

PreviousNext

A sidebar double-sided mail block.

Docs
blocksblock

Preview

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

import { Archive, Flag } from "lucide-react";
import * as React from "react";

import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInput,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  useSidebar,
} from "@/components/ui/sidebar";
import {
  IconBrandAmongUs,
  IconCarambola,
  IconHourglassHigh,
  IconMailbox,
} from "@tabler/icons-react";
import { useMail } from "@/components/sidebar-04/mail-context";
import { NavUser } from "@/components/sidebar-04/nav-user";

// This is sample data
const data = {
  user: {
    name: "ephraim",
    email: "ephraim@blocks.so",
    avatar: "/avatar-01.png",
  },
  navMain: [
    {
      title: "Inbox",
      url: "#",
      icon: IconMailbox,
      isActive: true,
    },
    {
      title: "Starred",
      url: "#",
      icon: IconCarambola,
      isActive: false,
    },
    {
      title: "Important",
      url: "#",
      icon: Flag,
      isActive: false,
    },
    {
      title: "Scheduled",
      url: "#",
      icon: IconHourglassHigh,
      isActive: false,
    },
    {
      title: "Archive",
      url: "#",
      icon: Archive,
      isActive: false,
    },
  ],
  labels: [
    { title: "Personal", color: "bg-green-400 dark:bg-green-300" },
    { title: "Work", color: "bg-blue-400 dark:bg-blue-300" },
    { title: "Travel", color: "bg-orange-400 dark:bg-orange-300" },
    { title: "Receipts", color: "bg-purple-400 dark:bg-purple-300" },
  ],
  mails: [
    {
      name: "Nora Patel",
      email: "nora@acme.co",
      subject: "Welcome to Acme Mail",
      date: "08:15 AM",
      teaser:
        "Hi there — here's a quick tour of your new inbox.\nPin, label, and schedule messages to stay organized.",
    },
    {
      name: "Stripe",
      email: "no-reply@stripe.com",
      subject: "Your payout has arrived",
      date: "Yesterday",
      teaser:
        "A payout of $3,245.90 was sent to your bank account.\nView details in your dashboard.",
    },
    {
      name: "GitHub",
      email: "noreply@github.com",
      subject: "New activity on acme/app",
      date: "Tue",
      teaser:
        "3 pull requests need your review. CI passed on main.\nClick to open the review queue.",
    },
    {
      name: "Ava Chen",
      email: "ava.chen@example.com",
      subject: "Agenda for Friday standup",
      date: "Mon",
      teaser:
        "Let's cover onboarding, billing bugs, and Q4 goals.\nReply with anything you want to add.",
    },
    {
      name: "Figma",
      email: "updates@figma.com",
      subject: "What's new in Figma",
      date: "Sep 12",
      teaser:
        "Variables, auto layout improvements, and dev mode updates.\nWatch the recap to learn more.",
    },
    {
      name: "Linear",
      email: "bot@linear.app",
      subject: "[ACME-432] Edit modal broken",
      date: "Sep 11",
      teaser:
        "Issue created by Wendy. Repro steps included.\nSeverity: high, priority: P1.",
    },
    {
      name: "Airbnb",
      email: "booking@airbnb.com",
      subject: "Your trip to Kyoto",
      date: "Sep 10",
      teaser:
        "Get ready for your stay. Check-in details and local recommendations inside.",
    },
    {
      name: "Notion",
      email: "team@notion.so",
      subject: "Weekly team recap",
      date: "Sep 09",
      teaser:
        "Marketing shipped pricing page revamp. Eng closed 14 issues.\nSee full notes in the doc.",
    },
    {
      name: "Google Calendar",
      email: "calendar@google.com",
      subject: "Design sync moved",
      date: "Sep 09",
      teaser:
        "Event 'Design sync' moved to 3:30 PM. Meet link updated in the invite.",
    },
    {
      name: "HN Daily",
      email: "digest@ycombinator.com",
      subject: "Top stories today",
      date: "Sep 08",
      teaser:
        "OpenAI releases new API, SQLite 3.46, and why we left Kubernetes.\nRead the full digest.",
    },
  ],
};

export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
  // Note: I'm using state to show active item.
  // IRL you should use the url/router.
  const [activeItem, setActiveItem] = React.useState(data.navMain[0]);
  const [mails, setMails] = React.useState(data.mails);
  const [query, setQuery] = React.useState("");
  const { setOpen } = useSidebar();
  const { setSelectedMail } = useMail();

  const filteredMails = React.useMemo(() => {
    const q = query.trim().toLowerCase();
    if (!q) return mails;
    return mails.filter((m) =>
      [m.name, m.email, m.subject, m.teaser]
        .join("\n")
        .toLowerCase()
        .includes(q)
    );
  }, [mails, query]);

  return (
    <div className="flex">
      {/* This is the first sidebar */}
      <Sidebar
        style={{ "--sidebar-width": "12rem" } as React.CSSProperties}
        collapsible="none"
        className="border-r p-2 px-1"
      >
        <SidebarHeader>
          <SidebarMenu>
            <SidebarMenuItem>
              <SidebarMenuButton size="lg" asChild className="md:h-8 md:p-0">
                <a href="#">
                  <div className="bg-sidebar-accent text-sidebar-accent-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
                    <IconBrandAmongUs className="size-4" />
                  </div>
                  <div className="grid flex-1 text-left text-sm leading-tight">
                    <span className="truncate font-medium">Acme Inc</span>
                    <span className="truncate text-xs">Enterprise</span>
                  </div>
                </a>
              </SidebarMenuButton>
            </SidebarMenuItem>
          </SidebarMenu>
        </SidebarHeader>
        <SidebarContent>
          <SidebarGroup>
            <SidebarGroupContent className="px-1.5 md:px-0">
              <SidebarMenu>
                {data.navMain.map((item) => (
                  <SidebarMenuItem key={item.title}>
                    <SidebarMenuButton
                      onClick={() => {
                        setActiveItem(item);
                        const mail = data.mails.sort(() => Math.random() - 0.5);
                        setMails(
                          mail.slice(
                            0,
                            Math.max(5, Math.floor(Math.random() * 10) + 1)
                          )
                        );
                        setOpen(true);
                      }}
                      isActive={activeItem?.title === item.title}
                      className="px-2.5 md:px-2"
                    >
                      <item.icon />
                      <span>{item.title}</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                ))}
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
          <SidebarGroup>
            <SidebarGroupLabel className="px-1.5 md:px-0 text-xs">
              Labels
            </SidebarGroupLabel>
            <SidebarGroupContent className="px-1.5 md:px-0">
              <SidebarMenu>
                {data.labels.map((label) => (
                  <SidebarMenuItem key={label.title}>
                    <SidebarMenuButton asChild className="px-2.5 md:px-2">
                      <div className="flex items-center gap-3">
                        <div
                          className={`h-3 w-3 rounded-[4px] ${label.color}`}
                        ></div>
                        <span>{label.title}</span>
                      </div>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                ))}
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
        </SidebarContent>
        <SidebarFooter>
          <NavUser user={data.user} />
        </SidebarFooter>
      </Sidebar>

      {/* This is the second sidebar */}
      {/* We disable collapsible and let it fill remaining space */}
      <Sidebar
        collapsible="none"
        className="hidden flex-1 border-r md:flex min-w-96"
      >
        <SidebarHeader className="gap-3.5 border-b p-4">
          <div className="flex w-full items-center justify-between">
            <div className="text-foreground text-base font-medium">
              {activeItem?.title}
            </div>
          </div>
          <SidebarInput
            placeholder="Type to search..."
            value={query}
            onChange={(e) => setQuery(e.target.value)}
          />
        </SidebarHeader>
        <SidebarContent>
          <SidebarGroup className="px-0 pt-0">
            <SidebarGroupContent>
              {filteredMails.length === 0 && (
                <div className="text-muted-foreground p-4 text-sm">
                  No results
                </div>
              )}
              {filteredMails.map((mail) => (
                <a
                  href="#"
                  key={mail.email}
                  onClick={(e) => {
                    e.preventDefault();
                    setSelectedMail(mail);
                  }}
                  className="hover:bg-sidebar-accent hover:text-sidebar-accent-foreground flex flex-col items-start gap-2 border-b p-4 text-sm leading-tight whitespace-nowrap"
                >
                  <div className="flex w-full items-center gap-2">
                    <span>{mail.name}</span>{" "}
                    <span className="ml-auto text-xs">{mail.date}</span>
                  </div>
                  <span className="font-medium">{mail.subject}</span>
                  <span className="line-clamp-2 w-[260px] text-xs whitespace-break-spaces">
                    {mail.teaser}
                  </span>
                </a>
              ))}
            </SidebarGroupContent>
          </SidebarGroup>
        </SidebarContent>
      </Sidebar>
    </div>
  );
}

Installation

npx shadcn@latest add @blocks/sidebar-04

Usage

import { Sidebar04 } from "@/components/sidebar-04"
<Sidebar04 />