Marketing Dashboard

Complete marketing dashboard template.

Installation

npx shadcn@latest add @boardcn/marketing-dashboard
npx shadcn@latest add @boardcn/marketing-dashboard

npm packages

  • @remixicon/react
  • react-aria-components

What it composes

22 BoardCN components, installed automatically.

Source

The 4 files the CLI copies into your project.

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

import { useRef, useState } from "react";
import {
  RiAddLine,
  RiFilter3Line,
  RiFlashlightLine,
  RiMenuLine,
  RiNotification3Line,
} from "@remixicon/react";
import { Dialog, Heading, Modal, ModalOverlay } from "react-aria-components";
import { NotificationCenter, type NotificationCenterItem } from "@/components/blocks/notification-center/notification-center";
import { DashboardSidebar } from "@/components/blocks/dashboard/dashboard-sidebar";
import { StatCards, type Stat } from "@/components/blocks/dashboard/stat-cards";
import { Avatar } from "@/components/base/avatar/avatar";
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 { Input } from "@/components/base/input/input";
import { AreaChartCard, type AreaChartRange } from "@/components/charts/area-chart-card";
import { BarListCard, type BarListTab } from "@/components/charts/bar-list-card";
import { ComboChartCard, type ComboChartRange } from "@/components/charts/combo-chart-card";
import { FunnelChartCard, type FunnelChartRange } from "@/components/charts/funnel-chart-card";
import { cx } from "@/utils/cx";
import { MarketingCampaignTable, type MarketingCampaignActions } from "./marketing-campaign-table";
import type { MarketingCampaign, SpendChannel } from "./marketing-types";

function compact(value: number) {
  if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(2).replace(/0+$/, "").replace(/\.$/, "")}M`;
  if (value >= 1_000) return `${(value / 1_000).toFixed(1).replace(/\.0$/, "")}K`;
  return value.toLocaleString("en-US");
}

function moneyCompact(value: number) {
  return value >= 1_000 ? `$${(value / 1_000).toFixed(1).replace(/\.0$/, "")}K` : `$${value}`;
}

function SpendByChannelCard({ channels }: { channels: readonly SpendChannel[] }) {
  const [range, setRange] = useState("30d");
  const [rangeOpen, setRangeOpen] = useState(false);
  const [active, setActive] = useState(0);
  const rangeLabel = range === "30d" ? "Last 30 days" : range === "7d" ? "Last 7 days" : "Last 90 days";
  const activeChannel = channels[active];
  const total = channels.reduce((sum, item) => sum + item.value, 0);
  const share = Math.round(activeChannel.value / total * 100);

  return (
    <section className="flex h-[329px] w-full min-w-0 flex-col gap-4 rounded-2xl bg-background-secondary-default px-4 pt-4 pb-3" aria-label="Spend by channel chart">
      <div className="flex w-full items-start justify-between gap-3">
        <div className="flex min-w-0 flex-1 flex-col gap-0.5">
          <p className="truncate text-body-medium text-text-secondary">Spend by channel</p>
          <div className="flex items-center gap-2">
            <p className="text-title-1-medium text-text-primary tabular-nums">$24,880</p>
            <span className="rounded-md bg-status-lime-background px-1.5 py-0.5 text-body-medium text-status-lime-text">+8.4%</span>
          </div>
        </div>
        <Dropdown isOpen={rangeOpen} onOpenChange={setRangeOpen}>
          <DropdownTrigger className="flex h-8 shrink-0 items-center gap-1.5 rounded-2lg border border-border-button-default bg-background-primary-default px-2 text-body-medium text-text-primary shadow-xs">
            {rangeLabel}
          </DropdownTrigger>
          <DropdownPopover aria-label="Spend period" placement="bottom end" className="w-[176px] p-2">
            {(["7d", "30d", "90d"] as const).map((id) => (
              <DropdownItem
                key={id}
                selected={range === id}
                onSelect={() => { setRange(id); setRangeOpen(false); }}
                className="w-full rounded-2lg px-2 py-1.5 text-left text-body-medium text-text-primary hover:bg-background-secondary-hover"
              >
                {id === "7d" ? "Last 7 days" : id === "30d" ? "Last 30 days" : "Last 90 days"}
              </DropdownItem>
            ))}
          </DropdownPopover>
        </Dropdown>
      </div>

      <div className="relative min-h-[132px] w-full flex-1" role="application" aria-label={`Spend by channel. ${channels.map((item) => `${item.label} ${moneyCompact(item.value)}`).join(", ")}`}>
        <svg viewBox="0 0 260 145" className="size-full overflow-visible" aria-hidden>
          {channels.map((item, index) => {
            const radii = [102, 84, 66, 48];
            const lengths = [320.4, 263.9, 207.3, 150.8];
            const channelShare = item.value / total;
            return (
              <g key={item.label} onMouseEnter={() => setActive(index)} onFocus={() => setActive(index)} tabIndex={0} role="button" aria-label={`${item.label}: ${moneyCompact(item.value)}`}>
                <path d={`M ${130 - radii[index]} 128 A ${radii[index]} ${radii[index]} 0 0 1 ${130 + radii[index]} 128`} fill="none" stroke="var(--color-chart-track)" strokeWidth="10" strokeLinecap="round" />
                <path
                  d={`M ${130 - radii[index]} 128 A ${radii[index]} ${radii[index]} 0 0 1 ${130 + radii[index]} 128`}
                  fill="none"
                  stroke={item.color}
                  strokeWidth="10"
                  strokeLinecap="round"
                  pathLength={lengths[index]}
                  strokeDasharray={`${lengths[index] * channelShare} ${lengths[index]}`}
                  className={cx("transition-opacity duration-200 ease-out motion-reduce:transition-none", active !== index && "opacity-45")}
                />
              </g>
            );
          })}
        </svg>
        <div className="pointer-events-none absolute inset-x-0 bottom-0 flex flex-col items-center">
          <span className="text-display-4-medium text-text-primary tabular-nums">{share}%</span>
          <span className="-mt-1 max-w-[120px] truncate text-caption-1-medium text-text-tertiary">{activeChannel.label}</span>
        </div>
      </div>
      <div className="flex flex-wrap items-center justify-center gap-x-4 gap-y-1 pb-1">
        {channels.map((item, index) => (
          <button key={item.label} type="button" onClick={() => setActive(index)} onMouseEnter={() => setActive(index)} className={cx("flex items-center gap-1.5 transition-opacity duration-200", active !== index && "opacity-55")}>
            <span className="size-3 rounded-[4px]" style={{ backgroundColor: item.color }} aria-hidden />
            <span className="text-body-regular text-text-secondary">{item.label}</span>
            <span className="text-body-medium text-text-primary tabular-nums">{moneyCompact(item.value)}</span>
          </button>
        ))}
      </div>
    </section>
  );
}

function NewCampaignDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) {
  const [created, setCreated] = useState(false);
  return (
    <ModalOverlay
      isOpen={open}
      onOpenChange={onOpenChange}
      isDismissable
      className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4 transition-opacity duration-200 ease-out data-[entering]:opacity-0 data-[exiting]:opacity-0 motion-reduce:transition-none"
    >
      <Modal className="w-full max-w-[460px] rounded-3xl border border-border-button-default bg-background-primary-default p-5 shadow-dropdown outline-none transition-[transform,opacity] duration-200 ease-out data-[entering]:scale-95 data-[entering]:opacity-0 data-[exiting]:scale-95 data-[exiting]:opacity-0 motion-reduce:transition-none">
        <Dialog className="flex flex-col gap-4 outline-none">
          {({ close }) => (
            <>
              <div className="flex items-start justify-between gap-3">
                <div>
                  <Heading slot="title" className="text-title-2-medium text-text-primary">New campaign</Heading>
                  <p className="mt-0.5 text-body-regular text-text-secondary">Create a marketing campaign draft.</p>
                </div>
                <IconButton icon={RiAddLine} aria-label="Close new campaign" className="rotate-45" onClick={close} />
              </div>
              <form
                className="flex flex-col gap-3"
                onSubmit={(event) => {
                  event.preventDefault();
                  setCreated(true);
                  window.setTimeout(() => { setCreated(false); close(); }, 600);
                }}
              >
                <Input label="Campaign name" placeholder="Campaign name" isRequired />
                <Input label="Budget" placeholder="$0" inputMode="numeric" />
                <div className="mt-1 flex items-center justify-end gap-2">
                  <Button variant="secondary" onClick={close}>Cancel</Button>
                  <Button type="submit" leadingIcon={created ? RiFlashlightLine : RiAddLine}>{created ? "Created" : "Create draft"}</Button>
                </div>
              </form>
            </>
          )}
        </Dialog>
      </Modal>
    </ModalOverlay>
  );
}

export interface MarketingDashboardProps extends MarketingCampaignActions {
  className?: string;
  onFilters?: () => void;
  stats: Stat[];
  notifications: readonly NotificationCenterItem[];
  campaigns: readonly MarketingCampaign[];
  acquisitionRanges: readonly FunnelChartRange[];
  sessionsTabs: readonly BarListTab[];
  roasRanges: readonly ComboChartRange[];
  visitorRanges: readonly AreaChartRange[];
  spendChannels: readonly SpendChannel[];
}

export function MarketingDashboard({
  className,
  onFilters,
  onDeleteCampaign,
  onEditCampaign,
  onViewCampaignReport,
  onMoreCampaignActions,
  stats,
  notifications,
  campaigns,
  acquisitionRanges,
  sessionsTabs,
  roasRanges,
  visitorRanges,
  spendChannels,
}: MarketingDashboardProps) {
  const [mobileNavOpen, setMobileNavOpen] = useState(false);
  const [newCampaignOpen, setNewCampaignOpen] = useState(false);
  const campaignsRef = useRef<HTMLDivElement>(null);

  return (
    <div className={cx("relative flex w-full bg-background-full max-lg:h-dvh max-lg:overflow-hidden lg:min-h-screen", className)}>
      <div className="sticky top-3 z-10 hidden h-[calc(100vh-24px)] shrink-0 py-0 pl-3 lg:block">
        <DashboardSidebar selected="marketing" />
      </div>

      <ModalOverlay
        isOpen={mobileNavOpen}
        onOpenChange={setMobileNavOpen}
        isDismissable
        className="fixed inset-0 z-40 bg-black/40 transition-opacity duration-300 ease-in-out data-[entering]:opacity-0 data-[exiting]:opacity-0 motion-reduce:transition-none lg:hidden"
      >
        <Modal className="fixed inset-y-0 left-0 w-[272px] py-3 pl-[6px] outline-none transition-transform duration-300 ease-in-out data-[entering]:-translate-x-full data-[exiting]:-translate-x-full motion-reduce:transition-none">
          <Dialog aria-label="Navigation" className="h-full outline-none">
            <DashboardSidebar mobile flat selected="marketing" onClose={() => setMobileNavOpen(false)} className="h-full" />
          </Dialog>
        </Modal>
      </ModalOverlay>

      <main className="relative z-20 flex min-w-0 flex-1 justify-center overflow-x-hidden overflow-y-auto bg-background-full p-3 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">
            <Breadcrumb>
              <BreadcrumbItem href="/templates/dashboard">
                <Avatar size="xs" initials="B" color="blue" /> BoardCN team
              </BreadcrumbItem>
              <BreadcrumbItem href="/templates/dashboard">
                <Avatar size="xs" initials="M" /> Mertcan
              </BreadcrumbItem>
              <BreadcrumbItem current>Marketing</BreadcrumbItem>
            </Breadcrumb>
            <div className="flex w-full flex-wrap items-end justify-between gap-2">
              <div className="flex min-w-0 items-center gap-1.5">
                <IconButton icon={RiMenuLine} aria-label="Open navigation" onClick={() => setMobileNavOpen(true)} className="lg:hidden" />
                <h1 className="px-1 text-title-2-medium whitespace-nowrap text-text-primary">Marketing</h1>
              </div>
              <div className="flex flex-wrap items-center justify-end gap-2.5">
                <Dropdown>
                  <span className="group relative inline-flex">
                    <DropdownTrigger
                      aria-label="Notifications"
                      className="relative inline-flex size-9 items-center justify-center rounded-2lg border border-border-button-default bg-background-primary-default text-foreground-icon-primary shadow-xs hover:bg-background-primary-hover"
                    >
                      <RiNotification3Line className="size-5" aria-hidden />
                    </DropdownTrigger>
                    <span className="pointer-events-none absolute top-0.5 left-[18px] flex size-4 items-center justify-center rounded-full border-[1.5px] border-background-primary-default bg-red-600">
                      <span className="w-4 text-center text-[10px] leading-4 font-bold text-white">5</span>
                    </span>
                  </span>
                  <DropdownPopover aria-label="Notifications" placement="bottom end" className="w-[min(430px,calc(100vw-24px))] border-0 bg-transparent p-0 shadow-none">
                    <NotificationCenter notifications={[...notifications]} />
                  </DropdownPopover>
                </Dropdown>
                <Button variant="secondary" leadingIcon={RiFilter3Line} onClick={() => {
                  onFilters?.();
                  campaignsRef.current?.scrollIntoView?.({ behavior: "smooth", block: "start" });
                }}>Filters</Button>
                <Button leadingIcon={RiAddLine} onClick={() => setNewCampaignOpen(true)}>New campaign</Button>
              </div>
            </div>
          </header>

          <div className="flex w-full flex-col gap-4">
            <StatCards stats={stats} columns={4} className="max-sm:grid-cols-1" />

            <div className="grid w-full grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
              <FunnelChartCard
                title="Acquisition funnel"
                shape="sharp"
                stages={acquisitionRanges[0].stages}
                ranges={[...acquisitionRanges]}
                defaultRange="30d"
                format={compact}
                className="pb-3"
              />
              <SpendByChannelCard channels={spendChannels} />
              <BarListCard
                tabs={[...sessionsTabs]}
                defaultTab="channels"
                metricLabel="Sessions"
                metric="value"
                format={(value) => `${value}%`}
                limit={5}
                className="md:col-span-2 xl:col-span-1"
              />
            </div>

            <div className="grid w-full grid-cols-1 gap-4 lg:grid-cols-2">
              <ComboChartCard
                title="Ad spend"
                data={roasRanges[0].data}
                ranges={[...roasRanges]}
                defaultRange="year"
                bar={{ key: "spend", label: "Ad spend", format: moneyCompact }}
                line={{ key: "roas", label: "ROAS", format: (value) => `${value.toFixed(1)}x` }}
                tiles
              />
              <AreaChartCard
                title="Visitors"
                data={visitorRanges[0].data}
                ranges={[...visitorRanges]}
                defaultRange="year"
                series={[
                  { key: "organic", label: "Organic" },
                  { key: "paid", label: "Paid" },
                  { key: "social", label: "Social" },
                ]}
                variant="stacked"
                shape="curved"
                tiles
                valueFormatter={(value) => value.toLocaleString("en-US")}
              />
            </div>

            <div ref={campaignsRef} className="scroll-mt-3">
              <MarketingCampaignTable
                campaigns={campaigns}
                onDeleteCampaign={onDeleteCampaign}
                onEditCampaign={onEditCampaign}
                onViewCampaignReport={onViewCampaignReport}
                onMoreCampaignActions={onMoreCampaignActions}
              />
            </div>
          </div>
        </div>
      </main>

      <NewCampaignDialog open={newCampaignOpen} onOpenChange={setNewCampaignOpen} />
    </div>
  );
}

export default MarketingDashboard;
"use client";

import { useRef, useState } from "react";
import {
  RiAddLine,
  RiFilter3Line,
  RiFlashlightLine,
  RiMenuLine,
  RiNotification3Line,
} from "@remixicon/react";
import { Dialog, Heading, Modal, ModalOverlay } from "react-aria-components";
import { NotificationCenter, type NotificationCenterItem } from "@/components/blocks/notification-center/notification-center";
import { DashboardSidebar } from "@/components/blocks/dashboard/dashboard-sidebar";
import { StatCards, type Stat } from "@/components/blocks/dashboard/stat-cards";
import { Avatar } from "@/components/base/avatar/avatar";
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 { Input } from "@/components/base/input/input";
import { AreaChartCard, type AreaChartRange } from "@/components/charts/area-chart-card";
import { BarListCard, type BarListTab } from "@/components/charts/bar-list-card";
import { ComboChartCard, type ComboChartRange } from "@/components/charts/combo-chart-card";
import { FunnelChartCard, type FunnelChartRange } from "@/components/charts/funnel-chart-card";
import { cx } from "@/utils/cx";
import { MarketingCampaignTable, type MarketingCampaignActions } from "./marketing-campaign-table";
import type { MarketingCampaign, SpendChannel } from "./marketing-types";

function compact(value: number) {
  if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(2).replace(/0+$/, "").replace(/\.$/, "")}M`;
  if (value >= 1_000) return `${(value / 1_000).toFixed(1).replace(/\.0$/, "")}K`;
  return value.toLocaleString("en-US");
}

function moneyCompact(value: number) {
  return value >= 1_000 ? `$${(value / 1_000).toFixed(1).replace(/\.0$/, "")}K` : `$${value}`;
}

function SpendByChannelCard({ channels }: { channels: readonly SpendChannel[] }) {
  const [range, setRange] = useState("30d");
  const [rangeOpen, setRangeOpen] = useState(false);
  const [active, setActive] = useState(0);
  const rangeLabel = range === "30d" ? "Last 30 days" : range === "7d" ? "Last 7 days" : "Last 90 days";
  const activeChannel = channels[active];
  const total = channels.reduce((sum, item) => sum + item.value, 0);
  const share = Math.round(activeChannel.value / total * 100);

  return (
    <section className="flex h-[329px] w-full min-w-0 flex-col gap-4 rounded-2xl bg-background-secondary-default px-4 pt-4 pb-3" aria-label="Spend by channel chart">
      <div className="flex w-full items-start justify-between gap-3">
        <div className="flex min-w-0 flex-1 flex-col gap-0.5">
          <p className="truncate text-body-medium text-text-secondary">Spend by channel</p>
          <div className="flex items-center gap-2">
            <p className="text-title-1-medium text-text-primary tabular-nums">$24,880</p>
            <span className="rounded-md bg-status-lime-background px-1.5 py-0.5 text-body-medium text-status-lime-text">+8.4%</span>
          </div>
        </div>
        <Dropdown isOpen={rangeOpen} onOpenChange={setRangeOpen}>
          <DropdownTrigger className="flex h-8 shrink-0 items-center gap-1.5 rounded-2lg border border-border-button-default bg-background-primary-default px-2 text-body-medium text-text-primary shadow-xs">
            {rangeLabel}
          </DropdownTrigger>
          <DropdownPopover aria-label="Spend period" placement="bottom end" className="w-[176px] p-2">
            {(["7d", "30d", "90d"] as const).map((id) => (
              <DropdownItem
                key={id}
                selected={range === id}
                onSelect={() => { setRange(id); setRangeOpen(false); }}
                className="w-full rounded-2lg px-2 py-1.5 text-left text-body-medium text-text-primary hover:bg-background-secondary-hover"
              >
                {id === "7d" ? "Last 7 days" : id === "30d" ? "Last 30 days" : "Last 90 days"}
              </DropdownItem>
            ))}
          </DropdownPopover>
        </Dropdown>
      </div>

      <div className="relative min-h-[132px] w-full flex-1" role="application" aria-label={`Spend by channel. ${channels.map((item) => `${item.label} ${moneyCompact(item.value)}`).join(", ")}`}>
        <svg viewBox="0 0 260 145" className="size-full overflow-visible" aria-hidden>
          {channels.map((item, index) => {
            const radii = [102, 84, 66, 48];
            const lengths = [320.4, 263.9, 207.3, 150.8];
            const channelShare = item.value / total;
            return (
              <g key={item.label} onMouseEnter={() => setActive(index)} onFocus={() => setActive(index)} tabIndex={0} role="button" aria-label={`${item.label}: ${moneyCompact(item.value)}`}>
                <path d={`M ${130 - radii[index]} 128 A ${radii[index]} ${radii[index]} 0 0 1 ${130 + radii[index]} 128`} fill="none" stroke="var(--color-chart-track)" strokeWidth="10" strokeLinecap="round" />
                <path
                  d={`M ${130 - radii[index]} 128 A ${radii[index]} ${radii[index]} 0 0 1 ${130 + radii[index]} 128`}
                  fill="none"
                  stroke={item.color}
                  strokeWidth="10"
                  strokeLinecap="round"
                  pathLength={lengths[index]}
                  strokeDasharray={`${lengths[index] * channelShare} ${lengths[index]}`}
                  className={cx("transition-opacity duration-200 ease-out motion-reduce:transition-none", active !== index && "opacity-45")}
                />
              </g>
            );
          })}
        </svg>
        <div className="pointer-events-none absolute inset-x-0 bottom-0 flex flex-col items-center">
          <span className="text-display-4-medium text-text-primary tabular-nums">{share}%</span>
          <span className="-mt-1 max-w-[120px] truncate text-caption-1-medium text-text-tertiary">{activeChannel.label}</span>
        </div>
      </div>
      <div className="flex flex-wrap items-center justify-center gap-x-4 gap-y-1 pb-1">
        {channels.map((item, index) => (
          <button key={item.label} type="button" onClick={() => setActive(index)} onMouseEnter={() => setActive(index)} className={cx("flex items-center gap-1.5 transition-opacity duration-200", active !== index && "opacity-55")}>
            <span className="size-3 rounded-[4px]" style={{ backgroundColor: item.color }} aria-hidden />
            <span className="text-body-regular text-text-secondary">{item.label}</span>
            <span className="text-body-medium text-text-primary tabular-nums">{moneyCompact(item.value)}</span>
          </button>
        ))}
      </div>
    </section>
  );
}

function NewCampaignDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) {
  const [created, setCreated] = useState(false);
  return (
    <ModalOverlay
      isOpen={open}
      onOpenChange={onOpenChange}
      isDismissable
      className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4 transition-opacity duration-200 ease-out data-[entering]:opacity-0 data-[exiting]:opacity-0 motion-reduce:transition-none"
    >
      <Modal className="w-full max-w-[460px] rounded-3xl border border-border-button-default bg-background-primary-default p-5 shadow-dropdown outline-none transition-[transform,opacity] duration-200 ease-out data-[entering]:scale-95 data-[entering]:opacity-0 data-[exiting]:scale-95 data-[exiting]:opacity-0 motion-reduce:transition-none">
        <Dialog className="flex flex-col gap-4 outline-none">
          {({ close }) => (
            <>
              <div className="flex items-start justify-between gap-3">
                <div>
                  <Heading slot="title" className="text-title-2-medium text-text-primary">New campaign</Heading>
                  <p className="mt-0.5 text-body-regular text-text-secondary">Create a marketing campaign draft.</p>
                </div>
                <IconButton icon={RiAddLine} aria-label="Close new campaign" className="rotate-45" onClick={close} />
              </div>
              <form
                className="flex flex-col gap-3"
                onSubmit={(event) => {
                  event.preventDefault();
                  setCreated(true);
                  window.setTimeout(() => { setCreated(false); close(); }, 600);
                }}
              >
                <Input label="Campaign name" placeholder="Campaign name" isRequired />
                <Input label="Budget" placeholder="$0" inputMode="numeric" />
                <div className="mt-1 flex items-center justify-end gap-2">
                  <Button variant="secondary" onClick={close}>Cancel</Button>
                  <Button type="submit" leadingIcon={created ? RiFlashlightLine : RiAddLine}>{created ? "Created" : "Create draft"}</Button>
                </div>
              </form>
            </>
          )}
        </Dialog>
      </Modal>
    </ModalOverlay>
  );
}

export interface MarketingDashboardProps extends MarketingCampaignActions {
  className?: string;
  onFilters?: () => void;
  stats: Stat[];
  notifications: readonly NotificationCenterItem[];
  campaigns: readonly MarketingCampaign[];
  acquisitionRanges: readonly FunnelChartRange[];
  sessionsTabs: readonly BarListTab[];
  roasRanges: readonly ComboChartRange[];
  visitorRanges: readonly AreaChartRange[];
  spendChannels: readonly SpendChannel[];
}

export function MarketingDashboard({
  className,
  onFilters,
  onDeleteCampaign,
  onEditCampaign,
  onViewCampaignReport,
  onMoreCampaignActions,
  stats,
  notifications,
  campaigns,
  acquisitionRanges,
  sessionsTabs,
  roasRanges,
  visitorRanges,
  spendChannels,
}: MarketingDashboardProps) {
  const [mobileNavOpen, setMobileNavOpen] = useState(false);
  const [newCampaignOpen, setNewCampaignOpen] = useState(false);
  const campaignsRef = useRef<HTMLDivElement>(null);

  return (
    <div className={cx("relative flex w-full bg-background-full max-lg:h-dvh max-lg:overflow-hidden lg:min-h-screen", className)}>
      <div className="sticky top-3 z-10 hidden h-[calc(100vh-24px)] shrink-0 py-0 pl-3 lg:block">
        <DashboardSidebar selected="marketing" />
      </div>

      <ModalOverlay
        isOpen={mobileNavOpen}
        onOpenChange={setMobileNavOpen}
        isDismissable
        className="fixed inset-0 z-40 bg-black/40 transition-opacity duration-300 ease-in-out data-[entering]:opacity-0 data-[exiting]:opacity-0 motion-reduce:transition-none lg:hidden"
      >
        <Modal className="fixed inset-y-0 left-0 w-[272px] py-3 pl-[6px] outline-none transition-transform duration-300 ease-in-out data-[entering]:-translate-x-full data-[exiting]:-translate-x-full motion-reduce:transition-none">
          <Dialog aria-label="Navigation" className="h-full outline-none">
            <DashboardSidebar mobile flat selected="marketing" onClose={() => setMobileNavOpen(false)} className="h-full" />
          </Dialog>
        </Modal>
      </ModalOverlay>

      <main className="relative z-20 flex min-w-0 flex-1 justify-center overflow-x-hidden overflow-y-auto bg-background-full p-3 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">
            <Breadcrumb>
              <BreadcrumbItem href="/templates/dashboard">
                <Avatar size="xs" initials="B" color="blue" /> BoardCN team
              </BreadcrumbItem>
              <BreadcrumbItem href="/templates/dashboard">
                <Avatar size="xs" initials="M" /> Mertcan
              </BreadcrumbItem>
              <BreadcrumbItem current>Marketing</BreadcrumbItem>
            </Breadcrumb>
            <div className="flex w-full flex-wrap items-end justify-between gap-2">
              <div className="flex min-w-0 items-center gap-1.5">
                <IconButton icon={RiMenuLine} aria-label="Open navigation" onClick={() => setMobileNavOpen(true)} className="lg:hidden" />
                <h1 className="px-1 text-title-2-medium whitespace-nowrap text-text-primary">Marketing</h1>
              </div>
              <div className="flex flex-wrap items-center justify-end gap-2.5">
                <Dropdown>
                  <span className="group relative inline-flex">
                    <DropdownTrigger
                      aria-label="Notifications"
                      className="relative inline-flex size-9 items-center justify-center rounded-2lg border border-border-button-default bg-background-primary-default text-foreground-icon-primary shadow-xs hover:bg-background-primary-hover"
                    >
                      <RiNotification3Line className="size-5" aria-hidden />
                    </DropdownTrigger>
                    <span className="pointer-events-none absolute top-0.5 left-[18px] flex size-4 items-center justify-center rounded-full border-[1.5px] border-background-primary-default bg-red-600">
                      <span className="w-4 text-center text-[10px] leading-4 font-bold text-white">5</span>
                    </span>
                  </span>
                  <DropdownPopover aria-label="Notifications" placement="bottom end" className="w-[min(430px,calc(100vw-24px))] border-0 bg-transparent p-0 shadow-none">
                    <NotificationCenter notifications={[...notifications]} />
                  </DropdownPopover>
                </Dropdown>
                <Button variant="secondary" leadingIcon={RiFilter3Line} onClick={() => {
                  onFilters?.();
                  campaignsRef.current?.scrollIntoView?.({ behavior: "smooth", block: "start" });
                }}>Filters</Button>
                <Button leadingIcon={RiAddLine} onClick={() => setNewCampaignOpen(true)}>New campaign</Button>
              </div>
            </div>
          </header>

          <div className="flex w-full flex-col gap-4">
            <StatCards stats={stats} columns={4} className="max-sm:grid-cols-1" />

            <div className="grid w-full grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
              <FunnelChartCard
                title="Acquisition funnel"
                shape="sharp"
                stages={acquisitionRanges[0].stages}
                ranges={[...acquisitionRanges]}
                defaultRange="30d"
                format={compact}
                className="pb-3"
              />
              <SpendByChannelCard channels={spendChannels} />
              <BarListCard
                tabs={[...sessionsTabs]}
                defaultTab="channels"
                metricLabel="Sessions"
                metric="value"
                format={(value) => `${value}%`}
                limit={5}
                className="md:col-span-2 xl:col-span-1"
              />
            </div>

            <div className="grid w-full grid-cols-1 gap-4 lg:grid-cols-2">
              <ComboChartCard
                title="Ad spend"
                data={roasRanges[0].data}
                ranges={[...roasRanges]}
                defaultRange="year"
                bar={{ key: "spend", label: "Ad spend", format: moneyCompact }}
                line={{ key: "roas", label: "ROAS", format: (value) => `${value.toFixed(1)}x` }}
                tiles
              />
              <AreaChartCard
                title="Visitors"
                data={visitorRanges[0].data}
                ranges={[...visitorRanges]}
                defaultRange="year"
                series={[
                  { key: "organic", label: "Organic" },
                  { key: "paid", label: "Paid" },
                  { key: "social", label: "Social" },
                ]}
                variant="stacked"
                shape="curved"
                tiles
                valueFormatter={(value) => value.toLocaleString("en-US")}
              />
            </div>

            <div ref={campaignsRef} className="scroll-mt-3">
              <MarketingCampaignTable
                campaigns={campaigns}
                onDeleteCampaign={onDeleteCampaign}
                onEditCampaign={onEditCampaign}
                onViewCampaignReport={onViewCampaignReport}
                onMoreCampaignActions={onMoreCampaignActions}
              />
            </div>
          </div>
        </div>
      </main>

      <NewCampaignDialog open={newCampaignOpen} onOpenChange={setNewCampaignOpen} />
    </div>
  );
}

export default MarketingDashboard;
View this template on GitHub