refactor: replace tremor with recharts (#22791)

* refactor: replace tremor with recharts

* update tooltip

* clean up types

* replace tremor with recharts

* replace BarList with recharts

* replace ProgressBar

* remove tremor from the repository

* clean up

* fix UserStatsTable

* fix type error

* add explicit return type
This commit is contained in:
Eunjae Lee
2025-08-05 01:21:23 +01:00
committed by GitHub
parent fb364971fa
commit 593c48c8b4
33 changed files with 467 additions and 717 deletions
@@ -22,7 +22,6 @@ import {
RecentFeedbackTable,
TimezoneBadge,
} from "@calcom/features/insights/components/booking";
import "@calcom/features/insights/components/tremor.css";
import { InsightsOrgTeamsProvider } from "@calcom/features/insights/context/InsightsOrgTeamsProvider";
import { Download } from "@calcom/features/insights/filters/Download";
import { OrgTeamsFilter } from "@calcom/features/insights/filters/OrgTeamsFilter";
@@ -1,6 +1,5 @@
"use client";
import { ProgressBar } from "@tremor/react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState, useMemo } from "react";
@@ -16,6 +15,7 @@ import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui/components/button";
import { Select } from "@calcom/ui/components/form";
import { TextField, Label, InputError } from "@calcom/ui/components/form";
import { ProgressBar } from "@calcom/ui/components/progress-bar";
import { showToast } from "@calcom/ui/toast";
import { BillingCreditsSkeleton } from "./BillingCreditsSkeleton";
-1
View File
@@ -76,7 +76,6 @@
"@stripe/stripe-js": "^1.35.0",
"@tanstack/react-query": "^5.17.15",
"@team-plain/typescript-sdk": "^5.9.0",
"@tremor/react": "^2.11.0",
"@types/turndown": "^5.0.1",
"@unkey/ratelimit": "^0.1.1",
"@upstash/redis": "^1.21.0",
@@ -3402,6 +3402,7 @@
"license_key_saved": "License key saved successfully",
"timezone_mismatch_tooltip": "You are viewing the report based on your profile timezone ({{userTimezone}}), while your browser is set to timezone ({{browserTimezone}})",
"failed_bookings_by_field": "Failed Bookings By Field",
"failed_bookings": "Failed bookings",
"event_type_no_hosts": "No hosts are assigned to event type",
"cache_status": "Cache Status",
"cache_last_updated": "Last updated: {{timestamp}}",
@@ -3429,6 +3430,7 @@
"usage_based_expiration_description": "This link can be used for {{count}} booking",
"usage_based_generic_expiration_description": "This link can be configured to expire after a set number of bookings",
"usage_based_expiration_description_plural": "This link can be used for {{count}} bookings",
"stats": "Stats",
"booking_status": "Booking status",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
+1 -5
View File
@@ -2,10 +2,6 @@ const base = require("@calcom/config/tailwind-preset");
/** @type {import('tailwindcss').Config} */
module.exports = {
...base,
content: [
...base.content,
"../../packages/app-store/routing-forms/**/*.{js,ts,jsx,tsx}",
"../../node_modules/@tremor/**/*.{js,ts,jsx,tsx}",
],
content: [...base.content, "../../packages/app-store/routing-forms/**/*.{js,ts,jsx,tsx}"],
plugins: [...base.plugins, require("tailwindcss-animate")],
};
@@ -1,18 +0,0 @@
import { Card } from "@tremor/react";
import classNames from "@calcom/ui/classNames";
interface ICardProps {
children: React.ReactNode;
className?: string;
}
export const CardInsights = (props: ICardProps) => {
const { children, className = "", ...rest } = props;
return (
<Card className={classNames(`ring-subtle bg-muted shadow-none `, className)} {...rest}>
{children}
</Card>
);
};
@@ -21,7 +21,7 @@ export function ChartCard({
legendSize,
children,
}: {
title: string;
title: string | React.ReactNode;
subtitle?: string;
cta?: { label: string; onClick: () => void };
legend?: Array<LegendItem>;
@@ -31,7 +31,11 @@ export function ChartCard({
return (
<div className="bg-muted group relative flex w-full flex-col items-center rounded-2xl px-1 pb-1">
<div className="flex h-11 w-full shrink-0 items-center justify-between gap-2 px-4">
<h2 className="text-emphasis mr-4 shrink-0 text-sm font-semibold">{title}</h2>
{typeof title === "string" ? (
<h2 className="text-emphasis mr-4 shrink-0 text-sm font-semibold">{title}</h2>
) : (
title
)}
<div className="no-scrollbar flex items-center gap-2 overflow-x-auto">
{legend && legend.length > 0 && <Legend items={legend} size={legendSize} />}
{cta && (
@@ -80,13 +84,13 @@ function Legend({ items, size = "default" }: { items: LegendItem[]; size?: Legen
{items.map((item, index) => (
<Fragment key={item.label}>
<div
className="relative flex items-center gap-2 rounded-md px-1.5 py-1"
className="relative flex items-center gap-2 rounded-md px-1.5 py-0.5"
style={{ backgroundColor: `${item.color}33` }}>
<div className="h-2 w-2 rounded-full" style={{ backgroundColor: item.color }} />
<Tooltip content={item.label}>
<p
className={classNames(
"text-default truncate text-sm font-medium leading-none",
"text-default truncate py-0.5 text-sm font-medium leading-none",
size === "sm" ? "w-16" : ""
)}>
{item.label}
@@ -1,11 +1,11 @@
"use client";
import { Flex, Text, Metric, BadgeDelta } from "@tremor/react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Badge } from "@calcom/ui/components/badge";
import { Icon } from "@calcom/ui/components/icon";
import { Tooltip } from "@calcom/ui/components/tooltip";
import { calculateDeltaType, colors, valueFormatter } from "../lib";
import { calculateDeltaType, valueFormatter } from "../lib";
export const KPICard = ({
title,
@@ -13,7 +13,6 @@ export const KPICard = ({
previousDateRange,
}: {
title: string;
value: number;
previousMetricData: {
count: number;
deltaPrevious: number;
@@ -21,22 +20,54 @@ export const KPICard = ({
previousDateRange: { startDate: string; endDate: string };
}) => {
const { t } = useLocale();
const deltaType = calculateDeltaType(previousMetricData.deltaPrevious - previousMetricData.count);
const deltaValue = Number(previousMetricData.deltaPrevious).toFixed(0);
const getBadgeVariant = (type: string) => {
switch (type) {
case "increase":
case "moderateIncrease":
return "success" as const;
case "decrease":
case "moderateDecrease":
return "error" as const;
case "unchanged":
default:
return "gray" as const;
}
};
// Get appropriate icon for delta
const getDeltaIcon = (type: string) => {
switch (type) {
case "increase":
case "moderateIncrease":
return "arrow-up" as const;
case "decrease":
case "moderateDecrease":
return "arrow-down" as const;
case "unchanged":
default:
return null;
}
};
const badgeVariant = getBadgeVariant(deltaType);
const deltaIcon = getDeltaIcon(deltaType);
return (
<div>
<Text className="text-default">{title}</Text>
<Flex className="items-baseline justify-start space-x-3 truncate">
<Metric className="text-emphasis">{valueFormatter(previousMetricData.count)}</Metric>
</Flex>
<Flex className="mt-4 justify-start space-x-2">
<BadgeDelta
deltaType={calculateDeltaType(previousMetricData.deltaPrevious - previousMetricData.count)}
/>
<Flex className="justify-start space-x-1 truncate">
<Text
color={colors[calculateDeltaType(previousMetricData.deltaPrevious - previousMetricData.count)]}>
{Number(previousMetricData.deltaPrevious).toFixed(0)}%
</Text>
<div className="text-default text-sm">{title}</div>
<div className="flex items-baseline justify-start space-x-3 truncate">
<div className="text-emphasis text-2xl font-semibold">{valueFormatter(previousMetricData.count)}</div>
</div>
<div className="mt-4 flex items-center justify-start space-x-2">
<Badge variant={badgeVariant} className="flex items-center gap-1">
{deltaIcon && <Icon name={deltaIcon} className="h-3 w-3" />}
{deltaValue}%
</Badge>
<div className="flex justify-start space-x-1 truncate">
<Tooltip
content={t("from_to_date_period", {
startDate: previousDateRange.startDate,
@@ -46,8 +77,8 @@ export const KPICard = ({
{t("from_last_period")}
</small>
</Tooltip>
</Flex>
</Flex>
</div>
</div>
</div>
);
};
@@ -1,8 +0,0 @@
import { LineChart as ExternalLineChart } from "@tremor/react";
import type { LineChartProps } from "./tremor.types";
// Honestly this is a mess. Why are all chart libraries in existance horrible to theme
export const LineChart = (props: LineChartProps) => {
return <ExternalLineChart {...props} />;
};
@@ -1,26 +1,25 @@
import classNames from "@calcom/ui/classNames";
import { SkeletonText } from "@calcom/ui/components/skeleton";
import { CardInsights } from "./Card";
import { ChartCard } from "./ChartCard";
export const LoadingInsight = () => {
return (
<CardInsights>
<SkeletonText className="w-32" />
<ChartCard title={<SkeletonText className="w-32" />}>
<div className="m-auto flex h-80 flex-col items-center justify-center">
<svg
className={classNames("mx-4 h-8 w-8 animate-spin", "text-black dark:text-white")}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
className="animate-spin">
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg>
</div>
</CardInsights>
</ChartCard>
);
};
@@ -2,25 +2,24 @@
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { User } from "@calcom/prisma/client";
import { Avatar } from "@calcom/ui/components/avatar";
import { ChartCardItem } from "./ChartCard";
export const UserStatsTable = ({
data,
}: {
data:
| {
userId: number | null;
user: Pick<User, "avatarUrl" | "name">;
emailMd5?: string;
count?: number;
averageRating?: number | null;
username?: string;
}[]
| undefined;
}) => {
type UserStatsData = {
userId: number;
user: {
id: number;
username: string | null;
name: string | null;
email: string;
avatarUrl: string;
};
emailMd5: string;
count: number;
}[];
export const UserStatsTable = ({ data }: { data: UserStatsData }) => {
const { t } = useLocale();
// Filter out items without user data
@@ -32,7 +31,7 @@ export const UserStatsTable = ({
filteredData.map((item) => (
<ChartCardItem
key={item.userId || `user-${Math.random()}`}
count={item.averageRating ? item.averageRating.toFixed(1) : item.count}
count={Number.isInteger(item.count) ? item.count : item.count.toFixed(1)}
className="py-3">
<div className="flex items-center">
<Avatar
@@ -1,14 +1,70 @@
"use client";
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc";
import type { RouterOutputs } from "@calcom/trpc/react";
import { useInsightsBookingParameters } from "../../hooks/useInsightsBookingParameters";
import { valueFormatter } from "../../lib/valueFormatter";
import { ChartCard } from "../ChartCard";
import { LineChart } from "../LineChart";
import { LoadingInsight } from "../LoadingInsights";
const COLOR = {
AVERAGE: "#3b82f6",
};
type AverageEventDurationData = RouterOutputs["viewer"]["insights"]["averageEventDuration"][number];
// Custom duration formatter that shows hours and minutes
const formatDuration = (minutes: number) => {
if (!minutes) return "0m";
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
const remainingMinutesStr = remainingMinutes.toFixed(1);
if (hours > 0 && remainingMinutes > 0) {
return `${hours}h ${remainingMinutesStr}m`;
} else if (hours > 0) {
return `${hours}h`;
} else {
return `${remainingMinutesStr}m`;
}
};
// Custom Tooltip component
const CustomTooltip = ({
active,
payload,
label,
}: {
active?: boolean;
payload?: Array<{
value: number;
dataKey: string;
name: string;
color: string;
payload: AverageEventDurationData;
}>;
label?: string;
}) => {
if (!active || !payload?.length) {
return null;
}
return (
<div className="bg-default text-inverted border-subtle rounded-lg border p-3 shadow-lg">
<p className="text-default font-medium">{label}</p>
{payload.map((entry, index: number) => (
<p key={index} style={{ color: entry.color }}>
{entry.name}: {formatDuration(entry.value)}
</p>
))}
</div>
);
};
export const AverageEventDurationChart = () => {
const { t } = useLocale();
const insightsBookingParams = useInsightsBookingParameters();
@@ -36,14 +92,32 @@ export const AverageEventDurationChart = () => {
</div>
)}
{data && data.length > 0 && !isNoData && (
<LineChart
className="mt-4 h-80"
data={data}
index="Date"
categories={["Average"]}
colors={["blue"]}
valueFormatter={valueFormatter}
/>
<div className="mt-4 h-80">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={data} margin={{ top: 30, right: 20, left: 0, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" vertical={false} />
<XAxis dataKey="Date" className="text-xs" axisLine={false} tickLine={false} />
<YAxis
allowDecimals={false}
className="text-xs opacity-50"
axisLine={false}
tickLine={false}
tickFormatter={formatDuration}
/>
<Tooltip content={<CustomTooltip />} />
<Line
type="linear"
dataKey="Average"
name={t("average_event_duration")}
stroke={COLOR.AVERAGE}
strokeWidth={2}
dot={{ r: 4 }}
activeDot={{ r: 6 }}
animationDuration={1000}
/>
</LineChart>
</ResponsiveContainer>
</div>
)}
</ChartCard>
);
@@ -84,7 +84,6 @@ export const BookingKPICards = () => {
<StatItem key={item.title} index={index}>
<KPICard
title={item.title}
value={data[item.index].count}
previousMetricData={data[item.index]}
previousDateRange={data.previousRange}
/>
@@ -99,7 +98,6 @@ export const BookingKPICards = () => {
<StatItem key={item.title} index={index}>
<KPICard
title={item.title}
value={data[item.index].count}
previousMetricData={data[item.index]}
previousDateRange={data.previousRange}
/>
@@ -13,15 +13,13 @@ import {
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc";
import type { RouterOutputs } from "@calcom/trpc/react";
import { useInsightsBookingParameters } from "../../hooks/useInsightsBookingParameters";
import { ChartCard } from "../ChartCard";
import { LoadingInsight } from "../LoadingInsights";
type BookingsByHourData = {
hour: number;
count: number;
};
type BookingsByHourData = RouterOutputs["viewer"]["insights"]["bookingsByHourStats"][number];
export const BookingsByHourChartContent = ({ data }: { data: BookingsByHourData[] }) => {
const { t } = useLocale();
@@ -1,14 +1,68 @@
"use client";
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc";
import type { RouterOutputs } from "@calcom/trpc/react";
import { useInsightsBookingParameters } from "../../hooks/useInsightsBookingParameters";
import { valueFormatter } from "../../lib/valueFormatter";
import { ChartCard } from "../ChartCard";
import { LineChart } from "../LineChart";
import { LoadingInsight } from "../LoadingInsights";
const COLOR = {
CREATED: "#a855f7",
COMPLETED: "#22c55e",
RESCHEDULED: "#3b82f6",
CANCELLED: "#ef4444",
NO_SHOW_HOST: "#64748b",
NO_SHOW_GUEST: "#f97316",
};
export const legend = [
{ label: "Created", color: COLOR.CREATED },
{ label: "Completed", color: COLOR.COMPLETED },
{ label: "Rescheduled", color: COLOR.RESCHEDULED },
{ label: "Cancelled", color: COLOR.CANCELLED },
{ label: "No-Show (Host)", color: COLOR.NO_SHOW_HOST },
{ label: "No-Show (Guest)", color: COLOR.NO_SHOW_GUEST },
];
type EventTrendsData = RouterOutputs["viewer"]["insights"]["eventTrends"][number];
// Custom Tooltip component
const CustomTooltip = ({
active,
payload,
label,
}: {
active?: boolean;
payload?: Array<{
value: number;
dataKey: string;
name: string;
color: string;
payload: EventTrendsData;
}>;
label?: string;
}) => {
if (!active || !payload?.length) {
return null;
}
return (
<div className="bg-default text-inverted border-subtle rounded-lg border p-3 shadow-lg">
<p className="text-default font-medium">{payload[0].payload.formattedDateFull}</p>
{payload.map((entry, index: number) => (
<p key={index} style={{ color: entry.color }}>
{entry.name}: {valueFormatter ? valueFormatter(entry.value) : entry.value}
</p>
))}
</div>
);
};
export const EventTrendsChart = () => {
const { t } = useLocale();
const insightsBookingParams = useInsightsBookingParameters();
@@ -30,15 +84,36 @@ export const EventTrendsChart = () => {
if (!isSuccess) return null;
return (
<ChartCard title={t("event_trends")}>
<LineChart
className="linechart ml-4 mt-4 h-80 sm:ml-0"
data={eventTrends ?? []}
categories={["Created", "Completed", "Rescheduled", "Cancelled", "No-Show (Host)", "No-Show (Guest)"]}
index="Month"
colors={["purple", "green", "blue", "red", "slate", "orange"]}
valueFormatter={valueFormatter}
/>
<ChartCard title={t("event_trends")} legend={legend}>
<div className="linechart ml-4 mt-4 h-80 sm:ml-0">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={eventTrends ?? []} margin={{ top: 30, right: 20, left: 0, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" vertical={false} />
<XAxis dataKey="Month" className="text-xs" axisLine={false} tickLine={false} />
<YAxis
allowDecimals={false}
className="text-xs opacity-50"
axisLine={false}
tickLine={false}
tickFormatter={valueFormatter}
/>
<Tooltip content={<CustomTooltip />} />
{legend.map((item) => (
<Line
key={item.label}
type="linear"
dataKey={item.label}
name={item.label}
stroke={item.color}
strokeWidth={2}
dot={{ r: 4 }}
activeDot={{ r: 6 }}
animationDuration={1000}
/>
))}
</LineChart>
</ResponsiveContainer>
</div>
</ChartCard>
);
};
@@ -5,8 +5,8 @@ import { trpc } from "@calcom/trpc";
import { useInsightsBookingParameters } from "../../hooks/useInsightsBookingParameters";
import { ChartCard } from "../ChartCard";
import { FeedbackTable } from "../FeedbackTable";
import { LoadingInsight } from "../LoadingInsights";
import { RecentFeedbackTableContent } from "./RecentFeedbackTableContent";
export const RecentFeedbackTable = () => {
const { t } = useLocale();
@@ -26,7 +26,7 @@ export const RecentFeedbackTable = () => {
return (
<ChartCard title={t("recent_ratings")}>
<FeedbackTable data={data} />
<RecentFeedbackTableContent data={data} />
</ChartCard>
);
};
@@ -2,31 +2,20 @@
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { User } from "@calcom/prisma/client";
import type { RouterOutputs } from "@calcom/trpc/react";
import { Avatar } from "@calcom/ui/components/avatar";
import { Button } from "@calcom/ui/components/button";
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
import { Tooltip } from "@calcom/ui/components/tooltip";
export const FeedbackTable = ({
data,
}: {
data:
| {
userId: number | null;
user: Pick<User, "avatarUrl" | "name">;
emailMd5?: string;
username?: string;
rating: number | null;
feedback: string | null;
}[]
| undefined;
}) => {
type FeedbackData = RouterOutputs["viewer"]["insights"]["recentRatings"];
export const RecentFeedbackTableContent = ({ data }: { data: FeedbackData }) => {
const { t } = useLocale();
return (
<div className="overflow-hidden rounded-md">
{data && data?.length > 0 ? (
data?.map((item) => (
{data && data.length > 0 ? (
data.map((item) => (
<div
key={item.userId}
className="border-subtle flex items-center justify-between border-b px-4 py-3 last:border-b-0">
@@ -1,10 +1,8 @@
export { BookedByCell } from "./BookedByCell";
export { BookingAtCell } from "./BookingAtCell";
export { BookingStatusBadge } from "./BookingStatusBadge";
export { CardInsights } from "./Card";
export { CellWithOverflowX } from "./CellWithOverflowX";
export { ChartCard } from "./ChartCard";
export { FeedbackTable } from "./FeedbackTable";
export { KPICard } from "./KPICard";
export { LineChart } from "./LineChart";
export { LoadingInsight } from "./LoadingInsights";
@@ -1,6 +1,16 @@
"use client";
import { useState } from "react";
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Rectangle,
} from "recharts";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc";
@@ -8,7 +18,39 @@ import { ToggleGroup } from "@calcom/ui/components/form";
import { useInsightsParameters } from "../../hooks/useInsightsParameters";
import { ChartCard } from "../ChartCard";
import { BarList } from "../tremor/BarList";
// Custom Tooltip component
const CustomTooltip = ({
active,
payload,
label,
}: {
active?: boolean;
payload?: Array<{
value: number;
dataKey: string;
name: string;
color: string;
payload: { name: string; value: number };
}>;
label?: string;
}) => {
const { t } = useLocale();
if (!active || !payload?.length) {
return null;
}
return (
<div className="bg-default border-subtle rounded-lg border p-3 shadow-lg">
<p className="text-default font-medium">{label}</p>
{payload.map((entry, index: number) => (
<p key={index}>
{t("failed_bookings")}: {entry.value}
</p>
))}
</div>
);
};
interface FormCardProps {
formName: string;
@@ -16,6 +58,7 @@ interface FormCardProps {
}
function FormCard({ formName, fields }: FormCardProps) {
const { t } = useLocale();
const fieldNames = Object.keys(fields);
const [selectedField, setSelectedField] = useState(fieldNames[0]);
@@ -29,6 +72,17 @@ function FormCard({ formName, fields }: FormCardProps) {
value: option.count,
}));
const maxCount = Math.max(...(selectedFieldData?.map((item) => item.value) || [0]));
const isEmpty = maxCount === 0;
if (isEmpty) {
return (
<div className="text-default flex h-60 text-center">
<p className="m-auto text-sm font-light">{t("insights_no_data_found_for_filter")}</p>
</div>
);
}
return (
<div className="flex flex-col">
<div className="p-4">
@@ -38,13 +92,36 @@ function FormCard({ formName, fields }: FormCardProps) {
className="w-fit"
onValueChange={(value) => value && setSelectedField(value)}
/>
{selectedFieldData && (
<div className="scrollbar-thin mt-4 h-[400px] overflow-y-auto">
<BarList
data={selectedFieldData}
valueFormatter={(value: number) => value.toString()}
className="mt-2"
/>
{selectedFieldData && selectedFieldData.length > 0 ? (
<div className="mt-4 h-80">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={selectedFieldData}
layout="vertical"
margin={{
top: 20,
right: 10,
left: 20,
bottom: 5,
}}
barCategoryGap="10%">
<CartesianGrid strokeDasharray="3 3" horizontal={false} />
<XAxis type="number" className="text-xs" axisLine={false} tickLine={false} />
<YAxis dataKey="name" type="category" className="text-xs" axisLine={false} tickLine={false} />
<Tooltip cursor={false} content={<CustomTooltip />} />
<Bar
dataKey="value"
fill="var(--cal-bg-subtle)"
radius={[0, 2, 2, 0]}
activeBar={<Rectangle fill="var(--cal-bg-info)" />}
/>
</BarChart>
</ResponsiveContainer>
</div>
) : (
<div className="mt-4 p-4 text-center text-gray-500">
No data available for selected field
<div className="mt-2 text-xs">Data: {JSON.stringify(selectedFieldData, null, 2)}</div>
</div>
)}
</div>
@@ -66,8 +143,9 @@ export function FailedBookingsByField() {
// routingFormId is always set, meaning data has only one entry.
const [formName, fields] = Object.entries(data)[0];
return (
<ChartCard title={t("failed_bookings_by_field")} subtitle={formName}>
<ChartCard title={t("failed_bookings_by_field")}>
<FormCard key={formName} formName={formName} fields={fields} />
</ChartCard>
);
@@ -9,6 +9,7 @@ import { downloadAsCsv } from "@calcom/lib/csvUtils";
import { useInViewObserver } from "@calcom/lib/hooks/useInViewObserver";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc";
import type { RouterOutputs } from "@calcom/trpc/react";
import classNames from "@calcom/ui/classNames";
import { Avatar } from "@calcom/ui/components/avatar";
import { Badge } from "@calcom/ui/components/badge";
@@ -165,14 +166,7 @@ function FormCard({
);
}
type RoutedToTableRow = {
id: number;
name: string;
avatarUrl: string | null;
stats: { [key: string]: number };
performance: "above_average" | "at_average" | "below_average" | "median" | "no_data";
totalBookings: number;
};
type RoutedToTableRow = RouterOutputs["viewer"]["insights"]["routedToPerPeriod"]["users"]["data"][number];
const getPerformanceBadge = (performance: RoutedToTableRow["performance"], t: TFunction) => {
switch (performance) {
@@ -292,7 +286,7 @@ export function RoutedToPerPeriod() {
.sort((a, b) => a.getTime() - b.getTime());
}, [data?.pages]);
const processedData: RoutedToTableRow[] = useMemo(() => {
const processedData = useMemo(() => {
if (!data?.pages) return [];
// Create a map for quick lookup of stats
@@ -419,7 +413,7 @@ export function RoutedToPerPeriod() {
<HoverCard>
<HoverCardTrigger asChild>
<div className="flex cursor-pointer items-center gap-2">
<Avatar size="sm" imageSrc={row.avatarUrl} alt={row.name} />
<Avatar size="sm" imageSrc={row.avatarUrl} alt={row.name || ""} />
<div className="flex flex-col gap-1 truncate">
<span>{row.name}</span>
</div>
@@ -38,16 +38,13 @@ export function RoutingFormResponsesTable() {
const { isAll, teamId, userId, memberUserIds, routingFormId, startDate, endDate, columnFilters } =
useInsightsParameters();
const {
data: headers,
isLoading: isHeadersLoading,
isSuccess: isHeadersSuccess,
} = trpc.viewer.insights.routingFormResponsesHeaders.useQuery({
userId,
teamId,
isAll,
routingFormId,
});
const { data: headers, isSuccess: isHeadersSuccess } =
trpc.viewer.insights.routingFormResponsesHeaders.useQuery({
userId,
teamId,
isAll,
routingFormId,
});
const getInsightsFacetedUniqueValues = useInsightsRoutingFacetedUniqueValues({
headers,
@@ -114,10 +111,6 @@ export function RoutingFormResponsesTable() {
}
}, [table, getInsightsFacetedUniqueValues, routingFormId]);
if (isHeadersLoading && !headers) {
return <DataTableSkeleton columns={4} columnWidths={[200, 200, 250, 250]} />;
}
return (
<>
<div className="flex-1">
@@ -1,14 +1,13 @@
"use client";
import { Grid } from "@tremor/react";
import { Flex, Text, Metric } from "@tremor/react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc";
import classNames from "@calcom/ui/classNames";
import { SkeletonText } from "@calcom/ui/components/skeleton";
import { useInsightsParameters } from "../../hooks/useInsightsParameters";
import { valueFormatter } from "../../lib";
import { CardInsights } from "../Card";
import { ChartCard } from "../ChartCard";
export const RoutingKPICards = () => {
const { t } = useLocale();
@@ -61,35 +60,46 @@ export const RoutingKPICards = () => {
}
return (
<>
<Grid numColsSm={1} numColsLg={3} className="mt-4 gap-x-4 gap-y-4">
<ChartCard title={t("stats")}>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
{categories.map((item) => (
<CardInsights key={item.title}>
<Text className="text-default">{item.title}</Text>
<Flex className="items-baseline justify-start space-x-3 truncate">
<Metric className="text-emphasis">{valueFormatter(data[item.index])}</Metric>
</Flex>
</CardInsights>
<div
key={item.title}
className={classNames(
"border-muted border-b p-4 last:border-b-0 sm:border-b-0 sm:border-r sm:last:border-r-0"
)}>
<div className="text-default text-sm">{item.title}</div>
<div className="flex items-baseline justify-start space-x-3 truncate">
<div className="text-emphasis text-2xl font-semibold">{valueFormatter(data[item.index])}</div>
</div>
</div>
))}
</Grid>
</>
</div>
</ChartCard>
);
};
const LoadingKPICards = (props: { categories: { title: string; index: string }[] }) => {
const { t } = useLocale();
const { categories } = props;
return (
<Grid numColsSm={2} numColsLg={4} className="mt-4 gap-x-4 gap-y-4">
{categories.map((item) => (
<CardInsights key={item.title}>
<div className="animate-pulse">
<div className="h-4 w-24 rounded bg-gray-200" />
<div className="mt-4 flex items-baseline space-x-3">
<div className="h-8 w-16 rounded bg-gray-200" />
<ChartCard title={t("stats")}>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
{categories.map((item) => (
<div
key={item.title}
className={classNames(
"border-muted border-b p-4 last:border-b-0 sm:border-b-0 sm:border-r sm:last:border-r-0"
)}>
<div>
<SkeletonText className="mt-1 h-5 w-32" />
</div>
<div>
<SkeletonText className="h-6 w-12" />
</div>
</div>
</CardInsights>
))}
</Grid>
))}
</div>
</ChartCard>
);
};
@@ -1,107 +0,0 @@
.custom-date > .tremor-DateRangePicker-root > .tremor-DateRangePicker-button {
box-shadow: none;
width: 100%;
background-color: transparent;
}
/* Media query for screens larger than 768px */
@media (max-width: 639) {
.custom-date > .tremor-DateRangePicker-root > .tremor-DateRangePicker-button {
max-width: 400px;
}
}
.recharts-cartesian-grid-horizontal line {
@apply stroke-emphasis;
}
.tremor-DateRangePicker-button button {
@apply border-default hover:border-emphasis !h-9 !max-h-9;
}
.tremor-DateRangePicker-calendarButton,
.tremor-DateRangePicker-dropdownButton {
@apply border-subtle bg-default focus-within:ring-emphasis hover:border-subtle dark:focus-within:ring-emphasis hover:bg-subtle text-sm leading-4 placeholder:text-sm placeholder:font-normal focus-within:ring-0;
}
.tremor-DateRangePicker-dropdownModal {
@apply divide-none;
}
.tremor-DropdownItem-root {
@apply bg-default hover:bg-subtle text-default hover:text-emphasis !h-9 !max-h-9;
}
.tremor-DateRangePicker-calendarButtonText,
.tremor-DateRangePicker-dropdownButtonText {
@apply text-default;
}
.tremor-DateRangePicker-calendarHeaderText {
@apply !text-default;
}
.tremor-DateRangePicker-calendarHeader svg {
@apply text-default;
}
.tremor-DateRangePicker-calendarHeader button {
@apply hover:bg-emphasis shadow-none focus:ring-0;
}
.tremor-DateRangePicker-calendarHeader button:hover svg {
@apply text-emphasis;
}
.tremor-DateRangePicker-calendarButtonIcon {
@apply text-default;
}
.tremor-DateRangePicker-calendarModal,
.tremor-DateRangePicker-dropdownModal {
@apply bg-default border-subtle shadow-dropdown;
}
.tremor-DateRangePicker-calendarBodyDate button {
@apply text-default hover:bg-emphasis;
}
.tremor-DateRangePicker-calendarBodyDate button:disabled,
.tremor-DateRangePicker-calendarBodyDate button[disabled] {
@apply opacity-25;
}
.tremor-DateRangePicker-calendarHeader button {
@apply border-default text-default;
}
.tremor-DateRangePicker-calendarBodyDate .bg-gray-100 {
@apply bg-subtle;
}
.tremor-DateRangePicker-calendarBodyDate .bg-gray-500 {
@apply !bg-brand-default text-inverted;
}
.tremor-Card-root {
@apply bg-default p-5;
}
.tremor-TableCell-root {
@apply pl-0;
}
.recharts-responsive-container {
@apply -mx-4;
}
.tremor-Card-root > p {
@apply mb-2 text-base font-semibold;
}
.tremor-Legend-legendItem {
@apply ml-2;
}
.tremor-TableBody-root {
@apply divide-subtle;
}
@@ -1,49 +0,0 @@
type ValueFormatter = {
(value: number): string;
};
type Color =
| "slate"
| "gray"
| "zinc"
| "neutral"
| "stone"
| "red"
| "orange"
| "amber"
| "yellow"
| "lime"
| "green"
| "emerald"
| "teal"
| "cyan"
| "sky"
| "blue"
| "indigo"
| "violet"
| "purple"
| "fuchsia"
| "pink"
| "rose";
interface BaseChartProps extends React.HTMLAttributes<HTMLDivElement> {
data: any[];
categories: string[];
index: string;
colors?: Color[];
valueFormatter?: ValueFormatter;
startEndOnly?: boolean;
showXAxis?: boolean;
showYAxis?: boolean;
yAxisWidth?: number;
showAnimation?: boolean;
showTooltip?: boolean;
showGradient?: boolean;
showLegend?: boolean;
showGridLines?: boolean;
autoMinValue?: boolean;
minValue?: number;
maxValue?: number;
}
export type LineChartProps = BaseChartProps & React.RefAttributes<HTMLDivElement>;
@@ -1,167 +0,0 @@
"use client";
// Tremor BarList [v0.1.1]
import React from "react";
import classNames from "@calcom/ui/classNames";
type Bar<T> = T & {
key?: string;
href?: string;
value: number;
name: string;
};
const focusRing = [
// base
"outline outline-offset-2 outline-0 focus-visible:outline-2",
// outline color
"outline-brand",
];
interface BarListProps<T = unknown> extends React.HTMLAttributes<HTMLDivElement> {
data: Bar<T>[];
valueFormatter?: (value: number) => string;
showAnimation?: boolean;
onValueChange?: (payload: Bar<T>) => void;
sortOrder?: "ascending" | "descending" | "none";
}
function BarListInner<T>(
{
data = [],
valueFormatter = (value) => value.toString(),
showAnimation = false,
onValueChange,
sortOrder = "descending",
className,
...props
}: BarListProps<T>,
forwardedRef: React.ForwardedRef<HTMLDivElement>
) {
const Component = onValueChange ? "button" : "div";
const sortedData = React.useMemo(() => {
if (sortOrder === "none") {
return data;
}
return [...data].sort((a, b) => {
return sortOrder === "ascending" ? a.value - b.value : b.value - a.value;
});
}, [data, sortOrder]);
const widths = React.useMemo(() => {
const maxValue = Math.max(...sortedData.map((item) => item.value), 0);
return sortedData.map((item) => (item.value === 0 ? 0 : Math.max((item.value / maxValue) * 100, 2)));
}, [sortedData]);
const rowHeight = "h-8";
return (
<div
ref={forwardedRef}
className={classNames("flex justify-between space-x-6", className)}
aria-sort={sortOrder}
tremor-id="tremor-raw"
{...props}>
<div className="relative w-full space-y-1.5">
{sortedData.map((item, index) => (
<Component
key={item.key ?? item.name}
onClick={() => {
onValueChange?.(item);
}}
className={classNames(
// base
"group w-full rounded",
// focus
focusRing,
onValueChange
? [
"!-m-0 cursor-pointer",
// hover
"hover:bg-gray-50 hover:dark:bg-gray-900",
]
: ""
)}>
<div
className={classNames(
// base
"flex items-center rounded transition-all",
rowHeight,
// background color
"bg-blue-200 dark:bg-blue-900",
onValueChange ? "group-hover:bg-blue-300 group-hover:dark:bg-blue-800" : "",
// margin and duration
{
"mb-0": index === sortedData.length - 1,
"duration-800": showAnimation,
}
)}
style={{ width: `${widths[index]}%` }}>
<div className={classNames("absolute left-2 flex max-w-full pr-2")}>
{item.href ? (
<a
href={item.href}
className={classNames(
// base
"truncate whitespace-nowrap rounded text-sm",
// text color
"text-default",
// hover
"hover:underline hover:underline-offset-2",
// focus
focusRing
)}
target="_blank"
rel="noreferrer"
onClick={(event) => event.stopPropagation()}>
{item.name}
</a>
) : (
<p
className={classNames(
// base
"truncate whitespace-nowrap text-sm",
// text color
"text-default"
)}>
{item.name}
</p>
)}
</div>
</div>
</Component>
))}
</div>
<div>
{sortedData.map((item, index) => (
<div
key={item.key ?? item.name}
className={classNames(
"flex items-center justify-end",
rowHeight,
index === sortedData.length - 1 ? "mb-0" : "mb-1.5"
)}>
<p
className={classNames(
// base
"truncate whitespace-nowrap text-sm leading-none",
// text color
"text-default"
)}>
{valueFormatter(item.value)}
</p>
</div>
))}
</div>
</div>
);
}
BarListInner.displayName = "BarList";
const BarList = React.forwardRef(BarListInner) as <T>(
p: BarListProps<T> & { ref?: React.ForwardedRef<HTMLDivElement> }
) => ReturnType<typeof BarListInner>;
export { BarList, type BarListProps };
-9
View File
@@ -1,9 +0,0 @@
import type { Color } from "@tremor/react";
export const colors: { [key: string]: Color } = {
increase: "emerald",
moderateIncrease: "emerald",
unchanged: "orange",
moderateDecrease: "rose",
decrease: "rose",
};
-1
View File
@@ -1,3 +1,2 @@
export { calculateDeltaType } from "./calculateDeltaType";
export { colors } from "./colors";
export { valueFormatter } from "./valueFormatter";
+21 -7
View File
@@ -47,6 +47,19 @@ export type SelectedFields<T> = T extends undefined
: never;
};
type UserStatsData = {
userId: number;
user: {
id: number;
username: string | null;
name: string | null;
email: string;
avatarUrl: string;
};
emailMd5: string;
count: number;
}[];
export const bookingDataSchema = z
.object({
id: z.number(),
@@ -682,8 +695,9 @@ export class InsightsBookingService {
});
// Transform aggregate data into the expected format
const result = dateRanges.map(({ formattedDate }) => {
const result = dateRanges.map(({ formattedDate, formattedDateFull }) => {
const eventData = {
formattedDateFull: formattedDateFull,
Month: formattedDate,
Created: 0,
Completed: 0,
@@ -789,7 +803,7 @@ export class InsightsBookingService {
async getMembersStatsWithCount(
type: "all" | "cancelled" | "noShow" = "all",
sortOrder: "ASC" | "DESC" = "DESC"
) {
): Promise<UserStatsData> {
const baseConditions = await this.getBaseConditions();
let additionalCondition = Prisma.sql``;
@@ -857,22 +871,22 @@ export class InsightsBookingService {
return result;
}
async getMembersRatingStats(sortOrder: "ASC" | "DESC" = "DESC") {
async getMembersRatingStats(sortOrder: "ASC" | "DESC" = "DESC"): Promise<UserStatsData> {
const baseConditions = await this.getBaseConditions();
const bookingsFromTeam = await this.prisma.$queryRaw<
Array<{
userId: number;
averageRating: number;
count: number;
}>
>`
SELECT
"userId",
AVG("rating")::float as "averageRating"
AVG("rating")::float as "count"
FROM "BookingTimeStatusDenormalized"
WHERE ${baseConditions} AND "userId" IS NOT NULL AND "rating" IS NOT NULL
GROUP BY "userId"
ORDER BY "averageRating" ${sortOrder === "ASC" ? Prisma.sql`ASC` : Prisma.sql`DESC`}
ORDER BY "count" ${sortOrder === "ASC" ? Prisma.sql`ASC` : Prisma.sql`DESC`}
LIMIT 10
`;
@@ -910,7 +924,7 @@ export class InsightsBookingService {
userId: booking.userId,
user,
emailMd5: md5(user.email),
averageRating: booking.averageRating,
count: booking.count,
};
})
.filter((item): item is NonNullable<typeof item> => item !== null);
+1 -6
View File
@@ -2,12 +2,7 @@ const base = require("@calcom/config/tailwind-preset");
/** @type {import('tailwindcss').Config} */
module.exports = {
...base,
content: [
...base.content,
"../../../packages/ui/!(node_modules)/**/*.{js,ts,jsx,tsx,mdx}",
"../../../node_modules/@tremor/**/*.{js,ts,jsx,tsx}",
"./**/*.tsx",
],
content: [...base.content, "../../../packages/ui/!(node_modules)/**/*.{js,ts,jsx,tsx,mdx}", "./**/*.tsx"],
plugins: [...base.plugins, require("tailwindcss-animate")],
theme: {
...base.theme,
@@ -0,0 +1,38 @@
import type { VariantProps } from "class-variance-authority";
import { cva } from "class-variance-authority";
import classNames from "@calcom/ui/classNames";
const progressBarStyles = cva("h-2 rounded-full", {
variants: {
color: {
green: "bg-green-500",
blue: "bg-blue-500",
red: "bg-red-500",
yellow: "bg-yellow-500",
gray: "bg-gray-500",
},
},
defaultVariants: {
color: "gray",
},
});
export interface ProgressBarProps extends VariantProps<typeof progressBarStyles> {
percentageValue: number;
label?: string;
className?: string;
}
export function ProgressBar({ color, percentageValue, label, className }: ProgressBarProps) {
const clampedPercentage = Math.min(100, Math.max(0, percentageValue));
return (
<div className={classNames("flex w-full items-center gap-3", className)}>
<div className="h-2 flex-1 rounded-full bg-gray-200">
<div className={progressBarStyles({ color })} style={{ width: `${clampedPercentage}%` }} />
</div>
{label && <span className="text-sm font-medium text-gray-700">{label}</span>}
</div>
);
}
@@ -0,0 +1,2 @@
export { ProgressBar } from "./ProgressBar";
export type { ProgressBarProps } from "./ProgressBar";
+1
View File
@@ -43,6 +43,7 @@
"./components/organization-banner": "./components/organization-banner/index.ts",
"./components/pagination": "./components/pagination/index.ts",
"./components/popover": "./components/popover/index.ts",
"./components/progress-bar": "./components/progress-bar/index.ts",
"./components/radio": "./components/radio/index.ts",
"./components/scrollable": "./components/scrollable/index.ts",
"./components/sheet": "./components/sheet/index.ts",
+3 -180
View File
@@ -2096,15 +2096,6 @@ __metadata:
languageName: node
linkType: hard
"@babel/runtime@npm:^7.21.0":
version: 7.26.10
resolution: "@babel/runtime@npm:7.26.10"
dependencies:
regenerator-runtime: ^0.14.0
checksum: 22d2e0abb86e90de489ab16bb578db6fe2b63a88696db431198b24963749820c723f1982298cdbbea187f7b2b80fb4d98a514faf114ddb2fdc14a4b96277b955
languageName: node
linkType: hard
"@babel/runtime@npm:^7.23.2":
version: 7.23.5
resolution: "@babel/runtime@npm:7.23.5"
@@ -4061,7 +4052,6 @@ __metadata:
"@tanstack/react-query": ^5.17.15
"@team-plain/typescript-sdk": ^5.9.0
"@testing-library/react": ^13.3.0
"@tremor/react": ^2.11.0
"@types/accept-language-parser": 1.5.2
"@types/async": ^3.2.15
"@types/bcryptjs": ^2.4.2
@@ -5843,7 +5833,7 @@ __metadata:
languageName: node
linkType: hard
"@floating-ui/dom@npm:^1.0.1, @floating-ui/dom@npm:^1.2.1":
"@floating-ui/dom@npm:^1.0.1":
version: 1.2.6
resolution: "@floating-ui/dom@npm:1.2.6"
dependencies:
@@ -5871,18 +5861,6 @@ __metadata:
languageName: node
linkType: hard
"@floating-ui/react-dom@npm:^1.3.0":
version: 1.3.0
resolution: "@floating-ui/react-dom@npm:1.3.0"
dependencies:
"@floating-ui/dom": ^1.2.1
peerDependencies:
react: ">=16.8.0"
react-dom: ">=16.8.0"
checksum: ce0ad3e3bbe43cfd15a6a0d5cccede02175c845862bfab52027995ab99c6b29630180dc7d146f76ebb34730f90a6ab9bf193c8984fe8d7f56062308e4ca98f77
languageName: node
linkType: hard
"@floating-ui/react-dom@npm:^2.0.0":
version: 2.0.1
resolution: "@floating-ui/react-dom@npm:2.0.1"
@@ -5895,20 +5873,6 @@ __metadata:
languageName: node
linkType: hard
"@floating-ui/react@npm:^0.19.1":
version: 0.19.2
resolution: "@floating-ui/react@npm:0.19.2"
dependencies:
"@floating-ui/react-dom": ^1.3.0
aria-hidden: ^1.1.3
tabbable: ^6.0.1
peerDependencies:
react: ">=16.8.0"
react-dom: ">=16.8.0"
checksum: 00fd827c2dcf879fec221d89ef5b90836bbecacc236ce2acc787db32ae7311d490cd136b13a8d0b6ab12842554a2ee1110605aa832af71a45c0a7297e342072c
languageName: node
linkType: hard
"@floating-ui/utils@npm:^0.2.9":
version: 0.2.9
resolution: "@floating-ui/utils@npm:0.2.9"
@@ -16884,22 +16848,6 @@ __metadata:
languageName: node
linkType: hard
"@tremor/react@npm:^2.11.0":
version: 2.11.0
resolution: "@tremor/react@npm:2.11.0"
dependencies:
"@floating-ui/react": ^0.19.1
date-fns: ^2.28.0
react-transition-group: ^4.4.5
recharts: ^2.3.2
tailwind-merge: ^1.9.1
peerDependencies:
react: ^18.0.0
react-dom: ">=16.6.0"
checksum: 76bee230a819289a5e87204f51161724b49bcc281308b4c41a5918e57e93497e2e97eb85b951bd750b918093784fdb28a2d1060b89502544f1a9cf76232da6d3
languageName: node
linkType: hard
"@trivago/prettier-plugin-sort-imports@npm:4.1.1":
version: 4.1.1
resolution: "@trivago/prettier-plugin-sort-imports@npm:4.1.1"
@@ -20088,15 +20036,6 @@ __metadata:
languageName: node
linkType: hard
"aria-hidden@npm:^1.1.3":
version: 1.2.3
resolution: "aria-hidden@npm:1.2.3"
dependencies:
tslib: ^2.0.0
checksum: 7d7d211629eef315e94ed3b064c6823d13617e609d3f9afab1c2ed86399bb8e90405f9bdd358a85506802766f3ecb468af985c67c846045a34b973bcc0289db9
languageName: node
linkType: hard
"aria-hidden@npm:^1.2.4":
version: 1.2.4
resolution: "aria-hidden@npm:1.2.4"
@@ -23796,15 +23735,6 @@ __metadata:
languageName: node
linkType: hard
"date-fns@npm:^2.28.0":
version: 2.30.0
resolution: "date-fns@npm:2.30.0"
dependencies:
"@babel/runtime": ^7.21.0
checksum: f7be01523282e9bb06c0cd2693d34f245247a29098527d4420628966a2d9aad154bd0e90a6b1cf66d37adcb769cd108cf8a7bd49d76db0fb119af5cdd13644f4
languageName: node
linkType: hard
"date-fns@npm:^3.6.0":
version: 3.6.0
resolution: "date-fns@npm:3.6.0"
@@ -23927,7 +23857,7 @@ __metadata:
languageName: node
linkType: hard
"decimal.js-light@npm:^2.4.1, decimal.js-light@npm:^2.5.1":
"decimal.js-light@npm:^2.5.1":
version: 2.5.1
resolution: "decimal.js-light@npm:2.5.1"
checksum: f5a2c7eac1c4541c8ab8a5c8abea64fc1761cefc7794bd5f8afd57a8a78d1b51785e0c4e4f85f4895a043eaa90ddca1edc3981d1263eb6ddce60f32bf5fe66c9
@@ -26641,7 +26571,7 @@ __metadata:
languageName: node
linkType: hard
"eventemitter3@npm:^4.0.0, eventemitter3@npm:^4.0.1, eventemitter3@npm:^4.0.4":
"eventemitter3@npm:^4.0.0, eventemitter3@npm:^4.0.4":
version: 4.0.7
resolution: "eventemitter3@npm:4.0.7"
checksum: 1875311c42fcfe9c707b2712c32664a245629b42bb0a5a84439762dd0fd637fc54d078155ea83c2af9e0323c9ac13687e03cfba79b03af9f40c89b4960099374
@@ -26900,13 +26830,6 @@ __metadata:
languageName: node
linkType: hard
"fast-equals@npm:^5.0.1":
version: 5.2.2
resolution: "fast-equals@npm:5.2.2"
checksum: 7156bcade0be5ee4dc335969d255a5815348d57080e1876fa1584451eafd0c92588de5f5840e55f81841b6d907ade2a49a46e4ec33e6f7a283a209c0fd8f8a59
languageName: node
linkType: hard
"fast-glob@npm:3.3.1":
version: 3.3.1
resolution: "fast-glob@npm:3.3.1"
@@ -39389,13 +39312,6 @@ __metadata:
languageName: node
linkType: hard
"react-is@npm:^18.3.1":
version: 18.3.1
resolution: "react-is@npm:18.3.1"
checksum: e20fe84c86ff172fc8d898251b7cc2c43645d108bf96d0b8edf39b98f9a2cae97b40520ee7ed8ee0085ccc94736c4886294456033304151c3f94978cec03df21
languageName: node
linkType: hard
"react-live-chat-loader@npm:^2.8.1":
version: 2.8.1
resolution: "react-live-chat-loader@npm:2.8.1"
@@ -39709,20 +39625,6 @@ __metadata:
languageName: node
linkType: hard
"react-smooth@npm:^4.0.4":
version: 4.0.4
resolution: "react-smooth@npm:4.0.4"
dependencies:
fast-equals: ^5.0.1
prop-types: ^15.8.1
react-transition-group: ^4.4.5
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
checksum: 909305d40bae79a011ff21a10c4bc7ddadc87ac5ff093b4a5f827f730f093ec4e044c4330688d29b3ad2db83aab8997c3bb1bae550a9c66de74521b8ed52cc53
languageName: node
linkType: hard
"react-sticky-box@npm:^2.0.4":
version: 2.0.4
resolution: "react-sticky-box@npm:2.0.4"
@@ -39811,21 +39713,6 @@ __metadata:
languageName: node
linkType: hard
"react-transition-group@npm:^4.4.5":
version: 4.4.5
resolution: "react-transition-group@npm:4.4.5"
dependencies:
"@babel/runtime": ^7.5.5
dom-helpers: ^5.0.1
loose-envify: ^1.4.0
prop-types: ^15.6.2
peerDependencies:
react: ">=16.6.0"
react-dom: ">=16.6.0"
checksum: 75602840106aa9c6545149d6d7ae1502fb7b7abadcce70a6954c4b64a438ff1cd16fc77a0a1e5197cdd72da398f39eb929ea06f9005c45b132ed34e056ebdeb1
languageName: node
linkType: hard
"react-turnstile@npm:^1.1.3":
version: 1.1.3
resolution: "react-turnstile@npm:1.1.3"
@@ -40079,34 +39966,6 @@ __metadata:
languageName: node
linkType: hard
"recharts-scale@npm:^0.4.4":
version: 0.4.5
resolution: "recharts-scale@npm:0.4.5"
dependencies:
decimal.js-light: ^2.4.1
checksum: e970377190a610e684a32c7461c7684ac3603c2e0ac0020bbba1eea9d099b38138143a8e80bf769bb49c0b7cecf22a2f5c6854885efed2d56f4540d4aa7052bd
languageName: node
linkType: hard
"recharts@npm:^2.3.2":
version: 2.15.1
resolution: "recharts@npm:2.15.1"
dependencies:
clsx: ^2.0.0
eventemitter3: ^4.0.1
lodash: ^4.17.21
react-is: ^18.3.1
react-smooth: ^4.0.4
recharts-scale: ^0.4.4
tiny-invariant: ^1.3.1
victory-vendor: ^36.6.8
peerDependencies:
react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
checksum: c288012087424e7067bf4a48a3d50a7c0ea231188ecaaf17685d93711ef1846aedd37509dfc25c046052f671e7e19dd2de5d8b9c1e497a64c39995c9c338b40a
languageName: node
linkType: hard
"recharts@npm:^3.0.2":
version: 3.0.2
resolution: "recharts@npm:3.0.2"
@@ -43713,13 +43572,6 @@ __metadata:
languageName: node
linkType: hard
"tabbable@npm:^6.0.1":
version: 6.1.1
resolution: "tabbable@npm:6.1.1"
checksum: 348639497262241ce8e0ccb0664ea582a386183107299ee8f27cf7b56bc84f36e09eaf667d3cb4201e789634012a91f7129bcbd49760abe874fbace35b4cf429
languageName: node
linkType: hard
"tailwind-merge@npm:^1.13.2":
version: 1.13.2
resolution: "tailwind-merge@npm:1.13.2"
@@ -43727,13 +43579,6 @@ __metadata:
languageName: node
linkType: hard
"tailwind-merge@npm:^1.9.1":
version: 1.14.0
resolution: "tailwind-merge@npm:1.14.0"
checksum: 8cf5d37f51b3b32e4bdd5544feaed34357bfba2f64f14834cc4a21ac29b0ae22d7255386467a0f1dadb3e38499389efbbabeddcd0b16571af5a0d346a4921877
languageName: node
linkType: hard
"tailwind-merge@npm:^2.5.4":
version: 2.6.0
resolution: "tailwind-merge@npm:2.6.0"
@@ -46218,28 +46063,6 @@ __metadata:
languageName: node
linkType: hard
"victory-vendor@npm:^36.6.8":
version: 36.9.2
resolution: "victory-vendor@npm:36.9.2"
dependencies:
"@types/d3-array": ^3.0.3
"@types/d3-ease": ^3.0.0
"@types/d3-interpolate": ^3.0.1
"@types/d3-scale": ^4.0.2
"@types/d3-shape": ^3.1.0
"@types/d3-time": ^3.0.0
"@types/d3-timer": ^3.0.0
d3-array: ^3.1.6
d3-ease: ^3.0.1
d3-interpolate: ^3.0.1
d3-scale: ^4.0.2
d3-shape: ^3.1.0
d3-time: ^3.0.0
d3-timer: ^3.0.1
checksum: a755110e287b700202d08ac81982093ab100edaa9d61beef1476d59e9705605bd8299a3aa41fa04b933a12bd66737f4c8f7d18448dd6488c69d4f72480023a2e
languageName: node
linkType: hard
"victory-vendor@npm:^37.0.2":
version: 37.3.6
resolution: "victory-vendor@npm:37.3.6"