add domain filter on dashboard (#161)

This commit is contained in:
KM Koushik
2025-05-10 11:12:09 +10:00
committed by GitHub
parent a47e524f4c
commit be7f030d75
4 changed files with 62 additions and 26 deletions

View File

@@ -7,8 +7,6 @@ import {
Tooltip,
ResponsiveContainer,
CartesianGrid,
AreaChart,
Area,
} from "recharts";
import { EmailStatusIcon } from "../emails/email-status-badge";
import { EmailStatus } from "@prisma/client";
@@ -17,12 +15,29 @@ 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 statusQuery = api.email.dashboard.useQuery({ days: Number(days) });
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",
@@ -45,16 +60,35 @@ export default function DashboardChart() {
<div>
<div className="flex justify-between items-center">
<h1 className="font-semibold text-xl">Dashboard</h1>
<Tabs
value={days || "7"}
onValueChange={(value) => setDays(value)}
className=""
>
<TabsList>
<TabsTrigger value="7">7 Days</TabsTrigger>
<TabsTrigger value="30">30 Days</TabsTrigger>
</TabsList>
</Tabs>
<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">
@@ -159,14 +193,14 @@ export default function DashboardChart() {
if (!data || data.sent === 0) return null;
return (
<div className=" bg-background border shadow-lg p-2 rounded flex flex-col gap-2 px-4">
<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-[60px]">
<p className="text-xs text-muted-foreground w-[70px]">
Delivered
</p>
<p className="text-xs font-mono">
@@ -177,7 +211,7 @@ export default function DashboardChart() {
{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-[60px]">
<p className="text-xs text-muted-foreground w-[70px]">
Bounced
</p>
<p className="text-xs font-mono">{data.bounced}</p>
@@ -186,7 +220,7 @@ export default function DashboardChart() {
{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-[60px]">
<p className="text-xs text-muted-foreground w-[70px]">
Complained
</p>
<p className="text-xs font-mono">
@@ -197,7 +231,7 @@ export default function DashboardChart() {
{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-[60px]">
<p className="text-xs text-muted-foreground w-[70px]">
Opened
</p>
<p className="text-xs font-mono">{data.opened}</p>
@@ -206,7 +240,7 @@ export default function DashboardChart() {
{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-[60px]">
<p className="text-xs text-muted-foreground w-[70px]">
Clicked
</p>
<p className="text-xs font-mono">{data.clicked}</p>

View File

@@ -22,7 +22,7 @@ export const emailRouter = createTRPCRouter({
domain: z.number().optional(),
search: z.string().optional().nullable(),
apiId: z.number().optional(),
}),
})
)
.query(async ({ ctx, input }) => {
const page = input.page || 1;
@@ -65,7 +65,8 @@ export const emailRouter = createTRPCRouter({
.input(
z.object({
days: z.number().optional(),
}),
domain: z.number().optional(),
})
)
.query(async ({ ctx, input }) => {
const { team } = ctx;
@@ -97,6 +98,7 @@ export const emailRouter = createTRPCRouter({
FROM "DailyEmailUsage"
WHERE "teamId" = ${team.id}
AND "date" >= ${isoStartDate}
${input.domain ? Prisma.sql`AND "domainId" = ${input.domain}` : Prisma.sql``}
GROUP BY "date"
ORDER BY "date" ASC
`;
@@ -146,7 +148,7 @@ export const emailRouter = createTRPCRouter({
clicked: 0,
bounced: 0,
complained: 0,
},
}
);
return { result: filledResult, totalCounts };

View File

@@ -65,7 +65,7 @@ const DropdownMenuContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-xl border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
className
)}
{...props}
@@ -83,7 +83,7 @@ const DropdownMenuItem = React.forwardRef<
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
"relative flex cursor-default select-none items-center gap-2 rounded-lg px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}

View File

@@ -75,7 +75,7 @@ const SelectContent = React.forwardRef<
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-lg border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-xl border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
@@ -118,7 +118,7 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"relative flex w-full cursor-default select-none items-center rounded-lg py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}