Home Dashboard

Complete home dashboard template.

Installation

npx shadcn@latest add https://boardcn.dev/r/home-dashboard.json
npx shadcn@latest add https://boardcn.dev/r/home-dashboard.json

npm packages

  • @remixicon/react
  • react-aria-components
  • recharts

What it composes

21 BoardCN components, installed automatically.

Source

The 6 files the CLI copies into your project.

components/templates/home-dashboard/home-dashboard.tsx
"use client";

import { useEffect, useRef, useState } from "react";
import { Dialog, Modal, ModalOverlay } from "react-aria-components";
import { RiAddLine, RiCloseLine, RiEqualizer2Line, RiMenuLine, RiNotification3Line } from "@remixicon/react";
import { DashboardSidebar } from "@/components/blocks/dashboard/dashboard-sidebar";
import { StatCards } from "@/components/blocks/dashboard/stat-cards";
import { Avatar } from "@/components/base/avatar/avatar";
import { Badge } from "@/components/base/badges/badge";
import { Breadcrumb, BreadcrumbItem } from "@/components/base/breadcrumb/breadcrumb";
import { Button } from "@/components/base/buttons/button";
import { IconButton } from "@/components/base/buttons/icon-button";
import { Dropdown, DropdownItem, DropdownPopover, DropdownTrigger } from "@/components/base/dropdown/dropdown";
import { InputBase } from "@/components/base/input/input";
import { Select, SelectItem } from "@/components/base/select/select";
import type { Stat } from "@/components/blocks/dashboard/stat-cards";
import { ContributionsCard, type ContributionsStat } from "@/components/charts/contributions-card";
import { EarningsChartCard, type EarningsChartPeriods } from "@/components/charts/earnings-chart-card";
import { cx } from "@/utils/cx";
import { HomeCustomersTable } from "./customers-table";
import { RecentHiresCard } from "./recent-hires-card";
import { RevenueChartCard, type RevenuePeriods } from "./revenue-chart-card";
import type {
  HomeDashboardCustomer,
  HomeDashboardHire,
  HomeDashboardNotification,
} from "./types";

function MobileNavigation({ isOpen, onOpenChange }: { isOpen: boolean; onOpenChange: (open: boolean) => void }) {
  return (
    <ModalOverlay isOpen={isOpen} onOpenChange={onOpenChange} isDismissable className="fixed inset-0 z-50 flex bg-black/40 backdrop-blur-[2px] transition-opacity duration-300 data-[entering]:opacity-0 data-[exiting]:opacity-0 lg:hidden">
      <Modal className="h-full w-[min(320px,calc(100vw-48px))] bg-background-full p-3 shadow-dropdown outline-none transition-transform duration-300 ease-[cubic-bezier(0.32,0.72,0,1)] data-[entering]:-translate-x-full data-[exiting]:-translate-x-full">
        <Dialog aria-label="Navigation" className="h-full outline-none">
          <DashboardSidebar mobile fluid flat onClose={() => onOpenChange(false)} className="h-full" />
        </Dialog>
      </Modal>
    </ModalOverlay>
  );
}

function TicketDialog({ isOpen, onOpenChange }: { isOpen: boolean; onOpenChange: (open: boolean) => void }) {
  return (
    <ModalOverlay isOpen={isOpen} onOpenChange={onOpenChange} isDismissable className="fixed inset-0 z-60 flex items-center justify-center bg-black/50 p-4 backdrop-blur-[2px] transition-opacity duration-200 data-[entering]:opacity-0 data-[exiting]:opacity-0">
      <Modal className="w-full max-w-[430px] rounded-3xl bg-background-full p-5 shadow-dropdown outline-none transition-[opacity,transform] duration-200 data-[entering]:scale-95 data-[entering]:opacity-0 data-[exiting]:scale-95 data-[exiting]:opacity-0">
        <Dialog aria-label="Create ticket" className="flex flex-col gap-5 outline-none">
          <div className="flex items-start justify-between gap-4">
            <div>
              <h2 className="text-title-2-medium text-text-primary">Create ticket</h2>
              <p className="mt-1 text-body-regular text-text-secondary">Start a new request for the BoardCN team.</p>
            </div>
            <IconButton icon={RiCloseLine} size="small" aria-label="Close create ticket" onClick={() => onOpenChange(false)} />
          </div>
          <label className="flex flex-col gap-1.5 text-body-medium text-text-primary">
            Ticket title
            <InputBase autoFocus aria-label="Ticket title" placeholder="What needs attention?" />
          </label>
          <label className="flex flex-col gap-1.5 text-body-medium text-text-primary">
            Priority
            <Select aria-label="Ticket priority" defaultSelectedKey="normal">
              <SelectItem id="low" textValue="Low">Low</SelectItem>
              <SelectItem id="normal" textValue="Normal">Normal</SelectItem>
              <SelectItem id="urgent" textValue="Urgent">Urgent</SelectItem>
            </Select>
          </label>
          <div className="flex justify-end gap-2">
            <Button variant="secondary" onClick={() => onOpenChange(false)}>Cancel</Button>
            <Button onClick={() => onOpenChange(false)}>Create ticket</Button>
          </div>
        </Dialog>
      </Modal>
    </ModalOverlay>
  );
}

function NotificationMenu({ notifications }: { notifications: readonly HomeDashboardNotification[] }) {
  const [open, setOpen] = useState(false);
  const unread = notifications.length;
  return (
    <Dropdown isOpen={open} onOpenChange={setOpen}>
      <DropdownTrigger aria-label="Notifications" className="relative inline-flex size-9 shrink-0 items-center justify-center rounded-2lg border border-border-button-default bg-background-primary-default text-foreground-icon-primary shadow-xs transition-colors duration-150 hover:bg-background-primary-hover">
        <RiNotification3Line className="size-5" aria-hidden />
        <Badge color="primary" className="absolute -top-2 -right-2 min-w-5 justify-center px-1">{unread}</Badge>
      </DropdownTrigger>
      <DropdownPopover aria-label="Notifications" placement="bottom end" className="w-[min(360px,calc(100vw-24px))]">
        <div className="flex items-center justify-between px-2 py-1.5">
          <span className="text-body-semibold text-text-primary">Notifications</span>
          <span className="text-caption-1-medium text-text-tertiary">{unread} unread</span>
        </div>
        {notifications.map((item) => (
          <DropdownItem key={item.title} onSelect={() => setOpen(false)} className="items-start py-2.5">
            <span className="mt-1 size-2 shrink-0 rounded-full bg-accent-500" aria-hidden />
            <span className="flex min-w-0 flex-1 flex-col items-start">
              <span className="text-body-medium text-text-primary">{item.title}</span>
              <span className="line-clamp-2 text-left text-body-2-medium text-text-secondary">{item.description}</span>
            </span>
            <span className="text-caption-1-medium whitespace-nowrap text-text-tertiary">{item.time}</span>
          </DropdownItem>
        ))}
      </DropdownPopover>
    </Dropdown>
  );
}

export type HomeDashboardDateRange = "7-days" | "30-days" | "year";
export type HomeDashboardTeam = "all" | "board" | "engineering";

export interface HomeDashboardFiltersValue {
  dateRange: HomeDashboardDateRange;
  team: HomeDashboardTeam;
}

export const DEFAULT_HOME_DASHBOARD_FILTERS: HomeDashboardFiltersValue = {
  dateRange: "30-days",
  team: "all",
};

interface DashboardFiltersProps {
  filters?: HomeDashboardFiltersValue;
  defaultFilters?: HomeDashboardFiltersValue;
  onFiltersChange?: (filters: HomeDashboardFiltersValue) => void;
}

export function DashboardFilters({
  filters,
  defaultFilters = DEFAULT_HOME_DASHBOARD_FILTERS,
  onFiltersChange,
}: DashboardFiltersProps) {
  const [open, setOpen] = useState(false);
  const [uncontrolledFilters, setUncontrolledFilters] = useState(defaultFilters);
  const appliedFilters = filters ?? uncontrolledFilters;
  const [draftFilters, setDraftFilters] = useState(appliedFilters);
  const containerRef = useRef<HTMLDivElement>(null);
  const triggerRef = useRef<HTMLButtonElement>(null);

  const close = (restoreFocus = false) => {
    setOpen(false);
    if (restoreFocus) queueMicrotask(() => triggerRef.current?.focus());
  };

  useEffect(() => {
    if (!open) return;
    const closeOnOutsidePress = (event: MouseEvent) => {
      if (event.target instanceof Node && !containerRef.current?.contains(event.target)) setOpen(false);
    };
    const closeOnEscape = (event: KeyboardEvent) => {
      if (event.key === "Escape" && !event.defaultPrevented) {
        event.preventDefault();
        close(true);
      }
    };
    document.addEventListener("mousedown", closeOnOutsidePress);
    document.addEventListener("keydown", closeOnEscape);
    return () => {
      document.removeEventListener("mousedown", closeOnOutsidePress);
      document.removeEventListener("keydown", closeOnEscape);
    };
  }, [open]);

  return (
    <div ref={containerRef} className="relative">
      <Button
        ref={triggerRef}
        variant="secondary"
        leadingIcon={RiEqualizer2Line}
        aria-expanded={open}
        aria-haspopup="dialog"
        aria-controls="home-dashboard-filters"
        onClick={() => {
          if (!open) setDraftFilters(appliedFilters);
          setOpen((value) => !value);
        }}
      >
        Filters
      </Button>
      {open && (
        <div id="home-dashboard-filters" role="dialog" aria-label="Dashboard filters" className="absolute top-[calc(100%+6px)] right-0 z-30 flex w-[280px] flex-col gap-3 rounded-2xl border border-border-button-default bg-background-primary-default p-3 shadow-dropdown">
          <p className="text-body-semibold text-text-primary">Dashboard filters</p>
          <label className="flex flex-col gap-1 text-body-2-medium text-text-secondary">Date range
            <Select
              aria-label="Dashboard date range"
              selectedKey={draftFilters.dateRange}
              onSelectionChange={(key) => setDraftFilters((current) => ({ ...current, dateRange: String(key) as HomeDashboardDateRange }))}
            >
              <SelectItem id="7-days" textValue="Last 7 days">Last 7 days</SelectItem>
              <SelectItem id="30-days" textValue="Last 30 days">Last 30 days</SelectItem>
              <SelectItem id="year" textValue="This year">This year</SelectItem>
            </Select>
          </label>
          <label className="flex flex-col gap-1 text-body-2-medium text-text-secondary">Team
            <Select
              aria-label="Dashboard team"
              selectedKey={draftFilters.team}
              onSelectionChange={(key) => setDraftFilters((current) => ({ ...current, team: String(key) as HomeDashboardTeam }))}
            >
              <SelectItem id="all" textValue="All teams">All teams</SelectItem>
              <SelectItem id="board" textValue="BoardCN team">BoardCN team</SelectItem>
              <SelectItem id="engineering" textValue="Engineering">Engineering</SelectItem>
            </Select>
          </label>
          <div className="flex gap-2 pt-1">
            <Button size="small" variant="secondary" className="flex-1" onClick={() => setDraftFilters(DEFAULT_HOME_DASHBOARD_FILTERS)}>Reset</Button>
            <Button
              size="small"
              className="flex-1"
              onClick={() => {
                setUncontrolledFilters(draftFilters);
                onFiltersChange?.(draftFilters);
                close(true);
              }}
            >
              Apply
            </Button>
          </div>
        </div>
      )}
    </div>
  );
}

export interface HomeDashboardProps {
  className?: string;
  /** Applied filter value. When supplied, the dashboard filter panel is controlled. */
  filters?: HomeDashboardFiltersValue;
  /** Initial applied value for an uncontrolled dashboard. */
  defaultFilters?: HomeDashboardFiltersValue;
  /** Fires only when the user commits the draft date/team values with Apply. */
  onFiltersChange?: (filters: HomeDashboardFiltersValue) => void;
  stats: Stat[];
  hirePages: readonly (readonly HomeDashboardHire[])[];
  customers: readonly HomeDashboardCustomer[];
  notifications: readonly HomeDashboardNotification[];
  earningsPeriods: EarningsChartPeriods;
  contributionsStats: readonly ContributionsStat[];
  contributionsHeadline: number;
  contributionsDelta: string;
  revenuePeriods: RevenuePeriods;
}

export function HomeDashboard({
  className,
  filters,
  defaultFilters,
  onFiltersChange,
  stats,
  hirePages,
  customers,
  notifications,
  earningsPeriods,
  contributionsStats,
  contributionsHeadline,
  contributionsDelta,
  revenuePeriods,
}: HomeDashboardProps) {
  const [navigationOpen, setNavigationOpen] = useState(false);
  const [ticketOpen, setTicketOpen] = useState(false);

  return (
    <div className={cx("relative flex min-h-screen w-full items-start bg-background-full", className)}>
      <div className="sticky top-3 ml-3 hidden h-[calc(100dvh-24px)] shrink-0 lg:block">
        <DashboardSidebar selected="home" className="h-full" />
      </div>

      <main className="relative z-20 flex min-w-0 flex-1 justify-center overflow-x-hidden bg-background-full p-3 pt-4 will-change-transform sm:pt-6 lg:z-0 lg:overflow-visible">
        <div className="flex w-full max-w-[1300px] flex-col gap-2.5">
          <header className="flex w-full flex-col gap-2">
            <div className="flex items-center gap-2">
              <IconButton icon={RiMenuLine} aria-label="Open navigation" onClick={() => setNavigationOpen(true)} className="lg:hidden" />
              <Breadcrumb>
                <BreadcrumbItem href="#"><Avatar size="xs" initials="B" />BoardCN team</BreadcrumbItem>
                <BreadcrumbItem href="#"><Avatar size="xs" initials="M" />Mertcan</BreadcrumbItem>
                <BreadcrumbItem current>Home</BreadcrumbItem>
              </Breadcrumb>
            </div>
            <div className="flex w-full flex-wrap items-end justify-between gap-2">
              <div className="flex min-w-0 items-center gap-1.5">
                <h1 className="px-1 text-title-2-medium whitespace-nowrap text-text-primary">Welcome Mertcan</h1>
              </div>
              <div className="flex items-center gap-2">
                <NotificationMenu notifications={notifications} />
                <DashboardFilters filters={filters} defaultFilters={defaultFilters} onFiltersChange={onFiltersChange} />
                <Button leadingIcon={RiAddLine} onClick={() => setTicketOpen(true)}>Create ticket</Button>
              </div>
            </div>
          </header>

          <div className="flex w-full flex-col gap-4">
            <div className="flex w-full flex-col items-stretch gap-4 xl:flex-row xl:items-start">
              <RecentHiresCard pages={hirePages} />
              <EarningsChartCard periods={earningsPeriods} />
            </div>
            <div className="flex w-full flex-col items-stretch gap-4 lg:flex-row lg:items-start">
              <RevenueChartCard periods={revenuePeriods} />
              <ContributionsCard
                accent="violet"
                stats={contributionsStats}
                headline={contributionsHeadline}
                delta={contributionsDelta}
              />
            </div>
            <StatCards stats={stats} columns={4} />
            <HomeCustomersTable customers={customers} />
          </div>
        </div>
      </main>

      <MobileNavigation isOpen={navigationOpen} onOpenChange={setNavigationOpen} />
      <TicketDialog isOpen={ticketOpen} onOpenChange={setTicketOpen} />
    </div>
  );
}

export default HomeDashboard;
"use client";

import { useEffect, useRef, useState } from "react";
import { Dialog, Modal, ModalOverlay } from "react-aria-components";
import { RiAddLine, RiCloseLine, RiEqualizer2Line, RiMenuLine, RiNotification3Line } from "@remixicon/react";
import { DashboardSidebar } from "@/components/blocks/dashboard/dashboard-sidebar";
import { StatCards } from "@/components/blocks/dashboard/stat-cards";
import { Avatar } from "@/components/base/avatar/avatar";
import { Badge } from "@/components/base/badges/badge";
import { Breadcrumb, BreadcrumbItem } from "@/components/base/breadcrumb/breadcrumb";
import { Button } from "@/components/base/buttons/button";
import { IconButton } from "@/components/base/buttons/icon-button";
import { Dropdown, DropdownItem, DropdownPopover, DropdownTrigger } from "@/components/base/dropdown/dropdown";
import { InputBase } from "@/components/base/input/input";
import { Select, SelectItem } from "@/components/base/select/select";
import type { Stat } from "@/components/blocks/dashboard/stat-cards";
import { ContributionsCard, type ContributionsStat } from "@/components/charts/contributions-card";
import { EarningsChartCard, type EarningsChartPeriods } from "@/components/charts/earnings-chart-card";
import { cx } from "@/utils/cx";
import { HomeCustomersTable } from "./customers-table";
import { RecentHiresCard } from "./recent-hires-card";
import { RevenueChartCard, type RevenuePeriods } from "./revenue-chart-card";
import type {
  HomeDashboardCustomer,
  HomeDashboardHire,
  HomeDashboardNotification,
} from "./types";

function MobileNavigation({ isOpen, onOpenChange }: { isOpen: boolean; onOpenChange: (open: boolean) => void }) {
  return (
    <ModalOverlay isOpen={isOpen} onOpenChange={onOpenChange} isDismissable className="fixed inset-0 z-50 flex bg-black/40 backdrop-blur-[2px] transition-opacity duration-300 data-[entering]:opacity-0 data-[exiting]:opacity-0 lg:hidden">
      <Modal className="h-full w-[min(320px,calc(100vw-48px))] bg-background-full p-3 shadow-dropdown outline-none transition-transform duration-300 ease-[cubic-bezier(0.32,0.72,0,1)] data-[entering]:-translate-x-full data-[exiting]:-translate-x-full">
        <Dialog aria-label="Navigation" className="h-full outline-none">
          <DashboardSidebar mobile fluid flat onClose={() => onOpenChange(false)} className="h-full" />
        </Dialog>
      </Modal>
    </ModalOverlay>
  );
}

function TicketDialog({ isOpen, onOpenChange }: { isOpen: boolean; onOpenChange: (open: boolean) => void }) {
  return (
    <ModalOverlay isOpen={isOpen} onOpenChange={onOpenChange} isDismissable className="fixed inset-0 z-60 flex items-center justify-center bg-black/50 p-4 backdrop-blur-[2px] transition-opacity duration-200 data-[entering]:opacity-0 data-[exiting]:opacity-0">
      <Modal className="w-full max-w-[430px] rounded-3xl bg-background-full p-5 shadow-dropdown outline-none transition-[opacity,transform] duration-200 data-[entering]:scale-95 data-[entering]:opacity-0 data-[exiting]:scale-95 data-[exiting]:opacity-0">
        <Dialog aria-label="Create ticket" className="flex flex-col gap-5 outline-none">
          <div className="flex items-start justify-between gap-4">
            <div>
              <h2 className="text-title-2-medium text-text-primary">Create ticket</h2>
              <p className="mt-1 text-body-regular text-text-secondary">Start a new request for the BoardCN team.</p>
            </div>
            <IconButton icon={RiCloseLine} size="small" aria-label="Close create ticket" onClick={() => onOpenChange(false)} />
          </div>
          <label className="flex flex-col gap-1.5 text-body-medium text-text-primary">
            Ticket title
            <InputBase autoFocus aria-label="Ticket title" placeholder="What needs attention?" />
          </label>
          <label className="flex flex-col gap-1.5 text-body-medium text-text-primary">
            Priority
            <Select aria-label="Ticket priority" defaultSelectedKey="normal">
              <SelectItem id="low" textValue="Low">Low</SelectItem>
              <SelectItem id="normal" textValue="Normal">Normal</SelectItem>
              <SelectItem id="urgent" textValue="Urgent">Urgent</SelectItem>
            </Select>
          </label>
          <div className="flex justify-end gap-2">
            <Button variant="secondary" onClick={() => onOpenChange(false)}>Cancel</Button>
            <Button onClick={() => onOpenChange(false)}>Create ticket</Button>
          </div>
        </Dialog>
      </Modal>
    </ModalOverlay>
  );
}

function NotificationMenu({ notifications }: { notifications: readonly HomeDashboardNotification[] }) {
  const [open, setOpen] = useState(false);
  const unread = notifications.length;
  return (
    <Dropdown isOpen={open} onOpenChange={setOpen}>
      <DropdownTrigger aria-label="Notifications" className="relative inline-flex size-9 shrink-0 items-center justify-center rounded-2lg border border-border-button-default bg-background-primary-default text-foreground-icon-primary shadow-xs transition-colors duration-150 hover:bg-background-primary-hover">
        <RiNotification3Line className="size-5" aria-hidden />
        <Badge color="primary" className="absolute -top-2 -right-2 min-w-5 justify-center px-1">{unread}</Badge>
      </DropdownTrigger>
      <DropdownPopover aria-label="Notifications" placement="bottom end" className="w-[min(360px,calc(100vw-24px))]">
        <div className="flex items-center justify-between px-2 py-1.5">
          <span className="text-body-semibold text-text-primary">Notifications</span>
          <span className="text-caption-1-medium text-text-tertiary">{unread} unread</span>
        </div>
        {notifications.map((item) => (
          <DropdownItem key={item.title} onSelect={() => setOpen(false)} className="items-start py-2.5">
            <span className="mt-1 size-2 shrink-0 rounded-full bg-accent-500" aria-hidden />
            <span className="flex min-w-0 flex-1 flex-col items-start">
              <span className="text-body-medium text-text-primary">{item.title}</span>
              <span className="line-clamp-2 text-left text-body-2-medium text-text-secondary">{item.description}</span>
            </span>
            <span className="text-caption-1-medium whitespace-nowrap text-text-tertiary">{item.time}</span>
          </DropdownItem>
        ))}
      </DropdownPopover>
    </Dropdown>
  );
}

export type HomeDashboardDateRange = "7-days" | "30-days" | "year";
export type HomeDashboardTeam = "all" | "board" | "engineering";

export interface HomeDashboardFiltersValue {
  dateRange: HomeDashboardDateRange;
  team: HomeDashboardTeam;
}

export const DEFAULT_HOME_DASHBOARD_FILTERS: HomeDashboardFiltersValue = {
  dateRange: "30-days",
  team: "all",
};

interface DashboardFiltersProps {
  filters?: HomeDashboardFiltersValue;
  defaultFilters?: HomeDashboardFiltersValue;
  onFiltersChange?: (filters: HomeDashboardFiltersValue) => void;
}

export function DashboardFilters({
  filters,
  defaultFilters = DEFAULT_HOME_DASHBOARD_FILTERS,
  onFiltersChange,
}: DashboardFiltersProps) {
  const [open, setOpen] = useState(false);
  const [uncontrolledFilters, setUncontrolledFilters] = useState(defaultFilters);
  const appliedFilters = filters ?? uncontrolledFilters;
  const [draftFilters, setDraftFilters] = useState(appliedFilters);
  const containerRef = useRef<HTMLDivElement>(null);
  const triggerRef = useRef<HTMLButtonElement>(null);

  const close = (restoreFocus = false) => {
    setOpen(false);
    if (restoreFocus) queueMicrotask(() => triggerRef.current?.focus());
  };

  useEffect(() => {
    if (!open) return;
    const closeOnOutsidePress = (event: MouseEvent) => {
      if (event.target instanceof Node && !containerRef.current?.contains(event.target)) setOpen(false);
    };
    const closeOnEscape = (event: KeyboardEvent) => {
      if (event.key === "Escape" && !event.defaultPrevented) {
        event.preventDefault();
        close(true);
      }
    };
    document.addEventListener("mousedown", closeOnOutsidePress);
    document.addEventListener("keydown", closeOnEscape);
    return () => {
      document.removeEventListener("mousedown", closeOnOutsidePress);
      document.removeEventListener("keydown", closeOnEscape);
    };
  }, [open]);

  return (
    <div ref={containerRef} className="relative">
      <Button
        ref={triggerRef}
        variant="secondary"
        leadingIcon={RiEqualizer2Line}
        aria-expanded={open}
        aria-haspopup="dialog"
        aria-controls="home-dashboard-filters"
        onClick={() => {
          if (!open) setDraftFilters(appliedFilters);
          setOpen((value) => !value);
        }}
      >
        Filters
      </Button>
      {open && (
        <div id="home-dashboard-filters" role="dialog" aria-label="Dashboard filters" className="absolute top-[calc(100%+6px)] right-0 z-30 flex w-[280px] flex-col gap-3 rounded-2xl border border-border-button-default bg-background-primary-default p-3 shadow-dropdown">
          <p className="text-body-semibold text-text-primary">Dashboard filters</p>
          <label className="flex flex-col gap-1 text-body-2-medium text-text-secondary">Date range
            <Select
              aria-label="Dashboard date range"
              selectedKey={draftFilters.dateRange}
              onSelectionChange={(key) => setDraftFilters((current) => ({ ...current, dateRange: String(key) as HomeDashboardDateRange }))}
            >
              <SelectItem id="7-days" textValue="Last 7 days">Last 7 days</SelectItem>
              <SelectItem id="30-days" textValue="Last 30 days">Last 30 days</SelectItem>
              <SelectItem id="year" textValue="This year">This year</SelectItem>
            </Select>
          </label>
          <label className="flex flex-col gap-1 text-body-2-medium text-text-secondary">Team
            <Select
              aria-label="Dashboard team"
              selectedKey={draftFilters.team}
              onSelectionChange={(key) => setDraftFilters((current) => ({ ...current, team: String(key) as HomeDashboardTeam }))}
            >
              <SelectItem id="all" textValue="All teams">All teams</SelectItem>
              <SelectItem id="board" textValue="BoardCN team">BoardCN team</SelectItem>
              <SelectItem id="engineering" textValue="Engineering">Engineering</SelectItem>
            </Select>
          </label>
          <div className="flex gap-2 pt-1">
            <Button size="small" variant="secondary" className="flex-1" onClick={() => setDraftFilters(DEFAULT_HOME_DASHBOARD_FILTERS)}>Reset</Button>
            <Button
              size="small"
              className="flex-1"
              onClick={() => {
                setUncontrolledFilters(draftFilters);
                onFiltersChange?.(draftFilters);
                close(true);
              }}
            >
              Apply
            </Button>
          </div>
        </div>
      )}
    </div>
  );
}

export interface HomeDashboardProps {
  className?: string;
  /** Applied filter value. When supplied, the dashboard filter panel is controlled. */
  filters?: HomeDashboardFiltersValue;
  /** Initial applied value for an uncontrolled dashboard. */
  defaultFilters?: HomeDashboardFiltersValue;
  /** Fires only when the user commits the draft date/team values with Apply. */
  onFiltersChange?: (filters: HomeDashboardFiltersValue) => void;
  stats: Stat[];
  hirePages: readonly (readonly HomeDashboardHire[])[];
  customers: readonly HomeDashboardCustomer[];
  notifications: readonly HomeDashboardNotification[];
  earningsPeriods: EarningsChartPeriods;
  contributionsStats: readonly ContributionsStat[];
  contributionsHeadline: number;
  contributionsDelta: string;
  revenuePeriods: RevenuePeriods;
}

export function HomeDashboard({
  className,
  filters,
  defaultFilters,
  onFiltersChange,
  stats,
  hirePages,
  customers,
  notifications,
  earningsPeriods,
  contributionsStats,
  contributionsHeadline,
  contributionsDelta,
  revenuePeriods,
}: HomeDashboardProps) {
  const [navigationOpen, setNavigationOpen] = useState(false);
  const [ticketOpen, setTicketOpen] = useState(false);

  return (
    <div className={cx("relative flex min-h-screen w-full items-start bg-background-full", className)}>
      <div className="sticky top-3 ml-3 hidden h-[calc(100dvh-24px)] shrink-0 lg:block">
        <DashboardSidebar selected="home" className="h-full" />
      </div>

      <main className="relative z-20 flex min-w-0 flex-1 justify-center overflow-x-hidden bg-background-full p-3 pt-4 will-change-transform sm:pt-6 lg:z-0 lg:overflow-visible">
        <div className="flex w-full max-w-[1300px] flex-col gap-2.5">
          <header className="flex w-full flex-col gap-2">
            <div className="flex items-center gap-2">
              <IconButton icon={RiMenuLine} aria-label="Open navigation" onClick={() => setNavigationOpen(true)} className="lg:hidden" />
              <Breadcrumb>
                <BreadcrumbItem href="#"><Avatar size="xs" initials="B" />BoardCN team</BreadcrumbItem>
                <BreadcrumbItem href="#"><Avatar size="xs" initials="M" />Mertcan</BreadcrumbItem>
                <BreadcrumbItem current>Home</BreadcrumbItem>
              </Breadcrumb>
            </div>
            <div className="flex w-full flex-wrap items-end justify-between gap-2">
              <div className="flex min-w-0 items-center gap-1.5">
                <h1 className="px-1 text-title-2-medium whitespace-nowrap text-text-primary">Welcome Mertcan</h1>
              </div>
              <div className="flex items-center gap-2">
                <NotificationMenu notifications={notifications} />
                <DashboardFilters filters={filters} defaultFilters={defaultFilters} onFiltersChange={onFiltersChange} />
                <Button leadingIcon={RiAddLine} onClick={() => setTicketOpen(true)}>Create ticket</Button>
              </div>
            </div>
          </header>

          <div className="flex w-full flex-col gap-4">
            <div className="flex w-full flex-col items-stretch gap-4 xl:flex-row xl:items-start">
              <RecentHiresCard pages={hirePages} />
              <EarningsChartCard periods={earningsPeriods} />
            </div>
            <div className="flex w-full flex-col items-stretch gap-4 lg:flex-row lg:items-start">
              <RevenueChartCard periods={revenuePeriods} />
              <ContributionsCard
                accent="violet"
                stats={contributionsStats}
                headline={contributionsHeadline}
                delta={contributionsDelta}
              />
            </div>
            <StatCards stats={stats} columns={4} />
            <HomeCustomersTable customers={customers} />
          </div>
        </div>
      </main>

      <MobileNavigation isOpen={navigationOpen} onOpenChange={setNavigationOpen} />
      <TicketDialog isOpen={ticketOpen} onOpenChange={setTicketOpen} />
    </div>
  );
}

export default HomeDashboard;
View this template on GitHub