Files
calendar/apps/web/modules/insights/components/ChartCard.tsx
T
Eunjae LeeGitHubClaude Opus 4.5Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
81e4241f85 refactor: apply biome formatting to apps/web (#27692)
* refactor: apply biome formatting to apps/web (batch 1)

Formats apps/web non-modules directories:
- apps/web/app
- apps/web/lib
- apps/web/pages
- apps/web/styles
- apps/web/server
- apps/web/test
- Root-level .ts and .mjs files

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: apply biome formatting to apps/web/modules (batch 2)

Formats smaller apps/web/modules directories:
- onboarding, data-table, users, apps, auth
- integration-attribute-sync, shell, availability
- feature-flags, team, webhooks, getting-started
- feature-opt-in, troubleshooter, schedules, notifications
- signup-view.tsx

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: apply biome formatting to apps/web/modules (batch 3)

Formats medium apps/web/modules directories:
- bookings
- event-types
- insights
- embed

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: apply biome formatting to apps/web/modules/ee (batch 4)

Formats apps/web/modules/ee directory.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: apply biome formatting to apps/web (batch 5)

Formats remaining apps/web directories:
- apps/web/modules/settings
- apps/web/modules/booking-audit
- apps/web/playwright

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: apply biome formatting to apps/web/components (batch 6)

Formats remaining apps/web/components files.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: apply biome formatting to new apps/web files from main

Formats newly added/modified files from main merge:
- WorkflowStepContainer.tsx (resolved merge conflicts)
- Newly moved files (blocklist, form-builder, schedules, etc.)
- Other files modified in main

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: apply biome formatting to new apps/web files from main

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-10 23:25:24 +05:30

161 lines
4.7 KiB
TypeScript

"use client";
import { Fragment, useMemo, type ReactNode } from "react";
import classNames from "@calcom/ui/classNames";
import { PanelCard } from "@calcom/ui/components/card";
import { Spinner } from "@calcom/ui/components/icon";
import { SkeletonText } from "@calcom/ui/components/skeleton";
import { Tooltip } from "@calcom/ui/components/tooltip";
type PanelCardProps = React.ComponentProps<typeof PanelCard>;
type LegendItem = {
label: string;
color: string; // hex format
};
export type LegendSize = "sm" | "default";
export function ChartCard({
legend,
legendSize,
enabledLegend,
onSeriesToggle,
isPending,
isError,
...panelCardProps
}: Omit<PanelCardProps, "children"> & {
legend?: Array<LegendItem>;
legendSize?: LegendSize;
enabledLegend?: Array<LegendItem>;
onSeriesToggle?: (label: string) => void;
isPending?: boolean;
isError?: boolean;
children?: ReactNode;
}) {
const legendComponent =
legend && legend.length > 0 ? (
<Legend items={legend} size={legendSize} enabledItems={enabledLegend} onItemToggle={onSeriesToggle} />
) : null;
// Generate a chart ID from the title for testing purposes
const chartId = useMemo(() => {
if (typeof panelCardProps.title === "string") {
return panelCardProps.title.toLowerCase().replace(/\s+/g, "-");
}
return undefined;
}, [panelCardProps.title]);
// Calculate loading state from isPending/isError
const computedLoadingState = useMemo(() => {
if (isPending) return "loading";
if (isError) return "error";
return "loaded";
}, [isPending, isError]);
const shouldShowDefaultLoading = isPending && !panelCardProps.children;
const displayTitle = shouldShowDefaultLoading ? <SkeletonText className="w-32" /> : panelCardProps.title;
return (
<PanelCard
{...panelCardProps}
title={displayTitle}
data-testid="chart-card"
data-chart-id={chartId}
data-loading-state={computedLoadingState}
headerContent={
panelCardProps.headerContent ? (
<div className="flex items-center gap-2">
{panelCardProps.headerContent}
{legendComponent}
</div>
) : (
legendComponent
)
}>
{shouldShowDefaultLoading ? (
<div className="m-auto flex h-80 flex-col items-center justify-center">
<Spinner className="h-6 w-6" />
</div>
) : (
panelCardProps.children
)}
</PanelCard>
);
}
export function ChartCardItem({
count,
className,
children,
}: {
count?: number | string;
className?: string;
children: ReactNode;
}) {
return (
<div
className={classNames(
"text-default border-muted flex items-center justify-between border-b px-3 py-3.5 last:border-b-0",
className
)}>
<div className="grow text-sm font-medium">{children}</div>
{count !== undefined && <div className="text-sm font-medium">{count}</div>}
</div>
);
}
function Legend({
items,
size = "default",
enabledItems,
onItemToggle,
}: {
items: LegendItem[];
size?: LegendSize;
enabledItems?: LegendItem[];
onItemToggle?: (label: string) => void;
}) {
const enabledSet = useMemo(() => new Set((enabledItems ?? []).map((i) => i.label)), [enabledItems]);
const isClickable = Boolean(onItemToggle);
return (
<div className="bg-default flex items-center gap-2 rounded-lg px-1.5 py-1">
{items.map((item, index) => {
const isEnabled = enabledItems ? enabledSet.has(item.label) : true;
return (
<Fragment key={item.label}>
<button
type="button"
className={classNames(
"relative flex items-center gap-2 rounded-md px-1.5 py-0.5 transition-opacity",
isClickable && "cursor-pointer hover:bg-gray-100",
!isEnabled && "opacity-25"
)}
style={{ backgroundColor: `${item.color}33` }}
aria-pressed={isClickable ? isEnabled : undefined}
aria-label={`Toggle ${item.label}`}
disabled={!isClickable}
onClick={isClickable ? () => onItemToggle?.(item.label) : undefined}>
<div className="h-2 w-2 rounded-full" style={{ backgroundColor: item.color }} />
<Tooltip content={item.label}>
<span
className={classNames(
"text-default truncate py-0.5 text-sm font-medium leading-none",
size === "sm" ? "w-16" : ""
)}>
{item.label}
</span>
</Tooltip>
</button>
{index < items.length - 1 && <div className="bg-cal-muted h-5 w-px" />}
</Fragment>
);
})}
</div>
);
}