Installation
npx shadcn@latest add @boardcn/ai-profilenpx shadcn@latest add @boardcn/ai-profilenpm 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.
"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;"use client";
import { useState } from "react";
import { RiArrowLeftSLine, RiArrowRightSLine } from "@remixicon/react";
import { cx } from "@/utils/cx";
import type { AgentMonth } from "./types";
export interface AgentsChartCardProps {
months: readonly AgentMonth[];
className?: string;
defaultMonth?: string;
}
export function AgentsChartCard({ months, className, defaultMonth = "December" }: AgentsChartCardProps) {
const initialIndex = Math.max(0, months.findIndex((item) => item.month === defaultMonth));
const [monthIndex, setMonthIndex] = useState(initialIndex);
const month = months[monthIndex] ?? months[0];
const changeMonth = (direction: -1 | 1) => {
setMonthIndex((current) => (current + direction + months.length) % months.length);
};
return (
<section
aria-labelledby="agents-chart-title"
className={cx(
"relative flex h-[317px] w-full flex-col gap-2.5 rounded-[20px] bg-background-secondary-default px-2.5 py-3",
className,
)}
>
<div className="flex w-full px-1.5 pt-1">
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<p id="agents-chart-title" className="w-full text-body-medium text-text-secondary">Agents</p>
<p key={month.month} className="animate-number-fade text-title-2-medium whitespace-nowrap text-text-primary tabular-nums" aria-live="polite">
{month.total} agents
</p>
</div>
</div>
<div className="absolute top-4 right-4 flex h-8 w-[128px] shrink-0 items-center justify-between gap-1 rounded-2lg border border-border-button-default bg-background-primary-default px-1 py-1 shadow-xs">
<button
type="button"
aria-label="Previous"
onClick={() => changeMonth(-1)}
className="flex size-4 shrink-0 cursor-pointer items-center justify-center rounded-[3px] text-text-secondary outline-none transition-colors duration-150 ease hover:bg-background-secondary-hover focus-visible:ring-2 focus-visible:ring-border-focus-ring"
>
<RiArrowLeftSLine className="size-4" aria-hidden />
</button>
<span className="relative flex-1 overflow-hidden text-center text-body-medium whitespace-nowrap text-text-primary">
<span className="invisible">December</span>
<span key={month.month} className="absolute inset-0 flex animate-number-fade items-center justify-center">{month.month}</span>
</span>
<button
type="button"
aria-label="Next"
onClick={() => changeMonth(1)}
className="flex size-4 shrink-0 cursor-pointer items-center justify-center rounded-[3px] text-text-secondary outline-none transition-colors duration-150 ease hover:bg-background-secondary-hover focus-visible:ring-2 focus-visible:ring-border-focus-ring"
>
<RiArrowRightSLine className="size-4" aria-hidden />
</button>
</div>
<div
className="flex min-h-0 w-full flex-1 items-end gap-[7px]"
role="application"
aria-label={`${month.month} agents activity. ${month.total} agents across 30 days; inactive days are shown as neutral stubs.`}
>
{month.values.map((value, index) => (
<div key={`${month.month}-${index}`} className="flex h-full min-w-0 flex-1 items-end rounded-sm">
<div
className={cx(
"animate-bar-rise w-full rounded-sm transition-[height,background-color] duration-300 ease",
value === 0 ? "bg-chart-track" : "bg-chart-agents-bar",
)}
style={{ height: value === 0 ? 4 : value, animationDelay: `${index * 22}ms` }}
/>
</div>
))}
</div>
<div className="flex w-full items-start justify-between px-1.5 text-[11px] leading-[15px] font-medium tracking-[0.2px] whitespace-nowrap text-text-tertiary">
<p>Jun 14</p>
<p>Today</p>
</div>
</section>
);
}"use client";
import { useState } from "react";
import { RiArrowLeftSLine, RiArrowRightSLine } from "@remixicon/react";
import { cx } from "@/utils/cx";
import type { AgentMonth } from "./types";
export interface AgentsChartCardProps {
months: readonly AgentMonth[];
className?: string;
defaultMonth?: string;
}
export function AgentsChartCard({ months, className, defaultMonth = "December" }: AgentsChartCardProps) {
const initialIndex = Math.max(0, months.findIndex((item) => item.month === defaultMonth));
const [monthIndex, setMonthIndex] = useState(initialIndex);
const month = months[monthIndex] ?? months[0];
const changeMonth = (direction: -1 | 1) => {
setMonthIndex((current) => (current + direction + months.length) % months.length);
};
return (
<section
aria-labelledby="agents-chart-title"
className={cx(
"relative flex h-[317px] w-full flex-col gap-2.5 rounded-[20px] bg-background-secondary-default px-2.5 py-3",
className,
)}
>
<div className="flex w-full px-1.5 pt-1">
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<p id="agents-chart-title" className="w-full text-body-medium text-text-secondary">Agents</p>
<p key={month.month} className="animate-number-fade text-title-2-medium whitespace-nowrap text-text-primary tabular-nums" aria-live="polite">
{month.total} agents
</p>
</div>
</div>
<div className="absolute top-4 right-4 flex h-8 w-[128px] shrink-0 items-center justify-between gap-1 rounded-2lg border border-border-button-default bg-background-primary-default px-1 py-1 shadow-xs">
<button
type="button"
aria-label="Previous"
onClick={() => changeMonth(-1)}
className="flex size-4 shrink-0 cursor-pointer items-center justify-center rounded-[3px] text-text-secondary outline-none transition-colors duration-150 ease hover:bg-background-secondary-hover focus-visible:ring-2 focus-visible:ring-border-focus-ring"
>
<RiArrowLeftSLine className="size-4" aria-hidden />
</button>
<span className="relative flex-1 overflow-hidden text-center text-body-medium whitespace-nowrap text-text-primary">
<span className="invisible">December</span>
<span key={month.month} className="absolute inset-0 flex animate-number-fade items-center justify-center">{month.month}</span>
</span>
<button
type="button"
aria-label="Next"
onClick={() => changeMonth(1)}
className="flex size-4 shrink-0 cursor-pointer items-center justify-center rounded-[3px] text-text-secondary outline-none transition-colors duration-150 ease hover:bg-background-secondary-hover focus-visible:ring-2 focus-visible:ring-border-focus-ring"
>
<RiArrowRightSLine className="size-4" aria-hidden />
</button>
</div>
<div
className="flex min-h-0 w-full flex-1 items-end gap-[7px]"
role="application"
aria-label={`${month.month} agents activity. ${month.total} agents across 30 days; inactive days are shown as neutral stubs.`}
>
{month.values.map((value, index) => (
<div key={`${month.month}-${index}`} className="flex h-full min-w-0 flex-1 items-end rounded-sm">
<div
className={cx(
"animate-bar-rise w-full rounded-sm transition-[height,background-color] duration-300 ease",
value === 0 ? "bg-chart-track" : "bg-chart-agents-bar",
)}
style={{ height: value === 0 ? 4 : value, animationDelay: `${index * 22}ms` }}
/>
</div>
))}
</div>
<div className="flex w-full items-start justify-between px-1.5 text-[11px] leading-[15px] font-medium tracking-[0.2px] whitespace-nowrap text-text-tertiary">
<p>Jun 14</p>
<p>Today</p>
</div>
</section>
);
}export { AiProfile, type AiProfileProps, type ProfileFilter } from "./ai-profile";
export { AgentsChartCard, type AgentsChartCardProps } from "./agents-chart-card";
export { ProfileHeroCard, type ProfileHeroCardProps } from "./profile-hero-card";
export { TokensChartCard, type TokensChartCardProps } from "./tokens-chart-card";
export type { AgentMonth, ProfileStat, TokenDatum } from "./types";export { AiProfile, type AiProfileProps, type ProfileFilter } from "./ai-profile";
export { AgentsChartCard, type AgentsChartCardProps } from "./agents-chart-card";
export { ProfileHeroCard, type ProfileHeroCardProps } from "./profile-hero-card";
export { TokensChartCard, type TokensChartCardProps } from "./tokens-chart-card";
export type { AgentMonth, ProfileStat, TokenDatum } from "./types";"use client";
import { useState } from "react";
import { Chip } from "@/components/base/badges/chip";
import { Button } from "@/components/base/buttons/button";
import {
SegmentedControl,
SegmentedControlItem,
} from "@/components/base/segmented-control/segmented-control";
import { ContributionsGrid } from "@/components/charts/contributions-card";
import { cx } from "@/utils/cx";
import type { ProfileStat } from "./types";
const MONTHS = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
] as const;
const ACTIVITY_PERIODS = {
weekly: { seed: 0, label: "Weekly" },
monthly: { seed: 7, label: "Monthly" },
yearly: { seed: 19, label: "Yearly" },
} as const;
type ActivityPeriod = keyof typeof ACTIVITY_PERIODS;
export interface ProfileHeroCardProps {
name?: string;
handle?: string;
initials?: string;
contributionValue?: string;
coverImageSrc?: string;
stats: readonly ProfileStat[];
onShare?: () => void;
onEdit?: () => void;
className?: string;
}
function ProfileCover({ src }: { src?: string }) {
return (
<div className="absolute inset-x-0 top-0 h-[165px] overflow-hidden rounded-t-[23px] bg-background-tertiary-default">
{src ? (
<img src={src} alt="" className="size-full object-cover object-[50%_45%]" />
) : (
<div
aria-hidden
className={cx(
"size-full bg-[linear-gradient(128deg,#ece7de_0%,#d7d1c3_42%,#9a8d7b_100%)]",
"after:absolute after:inset-0 after:bg-[radial-gradient(circle_at_72%_38%,rgba(255,255,255,.8),transparent_24%),linear-gradient(18deg,transparent_52%,rgba(52,45,39,.16)_53%,transparent_54%)]",
"dark:bg-[linear-gradient(128deg,#252321_0%,#34302b_45%,#62594e_100%)]",
)}
/>
)}
</div>
);
}
export function ProfileHeroCard({
name = "Mertcan Esmergül",
handle = "@sitenley",
initials = "M",
stats,
contributionValue = "$7,462",
coverImageSrc,
onShare,
onEdit,
className,
}: ProfileHeroCardProps) {
const [period, setPeriod] = useState<ActivityPeriod>("weekly");
const activity = ACTIVITY_PERIODS[period];
return (
<section
aria-labelledby="ai-profile-name"
className={cx(
"relative w-full overflow-hidden rounded-3xl border border-border-ai-profile-card",
className,
)}
>
<ProfileCover src={coverImageSrc} />
<div className="relative flex w-full flex-col gap-[15px] px-4 pt-[124px] pb-4">
<span className="flex size-20 items-center justify-center rounded-full bg-background-tertiary-default ring-4 ring-background-full">
<span className="text-[30px] leading-[42.5px] font-medium text-text-secondary">{initials}</span>
</span>
<div className="relative flex w-full items-start gap-[15px]">
<div className="flex min-w-0 flex-1 flex-col gap-1">
<div className="flex min-w-0 items-center gap-2">
<p id="ai-profile-name" className="truncate text-title-2-medium text-text-primary">{name}</p>
<Chip color="purple" variant="caption" className="py-0.5">PRO</Chip>
</div>
<p className="text-body-medium text-text-secondary">{handle}</p>
</div>
<div className="absolute -top-[34px] right-1 flex items-center justify-end gap-2.5">
<Button variant="secondary" size="small" onClick={onShare}>Share</Button>
<Button variant="secondary" size="small" onClick={onEdit}>Edit</Button>
</div>
</div>
<div className="flex w-full flex-col gap-2">
<div className="flex flex-col gap-0.5">
<p className="text-body-medium text-text-secondary">Contributions this year</p>
<div className="flex items-center gap-2">
<p className="text-title-1-medium whitespace-nowrap text-text-primary tabular-nums">
{contributionValue}
</p>
<Chip color="purple">+14.8%</Chip>
</div>
</div>
<div className="grid grid-cols-2 gap-2 sm:flex sm:items-stretch">
{stats.map((stat) => (
<div
key={stat.label}
className="flex min-w-0 flex-col items-start rounded-2lg bg-background-inner-default p-2.5 shadow-card sm:flex-1"
>
<p className="w-full truncate text-body-medium text-text-primary">{stat.value}</p>
<p className="w-full truncate text-body-medium text-text-secondary">{stat.label}</p>
</div>
))}
</div>
<div className="flex w-full items-center justify-between pt-1.5 pl-0.5">
<p className="text-body-medium text-text-secondary">Activity</p>
<SegmentedControl
variant="plain"
selectedKeys={[period]}
onSelectionChange={(keys) => {
const selected = [...keys][0];
if (selected && selected in ACTIVITY_PERIODS) setPeriod(String(selected) as ActivityPeriod);
}}
aria-label="Activity period"
>
<SegmentedControlItem id="weekly">Weekly</SegmentedControlItem>
<SegmentedControlItem id="monthly">Monthly</SegmentedControlItem>
<SegmentedControlItem id="yearly">Yearly</SegmentedControlItem>
</SegmentedControl>
</div>
<div className="flex w-full overflow-x-auto sm:overflow-visible">
<div className="flex w-[646px] shrink-0 flex-col gap-1.5 sm:w-full">
<ContributionsGrid
key={period}
columns={38}
accent="violet"
animateIn
seed={activity.seed}
aria-label={`${activity.label} contribution activity for ${name}`}
/>
<span className="sr-only" aria-live="polite">{activity.label} activity selected</span>
<div className="flex w-full justify-between text-body-2-medium text-text-tertiary">
{MONTHS.map((month) => <span key={month}>{month}</span>)}
</div>
</div>
</div>
</div>
</div>
</section>
);
}"use client";
import { useState } from "react";
import { Chip } from "@/components/base/badges/chip";
import { Button } from "@/components/base/buttons/button";
import {
SegmentedControl,
SegmentedControlItem,
} from "@/components/base/segmented-control/segmented-control";
import { ContributionsGrid } from "@/components/charts/contributions-card";
import { cx } from "@/utils/cx";
import type { ProfileStat } from "./types";
const MONTHS = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
] as const;
const ACTIVITY_PERIODS = {
weekly: { seed: 0, label: "Weekly" },
monthly: { seed: 7, label: "Monthly" },
yearly: { seed: 19, label: "Yearly" },
} as const;
type ActivityPeriod = keyof typeof ACTIVITY_PERIODS;
export interface ProfileHeroCardProps {
name?: string;
handle?: string;
initials?: string;
contributionValue?: string;
coverImageSrc?: string;
stats: readonly ProfileStat[];
onShare?: () => void;
onEdit?: () => void;
className?: string;
}
function ProfileCover({ src }: { src?: string }) {
return (
<div className="absolute inset-x-0 top-0 h-[165px] overflow-hidden rounded-t-[23px] bg-background-tertiary-default">
{src ? (
<img src={src} alt="" className="size-full object-cover object-[50%_45%]" />
) : (
<div
aria-hidden
className={cx(
"size-full bg-[linear-gradient(128deg,#ece7de_0%,#d7d1c3_42%,#9a8d7b_100%)]",
"after:absolute after:inset-0 after:bg-[radial-gradient(circle_at_72%_38%,rgba(255,255,255,.8),transparent_24%),linear-gradient(18deg,transparent_52%,rgba(52,45,39,.16)_53%,transparent_54%)]",
"dark:bg-[linear-gradient(128deg,#252321_0%,#34302b_45%,#62594e_100%)]",
)}
/>
)}
</div>
);
}
export function ProfileHeroCard({
name = "Mertcan Esmergül",
handle = "@sitenley",
initials = "M",
stats,
contributionValue = "$7,462",
coverImageSrc,
onShare,
onEdit,
className,
}: ProfileHeroCardProps) {
const [period, setPeriod] = useState<ActivityPeriod>("weekly");
const activity = ACTIVITY_PERIODS[period];
return (
<section
aria-labelledby="ai-profile-name"
className={cx(
"relative w-full overflow-hidden rounded-3xl border border-border-ai-profile-card",
className,
)}
>
<ProfileCover src={coverImageSrc} />
<div className="relative flex w-full flex-col gap-[15px] px-4 pt-[124px] pb-4">
<span className="flex size-20 items-center justify-center rounded-full bg-background-tertiary-default ring-4 ring-background-full">
<span className="text-[30px] leading-[42.5px] font-medium text-text-secondary">{initials}</span>
</span>
<div className="relative flex w-full items-start gap-[15px]">
<div className="flex min-w-0 flex-1 flex-col gap-1">
<div className="flex min-w-0 items-center gap-2">
<p id="ai-profile-name" className="truncate text-title-2-medium text-text-primary">{name}</p>
<Chip color="purple" variant="caption" className="py-0.5">PRO</Chip>
</div>
<p className="text-body-medium text-text-secondary">{handle}</p>
</div>
<div className="absolute -top-[34px] right-1 flex items-center justify-end gap-2.5">
<Button variant="secondary" size="small" onClick={onShare}>Share</Button>
<Button variant="secondary" size="small" onClick={onEdit}>Edit</Button>
</div>
</div>
<div className="flex w-full flex-col gap-2">
<div className="flex flex-col gap-0.5">
<p className="text-body-medium text-text-secondary">Contributions this year</p>
<div className="flex items-center gap-2">
<p className="text-title-1-medium whitespace-nowrap text-text-primary tabular-nums">
{contributionValue}
</p>
<Chip color="purple">+14.8%</Chip>
</div>
</div>
<div className="grid grid-cols-2 gap-2 sm:flex sm:items-stretch">
{stats.map((stat) => (
<div
key={stat.label}
className="flex min-w-0 flex-col items-start rounded-2lg bg-background-inner-default p-2.5 shadow-card sm:flex-1"
>
<p className="w-full truncate text-body-medium text-text-primary">{stat.value}</p>
<p className="w-full truncate text-body-medium text-text-secondary">{stat.label}</p>
</div>
))}
</div>
<div className="flex w-full items-center justify-between pt-1.5 pl-0.5">
<p className="text-body-medium text-text-secondary">Activity</p>
<SegmentedControl
variant="plain"
selectedKeys={[period]}
onSelectionChange={(keys) => {
const selected = [...keys][0];
if (selected && selected in ACTIVITY_PERIODS) setPeriod(String(selected) as ActivityPeriod);
}}
aria-label="Activity period"
>
<SegmentedControlItem id="weekly">Weekly</SegmentedControlItem>
<SegmentedControlItem id="monthly">Monthly</SegmentedControlItem>
<SegmentedControlItem id="yearly">Yearly</SegmentedControlItem>
</SegmentedControl>
</div>
<div className="flex w-full overflow-x-auto sm:overflow-visible">
<div className="flex w-[646px] shrink-0 flex-col gap-1.5 sm:w-full">
<ContributionsGrid
key={period}
columns={38}
accent="violet"
animateIn
seed={activity.seed}
aria-label={`${activity.label} contribution activity for ${name}`}
/>
<span className="sr-only" aria-live="polite">{activity.label} activity selected</span>
<div className="flex w-full justify-between text-body-2-medium text-text-tertiary">
{MONTHS.map((month) => <span key={month}>{month}</span>)}
</div>
</div>
</div>
</div>
</div>
</section>
);
}"use client";
import { useId } from "react";
import { Area, AreaChart, Line, ResponsiveContainer, YAxis } from "recharts";
import { Chip } from "@/components/base/badges/chip";
import { cx } from "@/utils/cx";
import type { TokenDatum } from "./types";
export interface TokensChartCardProps {
series: readonly TokenDatum[];
className?: string;
}
export function TokensChartCard({ series, className }: TokensChartCardProps) {
const gradientId = useId().replace(/:/g, "");
const tokenData = series.map((point, index) => ({
...point,
opening: index <= 3 ? point.value : null,
idle: index >= 3 && index <= 11 ? point.value : null,
active: index >= 11 ? point.value : null,
}));
return (
<section
aria-labelledby="tokens-chart-title"
className={cx("flex h-[267px] w-full flex-col rounded-[20px] bg-background-secondary-default py-3", className)}
>
<div className="relative z-10 -mb-8 flex w-full px-4 pt-1">
<div className="flex flex-col gap-0.5">
<p id="tokens-chart-title" className="text-body-medium whitespace-nowrap text-text-secondary">Tokens</p>
<div className="flex items-center gap-2">
<p className="text-title-2-medium whitespace-nowrap text-text-primary tabular-nums">667.7M tokens</p>
<Chip color="purple">+9.4%</Chip>
</div>
</div>
</div>
<div
className="animate-chart-reveal h-[200px] w-full"
>
<ResponsiveContainer width="100%" height="100%">
<AreaChart
data={tokenData}
margin={{ top: 0, right: 0, bottom: 2, left: 0 }}
accessibilityLayer
aria-label="Tokens trend from Jun 14 to today. 667.7 million tokens, up 9.4 percent."
>
<defs>
<linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="var(--color-purple-400)" stopOpacity={0.18} />
<stop offset="100%" stopColor="var(--color-purple-400)" stopOpacity={0} />
</linearGradient>
</defs>
<YAxis hide domain={[0, 198]} />
<Area type="linear" dataKey="value" stroke="none" fill={`url(#${gradientId})`} isAnimationActive={false} />
<Line type="linear" dataKey="opening" stroke="var(--color-purple-400)" strokeWidth={2} dot={false} connectNulls={false} isAnimationActive={false} />
<Line type="linear" dataKey="idle" stroke="var(--color-neutral-400)" strokeWidth={2} dot={false} connectNulls={false} isAnimationActive={false} />
<Line type="linear" dataKey="active" stroke="var(--color-purple-400)" strokeWidth={2} dot={false} connectNulls={false} isAnimationActive={false} />
</AreaChart>
</ResponsiveContainer>
</div>
<div className="mt-2 flex w-full items-start justify-between px-4 text-[11px] leading-[15px] font-medium tracking-[0.2px] whitespace-nowrap text-text-tertiary">
<p>Jun 14</p>
<p>Today</p>
</div>
</section>
);
}"use client";
import { useId } from "react";
import { Area, AreaChart, Line, ResponsiveContainer, YAxis } from "recharts";
import { Chip } from "@/components/base/badges/chip";
import { cx } from "@/utils/cx";
import type { TokenDatum } from "./types";
export interface TokensChartCardProps {
series: readonly TokenDatum[];
className?: string;
}
export function TokensChartCard({ series, className }: TokensChartCardProps) {
const gradientId = useId().replace(/:/g, "");
const tokenData = series.map((point, index) => ({
...point,
opening: index <= 3 ? point.value : null,
idle: index >= 3 && index <= 11 ? point.value : null,
active: index >= 11 ? point.value : null,
}));
return (
<section
aria-labelledby="tokens-chart-title"
className={cx("flex h-[267px] w-full flex-col rounded-[20px] bg-background-secondary-default py-3", className)}
>
<div className="relative z-10 -mb-8 flex w-full px-4 pt-1">
<div className="flex flex-col gap-0.5">
<p id="tokens-chart-title" className="text-body-medium whitespace-nowrap text-text-secondary">Tokens</p>
<div className="flex items-center gap-2">
<p className="text-title-2-medium whitespace-nowrap text-text-primary tabular-nums">667.7M tokens</p>
<Chip color="purple">+9.4%</Chip>
</div>
</div>
</div>
<div
className="animate-chart-reveal h-[200px] w-full"
>
<ResponsiveContainer width="100%" height="100%">
<AreaChart
data={tokenData}
margin={{ top: 0, right: 0, bottom: 2, left: 0 }}
accessibilityLayer
aria-label="Tokens trend from Jun 14 to today. 667.7 million tokens, up 9.4 percent."
>
<defs>
<linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="var(--color-purple-400)" stopOpacity={0.18} />
<stop offset="100%" stopColor="var(--color-purple-400)" stopOpacity={0} />
</linearGradient>
</defs>
<YAxis hide domain={[0, 198]} />
<Area type="linear" dataKey="value" stroke="none" fill={`url(#${gradientId})`} isAnimationActive={false} />
<Line type="linear" dataKey="opening" stroke="var(--color-purple-400)" strokeWidth={2} dot={false} connectNulls={false} isAnimationActive={false} />
<Line type="linear" dataKey="idle" stroke="var(--color-neutral-400)" strokeWidth={2} dot={false} connectNulls={false} isAnimationActive={false} />
<Line type="linear" dataKey="active" stroke="var(--color-purple-400)" strokeWidth={2} dot={false} connectNulls={false} isAnimationActive={false} />
</AreaChart>
</ResponsiveContainer>
</div>
<div className="mt-2 flex w-full items-start justify-between px-4 text-[11px] leading-[15px] font-medium tracking-[0.2px] whitespace-nowrap text-text-tertiary">
<p>Jun 14</p>
<p>Today</p>
</div>
</section>
);
}export interface ProfileStat {
value: string;
label: string;
}
export interface AgentMonth {
month: string;
total: number;
values: readonly number[];
}
export interface TokenDatum {
day: number;
value: number;
}export interface ProfileStat {
value: string;
label: string;
}
export interface AgentMonth {
month: string;
total: number;
values: readonly number[];
}
export interface TokenDatum {
day: number;
value: number;
}