add analytics (#162)
This commit is contained in:
402
apps/web/src/app/(dashboard)/dashboard/reputation-metrics.tsx
Normal file
402
apps/web/src/app/(dashboard)/dashboard/reputation-metrics.tsx
Normal 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>
|
||||
);
|
||||
};
|
Reference in New Issue
Block a user