Action Drawer

Prerequisites

Ensure you have VizStats UI installed in your project. If not, follow the installation guide.

Click to switch between light and dark modes

Step 1: Install Dependencies

Install the necessary dependencies using your preferred package manager:

npm install zustand react-responsive

Step 2: Set Up the actionDrawerStore

Create a new file called actionDrawerStore.ts in your store directory: store/actionDrawerStore.ts

import { create } from "zustand";

type FilterState = {
  isDrawerOpen: boolean;
  actionDrawerContent: React.ReactNode; // New state for dynamic content

  toggleDrawer: () => void;
  setDrawerContent: (content: React.ReactNode) => void;

  openDrawer: () => void;
  closeDrawer: () => void;
};

const useActionDrawerStore = create<FilterState>((set) => ({
  isDrawerOpen: false,
  actionDrawerContent: null,
  toggleDrawer: () => set((state) => ({ isDrawerOpen: !state.isDrawerOpen })),
  setDrawerContent: (content) => set({ actionDrawerContent: content }),
  openDrawer: () => set({ isDrawerOpen: true }),
  closeDrawer: () => set({ isDrawerOpen: false, actionDrawerContent: null }),
}));

export default useActionDrawerStore;

Step 3: Set Up hooks

Create a new file called useDeviceType.ts in your hooks directory: hooks/useDeviceType.ts

import { useMediaQuery } from "react-responsive";

const useDeviceType = () => {
  const isMobile = useMediaQuery({ maxWidth: 639, minWidth: 200 });
  const isTablet = useMediaQuery({ minWidth: 640, maxWidth: 1023 });
  const isDesktop = useMediaQuery({ minWidth: 1024 });

  return { isMobile, isTablet, isDesktop };
};

export default useDeviceType;

Step 4: Create Action Drawer Component

ActionDrawer

Create a new component called action-drawer.tsx

"use client";

import { useCallback, ReactNode, useMemo } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Filter } from "lucide-react";
import { Cross2Icon } from "@radix-ui/react-icons";
import { Button, ButtonProps } from "@/components/ui/button";
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip";
import useDeviceType from "@/hooks/useDeviceType";
import useActionDrawerStore from "@/store/actionDrawerStore";
import { cn } from "@/lib/utils";

type ActionButtonProps = {
  children?: ReactNode;
  className?: string;
  label?: string;
  variant?: ButtonProps["variant"];
  size?: ButtonProps["size"];
};

type ActionTooltipProps = ActionButtonProps & {
  tooltip: string;
};

const sharedButtonStyles =
  "gap-2 rounded-lg shadow-lg transition-all duration-300 active:scale-95";

export const ActionDrawerButton: React.FC<ActionButtonProps> = ({
  children,
  className,
  label = "Button",
  variant = "default",
  size = "default",
}) => {
  const { toggleDrawer, setDrawerContent } = useActionDrawerStore();

  const handleClick = useCallback(() => {
    setDrawerContent(children);
    toggleDrawer();
  }, [children, setDrawerContent, toggleDrawer]);

  return (
    <Button
      variant={variant}
      size={size}
      className={cn(className, sharedButtonStyles)}
      onClick={handleClick}
    >
      <Filter className="h-4 w-4" />
      <span className="text-pxs sm:text-sm">{label}</span>
    </Button>
  );
};

export const ActionDrawerTooltip: React.FC<ActionTooltipProps> = ({
  children,
  label,
  className,
  tooltip = "Tooltip",
  variant = "default",
  size = "icon",
}) => {
  const { toggleDrawer, setDrawerContent } = useActionDrawerStore();

  const handleClick = useCallback(() => {
    setDrawerContent(children);
    toggleDrawer();
  }, [children, setDrawerContent, toggleDrawer]);

  return (
    <TooltipProvider>
      <Tooltip>
        <TooltipTrigger asChild>
          <Button
            variant={variant}
            size={size}
            className={cn(className, sharedButtonStyles)}
            onClick={handleClick}
          >
            <Filter className="h-4 w-4" />
            {label && <span className="text-pxs sm:text-sm">{label}</span>}
            <span className="sr-only">{label}</span>
          </Button>
        </TooltipTrigger>
        {tooltip && <TooltipContent side="top">{tooltip}</TooltipContent>}
      </Tooltip>
    </TooltipProvider>
  );
};

export const ActionDrawer = () => {
  const { isMobile } = useDeviceType();
  const { isDrawerOpen, closeDrawer, actionDrawerContent } =
    useActionDrawerStore();

  const memoizedContent = useMemo(
    () => actionDrawerContent,
    [actionDrawerContent]
  );
  const handleOverlayClick = () => {
    if (isDrawerOpen) {
      closeDrawer();
    }
  };

  const animationVariants = useMemo(
    () => ({
      initial: { x: "-100%" },
      animate: { x: 0 },
      exit: { x: "-100%" },
    }),
    []
  );

  return (
    <AnimatePresence>
      {isDrawerOpen && (
        <>
          {isMobile && (
            <div
              className="fixed inset-0 z-[35] bg-black opacity-50"
              aria-hidden="true"
              tabIndex={-1}
              onClick={handleOverlayClick}
            />
          )}

          <motion.div
            className="fixed left-0 top-0 z-40 h-screen overflow-hidden bg-background shadow-lg sm:border-r dark:bg-background sm:py-10"
            initial="initial"
            animate="animate"
            exit="exit"
            variants={animationVariants}
            transition={{
              type: isDrawerOpen ? "tween" : "spring",
              duration: 0.3,
            }}
            role="dialog"
            aria-modal="true"
          >
            <div className="h-full w-64 sm:w-72">
              <div className="flex h-full w-full flex-col gap-4">
                {!isMobile && (
                  <Button
                    variant="ghost"
                    size="icon"
                    className="absolute right-3 top-3 h-4 w-4 rounded-full p-0 opacity-70 ring-offset-background transition-opacity data-[state=open]:bg-secondary hover:bg-transparent hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none"
                    onClick={closeDrawer}
                  >
                    <Cross2Icon className="h-4 w-4" />
                    <span className="sr-only">Close</span>
                  </Button>
                )}
                <div className="py-2 mx-auto">{memoizedContent}</div>
              </div>
            </div>
          </motion.div>
        </>
      )}
    </AnimatePresence>
  );
};

Step 5: Use the Component

Add the ThemeToggle component to your layout or navbar:

import { ActionDrawer, ActionDrawerButton } from "@/components/elements/action-drawer";

export function Page() {
  return (
    <main>
      <ActionDrawerButton label="Filter" variant="secondary">
        {/* Other  content */}
      </ActionDrawerButton>
      <ActionDrawer />
    </main>
  )
}