add analytics (#162)

This commit is contained in:
KM Koushik
2025-05-11 23:34:21 +10:00
committed by GitHub
parent be7f030d75
commit b5ebd002e5
20 changed files with 1031 additions and 418 deletions

View File

@@ -1,315 +0,0 @@
import React from "react";
import {
BarChart,
Bar,
XAxis,
YAxis,
Tooltip,
ResponsiveContainer,
CartesianGrid,
} from "recharts";
import { EmailStatusIcon } from "../emails/email-status-badge";
import { EmailStatus } from "@prisma/client";
import { api } from "~/trpc/react";
import Spinner from "@unsend/ui/src/spinner";
import { Tabs, TabsList, TabsTrigger } from "@unsend/ui/src/tabs";
import { useUrlState } from "~/hooks/useUrlState";
import { useTheme } from "@unsend/ui";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
} from "@unsend/ui/src/select";
export default function DashboardChart() {
const [days, setDays] = useUrlState("days", "7");
const [domain, setDomain] = useUrlState("domain");
const { resolvedTheme } = useTheme();
const domainId = domain ? Number(domain) : undefined;
const statusQuery = api.email.dashboard.useQuery({
days: Number(days),
domain: domainId,
});
const { data: domainsQuery } = api.domain.domains.useQuery();
const handleDomain = (val: string) => {
setDomain(val === "All Domain" ? null : val);
};
const lightColors = {
delivered: "#40a02bcc",
bounced: "#d20f39cc",
complained: "#df8e1dcc",
opened: "#8839efcc",
clicked: "#04a5e5cc",
};
const darkColors = {
delivered: "#a6e3a1",
bounced: "#f38ba8",
complained: "#F9E2AF",
opened: "#cba6f7",
clicked: "#93c5fd",
};
const currentColors = resolvedTheme === "dark" ? darkColors : lightColors;
return (
<div>
<div className="flex justify-between items-center">
<h1 className="font-semibold text-xl">Dashboard</h1>
<div className="flex gap-3">
<Select
value={domain ?? "All Domain"}
onValueChange={(val) => handleDomain(val)}
>
<SelectTrigger className="w-[180px]">
{domain
? domainsQuery?.find((d) => d.id === Number(domain))?.name
: "All Domain"}
</SelectTrigger>
<SelectContent>
<SelectItem value="All Domain" className="capitalize">
All Domain
</SelectItem>
{domainsQuery &&
domainsQuery.map((domain) => (
<SelectItem key={domain.id} value={domain.id.toString()}>
{domain.name}
</SelectItem>
))}
</SelectContent>
</Select>
<Tabs value={days || "7"} onValueChange={(value) => setDays(value)}>
<TabsList>
<TabsTrigger value="7">7 Days</TabsTrigger>
<TabsTrigger value="30">30 Days</TabsTrigger>
</TabsList>
</Tabs>
</div>
</div>
<div className="flex flex-col gap-16 mt-10">
<div className="flex flex-wrap gap-2">
{!statusQuery.isLoading && statusQuery.data ? (
<>
<DashboardItemCard
status={"total"}
count={statusQuery.data.totalCounts.sent}
percentage={100}
/>
<DashboardItemCard
status={EmailStatus.DELIVERED}
count={statusQuery.data.totalCounts.delivered}
percentage={
statusQuery.data.totalCounts.delivered /
statusQuery.data.totalCounts.sent
}
/>
<DashboardItemCard
status={EmailStatus.BOUNCED}
count={statusQuery.data.totalCounts.bounced}
percentage={
statusQuery.data.totalCounts.bounced /
statusQuery.data.totalCounts.sent
}
/>
<DashboardItemCard
status={EmailStatus.COMPLAINED}
count={statusQuery.data.totalCounts.complained}
percentage={
statusQuery.data.totalCounts.complained /
statusQuery.data.totalCounts.sent
}
/>
<DashboardItemCard
status={EmailStatus.CLICKED}
count={statusQuery.data.totalCounts.clicked}
percentage={
statusQuery.data.totalCounts.clicked /
statusQuery.data.totalCounts.sent
}
/>
<DashboardItemCard
status={EmailStatus.OPENED}
count={statusQuery.data.totalCounts.opened}
percentage={
statusQuery.data.totalCounts.opened /
statusQuery.data.totalCounts.sent
}
/>
</>
) : (
<>
<div className="h-[100px] w-[1/5] min-w-[200px] bg-secondary/10 border rounded-lg p-4 flex justify-center items-center gap-3 ">
<Spinner className="w-4 h-4" innerSvgClass="stroke-primary" />
</div>
<div className="h-[100px] w-[1/5] min-w-[200px] bg-secondary/10 border rounded-lg p-4 flex justify-center items-center gap-3 ">
<Spinner className="w-4 h-4" innerSvgClass="stroke-primary" />
</div>
<div className="h-[100px] w-[1/5] min-w-[200px] bg-secondary/10 border rounded-lg p-4 flex justify-center items-center gap-3 ">
<Spinner className="w-4 h-4" innerSvgClass="stroke-primary" />
</div>
<div className="h-[100px] w-[1/5] min-w-[200px] bg-secondary/10 border rounded-lg p-4 flex justify-center items-center gap-3 ">
<Spinner className="w-4 h-4" innerSvgClass="stroke-primary" />
</div>
<div className="h-[100px] w-[1/5] min-w-[200px] bg-secondary/10 border rounded-lg p-4 flex justify-center items-center gap-3 ">
<Spinner className="w-4 h-4" innerSvgClass="stroke-primary" />
</div>
</>
)}
</div>
{!statusQuery.isLoading && statusQuery.data ? (
<div className="w-full h-[400px] border shadow rounded-lg p-4">
<ResponsiveContainer width="100%" height="100%">
<BarChart
width={900}
height={300}
data={statusQuery.data.result}
margin={{
top: 20,
right: 30,
left: 20,
bottom: 5,
}}
>
<CartesianGrid vertical={false} strokeDasharray="3 3" />
<XAxis dataKey="date" fontSize={12} />
<YAxis fontSize={12} />
<Tooltip
content={({ payload }) => {
const data = payload?.[0]?.payload as Record<
| "sent"
| "delivered"
| "opened"
| "clicked"
| "bounced"
| "complained",
number
> & { date: string };
if (!data || data.sent === 0) return null;
return (
<div className=" bg-background border shadow-lg p-2 rounded-xl flex flex-col gap-2 px-4">
<p className="text-sm text-muted-foreground">
{data.date}
</p>
{data.delivered ? (
<div className="flex gap-2 items-center">
<div className="w-2.5 h-2.5 bg-[#40a02bcc] dark:bg-[#a6e3a1] rounded-[2px]"></div>
<p className="text-xs text-muted-foreground w-[70px]">
Delivered
</p>
<p className="text-xs font-mono">
{data.delivered}
</p>
</div>
) : null}
{data.bounced ? (
<div className="flex gap-2 items-center">
<div className="w-2.5 h-2.5 bg-[#d20f39cc] dark:bg-[#f38ba8] rounded-[2px]"></div>
<p className="text-xs text-muted-foreground w-[70px]">
Bounced
</p>
<p className="text-xs font-mono">{data.bounced}</p>
</div>
) : null}
{data.complained ? (
<div className="flex gap-2 items-center">
<div className="w-2.5 h-2.5 bg-[#df8e1dcc] dark:bg-[#F9E2AF] rounded-[2px]"></div>
<p className="text-xs text-muted-foreground w-[70px]">
Complained
</p>
<p className="text-xs font-mono">
{data.complained}
</p>
</div>
) : null}
{data.opened ? (
<div className="flex gap-2 items-center">
<div className="w-2.5 h-2.5 bg-[#8839efcc] dark:bg-[#cba6f7] rounded-[2px]"></div>
<p className="text-xs text-muted-foreground w-[70px]">
Opened
</p>
<p className="text-xs font-mono">{data.opened}</p>
</div>
) : null}
{data.clicked ? (
<div className="flex gap-2 items-center">
<div className="w-2.5 h-2.5 bg-[#04a5e5cc] dark:bg-[#93c5fd] rounded-[2px]"></div>
<p className="text-xs text-muted-foreground w-[70px]">
Clicked
</p>
<p className="text-xs font-mono">{data.clicked}</p>
</div>
) : null}
</div>
);
}}
cursor={false}
/>
{/* <Legend /> */}
<Bar
barSize={8}
dataKey="delivered"
stackId="a"
fill={currentColors.delivered}
/>
<Bar
dataKey="bounced"
stackId="a"
fill={currentColors.bounced}
/>
<Bar
dataKey="complained"
stackId="a"
fill={currentColors.complained}
/>
<Bar dataKey="opened" stackId="a" fill={currentColors.opened} />
<Bar
dataKey="clicked"
stackId="a"
fill={currentColors.clicked}
/>
</BarChart>
</ResponsiveContainer>
</div>
) : null}
</div>
</div>
);
}
type DashboardItemCardProps = {
status: EmailStatus | "total";
count: number;
percentage: number;
};
const DashboardItemCard: React.FC<DashboardItemCardProps> = ({
status,
count,
percentage,
}) => {
return (
<div className="h-[100px] w-[16%] min-w-[170px] bg-secondary/10 border shadow rounded-lg p-4 flex flex-col gap-3">
<div className="flex items-center gap-3">
{status !== "total" ? <EmailStatusIcon status={status} /> : null}
<div className=" capitalize">{status.toLowerCase()}</div>
</div>
<div className="flex justify-between items-end">
<div className="text-foreground font-light text-2xl font-mono">
{count}
</div>
{status !== "total" ? (
<div className="text-sm pb-1">
{count > 0 ? (percentage * 100).toFixed(0) : 0}%
</div>
) : null}
</div>
</div>
);
};

View File

@@ -0,0 +1,62 @@
import React from "react";
import { Tabs, TabsList, TabsTrigger } from "@unsend/ui/src/tabs";
import { useUrlState } from "~/hooks/useUrlState";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
} from "@unsend/ui/src/select";
import { api } from "~/trpc/react";
interface DashboardFiltersProps {
days: string;
setDays: (days: string) => void;
domain: string | null;
setDomain: (domain: string | null) => void;
}
export default function DashboardFilters({
days,
setDays,
domain,
setDomain,
}: DashboardFiltersProps) {
const { data: domainsQuery } = api.domain.domains.useQuery();
const handleDomain = (val: string) => {
setDomain(val === "All Domain" ? null : val);
};
return (
<div className="flex gap-3">
<Select
value={domain ?? "All Domain"}
onValueChange={(val) => handleDomain(val)}
>
<SelectTrigger className="w-[180px]">
{domain
? domainsQuery?.find((d) => d.id === Number(domain))?.name
: "All Domain"}
</SelectTrigger>
<SelectContent>
<SelectItem value="All Domain" className="capitalize">
All Domain
</SelectItem>
{domainsQuery &&
domainsQuery.map((domain) => (
<SelectItem key={domain.id} value={domain.id.toString()}>
{domain.name}
</SelectItem>
))}
</SelectContent>
</Select>
<Tabs value={days || "7"} onValueChange={(value) => setDays(value)}>
<TabsList>
<TabsTrigger value="7">7 Days</TabsTrigger>
<TabsTrigger value="30">30 Days</TabsTrigger>
</TabsList>
</Tabs>
</div>
);
}

View File

@@ -0,0 +1,265 @@
import React from "react";
import {
BarChart,
Bar,
XAxis,
YAxis,
Tooltip,
ResponsiveContainer,
CartesianGrid,
AreaChart,
Area,
} from "recharts";
import { EmailStatusIcon } from "../emails/email-status-badge";
import { EmailStatus } from "@prisma/client";
import { api } from "~/trpc/react";
import Spinner from "@unsend/ui/src/spinner";
import { useTheme } from "@unsend/ui";
import { EMAIL_COLORS } from "~/lib/constants/colors";
import { useColors } from "./hooks/useColors";
interface EmailChartProps {
days: number;
domain: string | null;
}
export default function EmailChart({ days, domain }: EmailChartProps) {
const domainId = domain ? Number(domain) : undefined;
const statusQuery = api.dashboard.emailTimeSeries.useQuery({
days: days,
domain: domainId,
});
const currentColors = useColors();
return (
<div className="flex flex-col gap-16">
{!statusQuery.isLoading && statusQuery.data ? (
<div className="w-full h-[450px] border shadow rounded-xl p-4">
<div>
{/* <div className="mb-4 text-sm">Emails</div> */}
<div className="flex gap-10">
<EmailChartItem
status={"total"}
count={statusQuery.data.totalCounts.sent}
percentage={100}
/>
<EmailChartItem
status={EmailStatus.DELIVERED}
count={statusQuery.data.totalCounts.delivered}
percentage={
statusQuery.data.totalCounts.delivered /
statusQuery.data.totalCounts.sent
}
/>
<EmailChartItem
status={EmailStatus.BOUNCED}
count={statusQuery.data.totalCounts.bounced}
percentage={
statusQuery.data.totalCounts.bounced /
statusQuery.data.totalCounts.sent
}
/>
<EmailChartItem
status={EmailStatus.COMPLAINED}
count={statusQuery.data.totalCounts.complained}
percentage={
statusQuery.data.totalCounts.complained /
statusQuery.data.totalCounts.sent
}
/>
<EmailChartItem
status={EmailStatus.CLICKED}
count={statusQuery.data.totalCounts.clicked}
percentage={
statusQuery.data.totalCounts.clicked /
statusQuery.data.totalCounts.sent
}
/>
<EmailChartItem
status={EmailStatus.OPENED}
count={statusQuery.data.totalCounts.opened}
percentage={
statusQuery.data.totalCounts.opened /
statusQuery.data.totalCounts.sent
}
/>
</div>
</div>
<ResponsiveContainer width="100%" height="80%">
<BarChart
width={900}
height={200}
data={statusQuery.data.result}
margin={{
top: 20,
right: 30,
left: 20,
bottom: 5,
}}
>
<XAxis
dataKey="date"
fontSize={12}
className="font-mono"
stroke={currentColors.xaxis}
/>
{/* <YAxis fontSize={12} className="font-mono" /> */}
<Tooltip
content={({ payload }) => {
const data = payload?.[0]?.payload as Record<
| "sent"
| "delivered"
| "opened"
| "clicked"
| "bounced"
| "complained",
number
> & { date: string };
if (!data || data.sent === 0) return null;
return (
<div className=" bg-background border shadow-lg p-2 rounded-xl flex flex-col gap-2 px-4">
<p className="text-sm text-muted-foreground">
{data.date}
</p>
{data.delivered ? (
<div className="flex gap-2 items-center">
<div className="w-2.5 h-2.5 bg-[#40a02bcc] dark:bg-[#a6e3a1] rounded-[2px]"></div>
<p className="text-xs text-muted-foreground w-[70px]">
Delivered
</p>
<p className="text-xs font-mono">{data.delivered}</p>
</div>
) : null}
{data.bounced ? (
<div className="flex gap-2 items-center">
<div className="w-2.5 h-2.5 bg-[#d20f39cc] dark:bg-[#f38ba8] rounded-[2px]"></div>
<p className="text-xs text-muted-foreground w-[70px]">
Bounced
</p>
<p className="text-xs font-mono">{data.bounced}</p>
</div>
) : null}
{data.complained ? (
<div className="flex gap-2 items-center">
<div className="w-2.5 h-2.5 bg-[#df8e1dcc] dark:bg-[#F9E2AF] rounded-[2px]"></div>
<p className="text-xs text-muted-foreground w-[70px]">
Complained
</p>
<p className="text-xs font-mono">{data.complained}</p>
</div>
) : null}
{data.opened ? (
<div className="flex gap-2 items-center">
<div className="w-2.5 h-2.5 bg-[#8839efcc] dark:bg-[#cba6f7] rounded-[2px]"></div>
<p className="text-xs text-muted-foreground w-[70px]">
Opened
</p>
<p className="text-xs font-mono">{data.opened}</p>
</div>
) : null}
{data.clicked ? (
<div className="flex gap-2 items-center">
<div className="w-2.5 h-2.5 bg-[#04a5e5cc] dark:bg-[#93c5fd] rounded-[2px]"></div>
<p className="text-xs text-muted-foreground w-[70px]">
Clicked
</p>
<p className="text-xs font-mono">{data.clicked}</p>
</div>
) : null}
</div>
);
}}
cursor={false}
/>
{/* <Legend /> */}
<Bar
barSize={8}
dataKey="delivered"
stackId="a"
fill={currentColors.delivered}
/>
<Bar dataKey="bounced" stackId="a" fill={currentColors.bounced} />
<Bar
dataKey="complained"
stackId="a"
fill={currentColors.complained}
/>
<Bar dataKey="opened" stackId="a" fill={currentColors.opened} />
<Bar dataKey="clicked" stackId="a" fill={currentColors.clicked} />
</BarChart>
</ResponsiveContainer>
</div>
) : (
<div className="h-[450px]"> </div>
)}
</div>
);
}
type DashboardItemCardProps = {
status: EmailStatus | "total";
count: number;
percentage: number;
};
const DashboardItemCard: React.FC<DashboardItemCardProps> = ({
status,
count,
percentage,
}) => {
return (
<div className="h-[100px] w-[16%] min-w-[170px] bg-secondary/10 border shadow rounded-xl p-4 flex flex-col gap-3">
<div className="flex items-center gap-3">
{status !== "total" ? <EmailStatusIcon status={status} /> : null}
<div className=" capitalize">{status.toLowerCase()}</div>
</div>
<div className="flex justify-between items-end">
<div className="text-foreground font-light text-2xl font-mono">
{count}
</div>
{status !== "total" ? (
<div className="text-sm pb-1">
{count > 0 ? (percentage * 100).toFixed(0) : 0}%
</div>
) : null}
</div>
</div>
);
};
const EmailChartItem: React.FC<DashboardItemCardProps> = ({
status,
count,
percentage,
}) => {
const color = EMAIL_COLORS[status];
return (
<div className="flex gap-3 items-stretch font-mono">
{/* <div className={`${color} w-0.5 rounded-full`}></div> */}
<div>
<div className=" flex items-center gap-2">
<div className={`${color} w-2.5 h-2.5 rounded-[3px]`}></div>
{/* <div className={`${color} w-0.5 rounded-full`}></div> */}
<div className="text-xs uppercase text-muted-foreground ">
{status.toLowerCase()}
</div>
</div>
<div className="mt-1 -ml-0.5 ">
<span className="text-xl font-mono">{count}</span>
<span className="text-xs ml-2 font-mono">
{status !== "total"
? `(${count > 0 ? (percentage * 100).toFixed(0) : 0}%)`
: null}
</span>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,27 @@
import { useTheme } from "@unsend/ui";
export function useColors() {
const { resolvedTheme } = useTheme();
const lightColors = {
delivered: "#40a02b",
bounced: "#d20f39",
complained: "#df8e1d",
opened: "#8839ef",
clicked: "#04a5e5",
xaxis: "#6D6F84",
};
const darkColors = {
delivered: "#a6e3a1",
bounced: "#f38ba8",
complained: "#F9E2AF",
opened: "#cba6f7",
clicked: "#93c5fd",
xaxis: "#AAB1CD",
};
const currentColors = resolvedTheme === "dark" ? darkColors : lightColors;
return currentColors;
}

View File

@@ -1,12 +1,31 @@
"use client";
import DashboardChart from "./dashboard-chart";
import EmailChart from "./email-chart";
import DashboardFilters from "./dashboard-filters";
import { useUrlState } from "~/hooks/useUrlState";
import { ReputationMetrics } from "./reputation-metrics";
export default function Dashboard() {
const [days, setDays] = useUrlState("days", "7");
const [domain, setDomain] = useUrlState("domain");
return (
<div>
<div className="w-full ">
<DashboardChart />
<div className="w-full">
<div className="flex justify-between items-center mb-10">
<h1 className="font-semibold text-xl">Analytics</h1>
<DashboardFilters
days={days ?? "7"}
setDays={setDays}
domain={domain}
setDomain={setDomain}
/>
</div>
<div className=" space-y-12">
<EmailChart days={Number(days ?? "7")} domain={domain} />
<ReputationMetrics days={Number(days ?? "7")} domain={domain} />
</div>
</div>
</div>
);

View File

@@ -0,0 +1,402 @@
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@unsend/ui/src/tooltip";
import {
CheckCircle2,
CheckCircle2Icon,
InfoIcon,
OctagonAlertIcon,
TriangleAlertIcon,
} from "lucide-react";
import {
Bar,
BarChart,
ReferenceLine,
ResponsiveContainer,
Tooltip as RechartsTooltip,
CartesianGrid,
YAxis,
} from "recharts";
import {
HARD_BOUNCE_RISK_RATE,
HARD_BOUNCE_WARNING_RATE,
COMPLAINED_WARNING_RATE,
COMPLAINED_RISK_RATE,
} from "~/lib/constants";
import { api } from "~/trpc/react";
import { useColors } from "./hooks/useColors";
interface ReputationMetricsProps {
days: number;
domain: string | null;
}
enum ACCOUNT_STATUS {
HEALTHY = "HEALTHY",
WARNING = "WARNING",
RISK = "RISK",
}
const CustomLabel = ({ value, stroke }: { value: string; stroke: string }) => {
return (
<text x={0} y={-5} fill={stroke} fontSize={12} textAnchor="start">
{value}
</text>
);
};
export function ReputationMetrics({ days, domain }: ReputationMetricsProps) {
const { data: metrics, isLoading } =
api.dashboard.reputationMetricsData.useQuery({
domain: domain ? Number(domain) : undefined,
});
const colors = useColors();
const bouncedMetric = metrics
? [
{
name: "Bounce Rate",
value: metrics.bounceRate,
},
]
: [];
const complaintMetric = metrics
? [
{
name: "Complaint Rate",
value: metrics.complaintRate,
},
]
: [];
const bounceStatus =
(metrics?.bounceRate ?? 0) > HARD_BOUNCE_RISK_RATE
? ACCOUNT_STATUS.RISK
: (metrics?.bounceRate ?? 0) > HARD_BOUNCE_WARNING_RATE
? ACCOUNT_STATUS.WARNING
: ACCOUNT_STATUS.HEALTHY;
const complaintStatus =
(metrics?.complaintRate ?? 0) > COMPLAINED_RISK_RATE
? ACCOUNT_STATUS.RISK
: (metrics?.complaintRate ?? 0) > COMPLAINED_WARNING_RATE
? ACCOUNT_STATUS.WARNING
: ACCOUNT_STATUS.HEALTHY;
return (
<TooltipProvider>
<div className="flex gap-10 w-full">
<div className="w-1/2 border rounded-xl shadow p-4">
<div className="flex justify-between">
<div className=" flex items-center gap-2">
<div className="text-muted-foreground">Bounce Rate</div>
<Tooltip>
<TooltipTrigger>
<InfoIcon className=" h-3.5 w-3.5 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent className="w-[300px]">
The percentage of emails sent from your account that resulted
in a hard bounce.
</TooltipContent>
</Tooltip>
</div>
<div></div>
</div>
<div className="flex items-baseline gap-4">
<div className="text-2xl mt-2">{metrics?.bounceRate}%</div>
<StatusBadge status={bounceStatus} />
</div>
{/* <div className="flex">
<StatusBadge status={ACCOUNT_STATUS.HEALTHY} />
</div> */}
<ResponsiveContainer width="100%" height={200}>
<BarChart
// width={350}
// height={100}
data={bouncedMetric}
margin={{
top: 20,
// right: 30,
left: -30,
bottom: 5,
}}
>
<YAxis
domain={[0, 15]}
ticks={[0, 5, 10, 15]}
fontSize={12}
tickFormatter={(value) => `${value}%`}
hide={false}
axisLine={false}
tickLine={false}
interval={0}
/>
<CartesianGrid
vertical={false}
strokeDasharray="3 3"
stroke={`${colors.xaxis}50`}
/>
<ReferenceLine
y={HARD_BOUNCE_WARNING_RATE}
stroke={`${colors.complained}A0`}
label={{
value: "",
position: "insideBottomLeft",
fill: colors.complained,
fontSize: 12,
}}
strokeDasharray="3 3"
/>
{/* <CartesianGrid vertical={false} strokeDasharray="3 3" /> */}
{/* <YAxis fontSize={12} /> */}
{/* <ReferenceLine
y={0}
stroke={colors.xaxis}
strokeDasharray="3 3"
/> */}
<ReferenceLine
y={HARD_BOUNCE_RISK_RATE}
stroke={`${colors.bounced}A0`}
label={{
value: ``,
position: "insideBottomLeft",
fill: colors.bounced,
fontSize: 12,
}}
strokeDasharray="3 3"
/>
<RechartsTooltip
content={({ payload }) => {
const data = payload?.[0]?.payload as {
name: string;
value: number;
};
if (!data) return null;
return (
<div className="bg-background border shadow-lg p-2 rounded-xl flex flex-col gap-2 px-4">
<p className="text-sm text-muted-foreground">
{data.name}
</p>
<div className="flex gap-2 items-center">
<div
className="w-2.5 h-2.5 rounded-[2px]"
style={{ background: colors.clicked }}
></div>
<p className="text-xs text-muted-foreground w-[70px]">
Current
</p>
<p className="text-xs font-mono">{data.value}%</p>
</div>
<div className="flex gap-2 items-center">
<div
className="w-2.5 h-2.5 rounded-[2px]"
style={{ background: colors.complained }}
></div>
<p className="text-xs text-muted-foreground w-[70px]">
Warning at
</p>
<p className="text-xs font-mono">
{HARD_BOUNCE_WARNING_RATE}%
</p>
</div>
<div className="flex gap-2 items-center">
<div
className="w-2.5 h-2.5 rounded-[2px]"
style={{ background: colors.bounced }}
></div>
<p className="text-xs text-muted-foreground w-[70px]">
Risk at
</p>
<p className="text-xs font-mono">
{HARD_BOUNCE_RISK_RATE}%
</p>
</div>
</div>
);
}}
cursor={false}
/>
<Bar
barSize={150}
dataKey="value"
stackId="a"
fill={colors.clicked}
radius={[8, 8, 0, 0]}
/>
</BarChart>
</ResponsiveContainer>
</div>
<div className="w-1/2 border rounded-xl shadow p-4">
<div className=" flex items-center gap-2">
<div className=" text-muted-foreground">Complaint Rate</div>
<Tooltip>
<TooltipTrigger>
<InfoIcon className=" h-3.5 w-3.5 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent className="w-[300px]">
The percentage of emails sent from your account that resulted in
recipients reporting them as spam.
</TooltipContent>
</Tooltip>
</div>
<div className="flex items-baseline gap-4">
<div className="text-2xl mt-2">{metrics?.complaintRate}%</div>
<StatusBadge status={complaintStatus} />
</div>
<ResponsiveContainer width="100%" height={200}>
<BarChart
data={complaintMetric}
margin={{
top: 20,
left: -30,
bottom: 5,
}}
>
<YAxis
domain={[0, 0.8]}
ticks={[0, 0.2, 0.4, 0.6, 0.8]}
fontSize={12}
tickFormatter={(value) => `${value}%`}
hide={false}
axisLine={false}
interval={0}
/>
<CartesianGrid
vertical={false}
strokeDasharray="3 3"
stroke={`${colors.xaxis}50`}
/>
<ReferenceLine
y={COMPLAINED_WARNING_RATE}
stroke={`${colors.complained}A0`}
label={{
value: "",
position: "insideBottomLeft",
fill: colors.complained,
fontSize: 12,
}}
strokeDasharray="3 3"
/>
{/* <ReferenceLine
y={0}
stroke={colors.xaxis}
strokeDasharray="3 3"
/> */}
<ReferenceLine
y={COMPLAINED_RISK_RATE}
stroke={`${colors.bounced}A0`}
label={{
value: ``,
position: "insideBottomLeft",
fill: colors.bounced,
fontSize: 12,
}}
strokeDasharray="3 3"
/>
<RechartsTooltip
content={({ payload }) => {
const data = payload?.[0]?.payload as {
name: string;
value: number;
};
if (!data) return null;
return (
<div className="bg-background border shadow-lg p-2 rounded-xl flex flex-col gap-2 px-4">
<p className="text-sm text-muted-foreground">
{data.name}
</p>
<div className="flex gap-2 items-center">
<div
className="w-2.5 h-2.5 rounded-[2px]"
style={{ background: colors.clicked }}
></div>
<p className="text-xs text-muted-foreground w-[70px]">
Current
</p>
<p className="text-xs font-mono">{data.value}%</p>
</div>
<div className="flex gap-2 items-center">
<div
className="w-2.5 h-2.5 rounded-[2px]"
style={{ background: colors.complained }}
></div>
<p className="text-xs text-muted-foreground w-[70px]">
Warning at
</p>
<p className="text-xs font-mono">
{COMPLAINED_WARNING_RATE}%
</p>
</div>
<div className="flex gap-2 items-center">
<div
className="w-2.5 h-2.5 rounded-[2px]"
style={{ background: colors.bounced }}
></div>
<p className="text-xs text-muted-foreground w-[70px]">
Risk at
</p>
<p className="text-xs font-mono">
{COMPLAINED_RISK_RATE}%
</p>
</div>
</div>
);
}}
cursor={false}
/>
<Bar
barSize={150}
dataKey="value"
stackId="a"
fill={colors.clicked}
radius={[8, 8, 0, 0]}
/>
</BarChart>
</ResponsiveContainer>
</div>
</div>
</TooltipProvider>
);
}
export const StatusBadge: React.FC<{ status: ACCOUNT_STATUS }> = ({
status,
}) => {
const className =
status === "HEALTHY"
? " text-success border-success"
: status === "WARNING"
? " text-warning border-warning"
: " text-destructive border-destructive";
const StatusIcon =
status === "HEALTHY"
? CheckCircle2Icon
: status === "WARNING"
? TriangleAlertIcon
: OctagonAlertIcon;
return (
<div
className={` capitalize text-xs ${className} flex gap-1 items-center rounded-lg`}
>
<StatusIcon className="h-3.5 w-3.5" />
{status.toLowerCase()}
</div>
);
};