add domain filter on dashboard (#161)
This commit is contained in:
@@ -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>
|
||||
|
@@ -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 };
|
||||
|
@@ -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
|
||||
)}
|
||||
|
@@ -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}
|
||||
|
Reference in New Issue
Block a user