AI Profile

Complete AI profile template.

Installation

npx shadcn@latest add @boardcn/ai-profile
npx shadcn@latest add @boardcn/ai-profile

npm packages

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

BoardCN dependencies

The CLI installs these for you — you do not need to add them yourself.

What it composes

12 BoardCN components, installed automatically.

Source

The 6 files the CLI copies into your project.

components/templates/ai-profile/ai-profile.tsx
"use client";

import { useState } from "react";
import {
  RiCheckLine,
  RiEditLine,
  RiFilter3Line,
  RiMenuLine,
  RiNotification3Line,
} from "@remixicon/react";
import { Dialog, Modal, ModalOverlay } from "react-aria-components";
import { DashboardSidebar } from "@/components/blocks/dashboard/dashboard-sidebar";
import { NotificationCenter } from "@/components/blocks/notification-center/notification-center";
import type { NotificationCenterItem } from "@/components/blocks/notification-center/notification-center";
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 { cx } from "@/utils/cx";
import { AgentsChartCard } from "./agents-chart-card";
import { ProfileHeroCard, type ProfileHeroCardProps } from "./profile-hero-card";
import { TokensChartCard } from "./tokens-chart-card";
import type { AgentMonth, TokenDatum } from "./types";

export interface AiProfileProps extends ProfileHeroCardProps {
  defaultFilter?: ProfileFilter;
  onFilterChange?: (filter: ProfileFilter) => void;
  className?: string;
  agentMonths: readonly AgentMonth[];
  tokenSeries: readonly TokenDatum[];
  notifications: readonly NotificationCenterItem[];
}

export type ProfileFilter = "all" | "agents" | "tokens";

const FILTERS: readonly { id: ProfileFilter; label: string }[] = [
  { id: "all", label: "All activity" },
  { id: "agents", label: "Agents only" },
  { id: "tokens", label: "Tokens only" },
];

function ProfileNotifications({ notifications }: { notifications: readonly NotificationCenterItem[] }) {
  return (
    <Dropdown>
      <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 />
        <span className="absolute -top-1 -right-1 flex min-w-4 items-center justify-center rounded-full bg-button-danger px-1 text-[10px] leading-4 font-semibold text-white">{notifications.length}</span>
      </DropdownTrigger>
      <DropdownPopover aria-label="Notifications" placement="bottom end" className="w-[min(420px,calc(100vw-24px))] p-0">
        <NotificationCenter notifications={[...notifications]} className="max-h-[520px] overflow-y-auto" />
      </DropdownPopover>
    </Dropdown>
  );
}

function ProfileFilters({ value, onChange }: { value: ProfileFilter; onChange: (filter: ProfileFilter) => void }) {
  return (
    <Dropdown>
      <DropdownTrigger
        aria-label="Filters"
        className="inline-flex h-9 shrink-0 items-center justify-center gap-1.5 rounded-2lg border border-border-button-default bg-background-primary-default px-3 text-body-medium text-text-primary shadow-xs transition-colors duration-150 hover:bg-background-primary-hover"
      >
        <RiFilter3Line className="size-4 text-foreground-icon-secondary" aria-hidden />
        Filters
      </DropdownTrigger>
      <DropdownPopover aria-label="Profile filters" placement="bottom end" className="w-48">
        {FILTERS.map((filter) => (
          <DropdownItem key={filter.id} onSelect={() => onChange(filter.id)}>
            <span className="flex min-w-0 flex-1 items-center justify-between gap-2">
              <span>{filter.label}</span>
              {value === filter.id && <RiCheckLine className="size-4 shrink-0" aria-hidden />}
            </span>
          </DropdownItem>
        ))}
      </DropdownPopover>
    </Dropdown>
  );
}

export function AiProfile({
  className,
  defaultFilter = "all",
  onFilterChange,
  onEdit,
  agentMonths,
  tokenSeries,
  notifications,
  ...profileProps
}: AiProfileProps) {
  const [navigationOpen, setNavigationOpen] = useState(false);
  const [filter, setFilter] = useState<ProfileFilter>(defaultFilter);

  const changeFilter = (next: ProfileFilter) => {
    setFilter(next);
    onFilterChange?.(next);
  };

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

      <main
        className={cx(
          "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:p-6",
          "transition-[transform,border-radius] duration-300 ease-in-out lg:z-0 lg:!transform-none lg:!rounded-none lg:overflow-visible",
          navigationOpen && "translate-x-[272px] rounded-[32px]",
        )}
      >
        <div className="flex w-full max-w-[680px] flex-col gap-2.5">
          <header className="flex w-full flex-col gap-2">
            <div className="flex min-w-0 items-center gap-2">
              <IconButton
                icon={RiMenuLine}
                aria-label="Open navigation"
                onClick={() => setNavigationOpen(true)}
                className="shrink-0 rounded-full lg:hidden"
              />
              <Breadcrumb>
                <BreadcrumbItem href="/templates/dashboard"><Avatar size="xs" initials="B" />BoardCN team</BreadcrumbItem>
                <BreadcrumbItem href="/templates/dashboard"><Avatar size="xs" initials="M" />Mertcan</BreadcrumbItem>
                <BreadcrumbItem current>Profile</BreadcrumbItem>
              </Breadcrumb>
            </div>

            <div className="flex w-full flex-wrap items-end justify-between gap-2">
              <h1 className="px-1 text-title-2-medium text-text-primary">Profile</h1>
              <div className="flex flex-wrap items-center justify-end gap-2">
                <ProfileNotifications notifications={notifications} />
                <ProfileFilters value={filter} onChange={changeFilter} />
                <Button leadingIcon={RiEditLine} onClick={onEdit}>Edit profile</Button>
              </div>
            </div>
          </header>

          <div className="flex w-full flex-col items-center gap-4">
            <ProfileHeroCard {...profileProps} onEdit={onEdit} />
            {(filter === "all" || filter === "agents") && <AgentsChartCard months={agentMonths} />}
            {(filter === "all" || filter === "tokens") && <TokensChartCard series={tokenSeries} />}
          </div>
        </div>
      </main>

      <ModalOverlay
        isOpen={navigationOpen}
        onOpenChange={setNavigationOpen}
        isDismissable
        className="fixed inset-0 z-50 bg-black/10 lg:hidden dark:bg-white/5"
      >
        <button
          type="button"
          aria-label="Close navigation"
          onClick={() => setNavigationOpen(false)}
          className="absolute inset-y-0 right-0 left-[272px] cursor-default outline-none"
        />
        <Modal className="relative z-10 h-full w-[272px] origin-left p-3 outline-none">
          <Dialog aria-label="Navigation" className="h-full outline-none">
            {({ close }) => (
              <DashboardSidebar mobile flat selected="profile" onClose={close} className="w-[260px]" />
            )}
          </Dialog>
        </Modal>
      </ModalOverlay>
    </div>
  );
}

export default AiProfile;
"use client";

import { useState } from "react";
import {
  RiCheckLine,
  RiEditLine,
  RiFilter3Line,
  RiMenuLine,
  RiNotification3Line,
} from "@remixicon/react";
import { Dialog, Modal, ModalOverlay } from "react-aria-components";
import { DashboardSidebar } from "@/components/blocks/dashboard/dashboard-sidebar";
import { NotificationCenter } from "@/components/blocks/notification-center/notification-center";
import type { NotificationCenterItem } from "@/components/blocks/notification-center/notification-center";
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 { cx } from "@/utils/cx";
import { AgentsChartCard } from "./agents-chart-card";
import { ProfileHeroCard, type ProfileHeroCardProps } from "./profile-hero-card";
import { TokensChartCard } from "./tokens-chart-card";
import type { AgentMonth, TokenDatum } from "./types";

export interface AiProfileProps extends ProfileHeroCardProps {
  defaultFilter?: ProfileFilter;
  onFilterChange?: (filter: ProfileFilter) => void;
  className?: string;
  agentMonths: readonly AgentMonth[];
  tokenSeries: readonly TokenDatum[];
  notifications: readonly NotificationCenterItem[];
}

export type ProfileFilter = "all" | "agents" | "tokens";

const FILTERS: readonly { id: ProfileFilter; label: string }[] = [
  { id: "all", label: "All activity" },
  { id: "agents", label: "Agents only" },
  { id: "tokens", label: "Tokens only" },
];

function ProfileNotifications({ notifications }: { notifications: readonly NotificationCenterItem[] }) {
  return (
    <Dropdown>
      <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 />
        <span className="absolute -top-1 -right-1 flex min-w-4 items-center justify-center rounded-full bg-button-danger px-1 text-[10px] leading-4 font-semibold text-white">{notifications.length}</span>
      </DropdownTrigger>
      <DropdownPopover aria-label="Notifications" placement="bottom end" className="w-[min(420px,calc(100vw-24px))] p-0">
        <NotificationCenter notifications={[...notifications]} className="max-h-[520px] overflow-y-auto" />
      </DropdownPopover>
    </Dropdown>
  );
}

function ProfileFilters({ value, onChange }: { value: ProfileFilter; onChange: (filter: ProfileFilter) => void }) {
  return (
    <Dropdown>
      <DropdownTrigger
        aria-label="Filters"
        className="inline-flex h-9 shrink-0 items-center justify-center gap-1.5 rounded-2lg border border-border-button-default bg-background-primary-default px-3 text-body-medium text-text-primary shadow-xs transition-colors duration-150 hover:bg-background-primary-hover"
      >
        <RiFilter3Line className="size-4 text-foreground-icon-secondary" aria-hidden />
        Filters
      </DropdownTrigger>
      <DropdownPopover aria-label="Profile filters" placement="bottom end" className="w-48">
        {FILTERS.map((filter) => (
          <DropdownItem key={filter.id} onSelect={() => onChange(filter.id)}>
            <span className="flex min-w-0 flex-1 items-center justify-between gap-2">
              <span>{filter.label}</span>
              {value === filter.id && <RiCheckLine className="size-4 shrink-0" aria-hidden />}
            </span>
          </DropdownItem>
        ))}
      </DropdownPopover>
    </Dropdown>
  );
}

export function AiProfile({
  className,
  defaultFilter = "all",
  onFilterChange,
  onEdit,
  agentMonths,
  tokenSeries,
  notifications,
  ...profileProps
}: AiProfileProps) {
  const [navigationOpen, setNavigationOpen] = useState(false);
  const [filter, setFilter] = useState<ProfileFilter>(defaultFilter);

  const changeFilter = (next: ProfileFilter) => {
    setFilter(next);
    onFilterChange?.(next);
  };

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

      <main
        className={cx(
          "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:p-6",
          "transition-[transform,border-radius] duration-300 ease-in-out lg:z-0 lg:!transform-none lg:!rounded-none lg:overflow-visible",
          navigationOpen && "translate-x-[272px] rounded-[32px]",
        )}
      >
        <div className="flex w-full max-w-[680px] flex-col gap-2.5">
          <header className="flex w-full flex-col gap-2">
            <div className="flex min-w-0 items-center gap-2">
              <IconButton
                icon={RiMenuLine}
                aria-label="Open navigation"
                onClick={() => setNavigationOpen(true)}
                className="shrink-0 rounded-full lg:hidden"
              />
              <Breadcrumb>
                <BreadcrumbItem href="/templates/dashboard"><Avatar size="xs" initials="B" />BoardCN team</BreadcrumbItem>
                <BreadcrumbItem href="/templates/dashboard"><Avatar size="xs" initials="M" />Mertcan</BreadcrumbItem>
                <BreadcrumbItem current>Profile</BreadcrumbItem>
              </Breadcrumb>
            </div>

            <div className="flex w-full flex-wrap items-end justify-between gap-2">
              <h1 className="px-1 text-title-2-medium text-text-primary">Profile</h1>
              <div className="flex flex-wrap items-center justify-end gap-2">
                <ProfileNotifications notifications={notifications} />
                <ProfileFilters value={filter} onChange={changeFilter} />
                <Button leadingIcon={RiEditLine} onClick={onEdit}>Edit profile</Button>
              </div>
            </div>
          </header>

          <div className="flex w-full flex-col items-center gap-4">
            <ProfileHeroCard {...profileProps} onEdit={onEdit} />
            {(filter === "all" || filter === "agents") && <AgentsChartCard months={agentMonths} />}
            {(filter === "all" || filter === "tokens") && <TokensChartCard series={tokenSeries} />}
          </div>
        </div>
      </main>

      <ModalOverlay
        isOpen={navigationOpen}
        onOpenChange={setNavigationOpen}
        isDismissable
        className="fixed inset-0 z-50 bg-black/10 lg:hidden dark:bg-white/5"
      >
        <button
          type="button"
          aria-label="Close navigation"
          onClick={() => setNavigationOpen(false)}
          className="absolute inset-y-0 right-0 left-[272px] cursor-default outline-none"
        />
        <Modal className="relative z-10 h-full w-[272px] origin-left p-3 outline-none">
          <Dialog aria-label="Navigation" className="h-full outline-none">
            {({ close }) => (
              <DashboardSidebar mobile flat selected="profile" onClose={close} className="w-[260px]" />
            )}
          </Dialog>
        </Modal>
      </ModalOverlay>
    </div>
  );
}

export default AiProfile;
View this template on GitHub