Add click and open tracking

This commit is contained in:
KMKoushik
2024-04-10 08:35:03 +10:00
parent dab3d7ad25
commit ffad4050de
7 changed files with 156 additions and 47 deletions

View File

@@ -1,10 +1,11 @@
"use client"; "use client";
import { DomainStatus } from "@prisma/client"; import { Domain, DomainStatus } from "@prisma/client";
import { formatDistanceToNow } from "date-fns"; import { formatDistanceToNow } from "date-fns";
import Link from "next/link"; import Link from "next/link";
import { Switch } from "@unsend/ui/src/switch"; import { Switch } from "@unsend/ui/src/switch";
import { api } from "~/trpc/react"; import { api } from "~/trpc/react";
import React from "react";
export default function DomainsList() { export default function DomainsList() {
const domainsQuery = api.domain.domains.useQuery(); const domainsQuery = api.domain.domains.useQuery();
@@ -14,6 +15,50 @@ export default function DomainsList() {
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
{!domainsQuery.isLoading && domainsQuery.data?.length ? ( {!domainsQuery.isLoading && domainsQuery.data?.length ? (
domainsQuery.data?.map((domain) => ( domainsQuery.data?.map((domain) => (
<DomainItem key={domain.id} domain={domain} />
))
) : (
<div>No domains</div>
)}
</div>
</div>
);
}
const DomainItem: React.FC<{ domain: Domain }> = ({ domain }) => {
const updateDomain = api.domain.updateDomain.useMutation();
const utils = api.useUtils();
const [clickTracking, setClickTracking] = React.useState(
domain.clickTracking
);
const [openTracking, setOpenTracking] = React.useState(domain.openTracking);
function handleClickTrackingChange() {
setClickTracking(!clickTracking);
updateDomain.mutate(
{ id: domain.id, clickTracking: !clickTracking },
{
onSuccess: () => {
utils.domain.domains.invalidate();
},
}
);
}
function handleOpenTrackingChange() {
setOpenTracking(!openTracking);
updateDomain.mutate(
{ id: domain.id, openTracking: !openTracking },
{
onSuccess: () => {
utils.domain.domains.invalidate();
},
}
);
}
return (
<div key={domain.id}> <div key={domain.id}>
<div className=" pr-8 border rounded-lg flex items-stretch"> <div className=" pr-8 border rounded-lg flex items-stretch">
<StatusIndicator status={domain.status} /> <StatusIndicator status={domain.status} />
@@ -29,9 +74,7 @@ export default function DomainsList() {
</div> </div>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div> <div>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">Created at</p>
Created at
</p>
<p className="text-sm"> <p className="text-sm">
{formatDistanceToNow(new Date(domain.createdAt), { {formatDistanceToNow(new Date(domain.createdAt), {
addSuffix: true, addSuffix: true,
@@ -49,24 +92,26 @@ export default function DomainsList() {
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<p className="text-sm">Click tracking</p> <p className="text-sm">Click tracking</p>
<Switch className="data-[state=checked]:bg-emerald-500" /> <Switch
checked={clickTracking}
onCheckedChange={handleClickTrackingChange}
className="data-[state=checked]:bg-emerald-500"
/>
</div> </div>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<p className="text-sm">Open tracking</p> <p className="text-sm">Open tracking</p>
<Switch className="data-[state=checked]:bg-emerald-500" /> <Switch
checked={openTracking}
onCheckedChange={handleOpenTrackingChange}
className="data-[state=checked]:bg-emerald-500"
/>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
))
) : (
<div>No domains</div>
)}
</div>
</div>
); );
} };
const DomainStatusBadge: React.FC<{ status: DomainStatus }> = ({ status }) => { const DomainStatusBadge: React.FC<{ status: DomainStatus }> = ({ status }) => {
let badgeColor = "bg-gray-400/10 text-gray-500 border-gray-400/10"; // Default color let badgeColor = "bg-gray-400/10 text-gray-500 border-gray-400/10"; // Default color

View File

@@ -7,7 +7,11 @@ import {
teamProcedure, teamProcedure,
} from "~/server/api/trpc"; } from "~/server/api/trpc";
import { db } from "~/server/db"; import { db } from "~/server/db";
import { createDomain, getDomain } from "~/server/service/domain-service"; import {
createDomain,
getDomain,
updateDomain,
} from "~/server/service/domain-service";
export const domainRouter = createTRPCRouter({ export const domainRouter = createTRPCRouter({
createDomain: teamProcedure createDomain: teamProcedure
@@ -21,6 +25,9 @@ export const domainRouter = createTRPCRouter({
where: { where: {
teamId: ctx.team.id, teamId: ctx.team.id,
}, },
orderBy: {
createdAt: "desc",
},
}); });
return domains; return domains;
@@ -31,4 +38,19 @@ export const domainRouter = createTRPCRouter({
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
return getDomain(input.id); return getDomain(input.id);
}), }),
updateDomain: teamProcedure
.input(
z.object({
id: z.number(),
clickTracking: z.boolean().optional(),
openTracking: z.boolean().optional(),
})
)
.mutation(async ({ ctx, input }) => {
return updateDomain(input.id, {
clickTracking: input.clickTracking,
openTracking: input.openTracking,
});
}),
}); });

View File

@@ -78,6 +78,12 @@ async function setupSESConfiguration() {
topicArn, topicArn,
[...GENERAL_EVENTS, "OPEN"] [...GENERAL_EVENTS, "OPEN"]
); );
await setWebhookConfiguration(APP_SETTINGS.SES_CONFIGURATION_FULL, topicArn, [
...GENERAL_EVENTS,
"CLICK",
"OPEN",
]);
} }
async function setWebhookConfiguration( async function setWebhookConfiguration(

View File

@@ -67,3 +67,13 @@ export async function getDomain(id: number) {
return domain; return domain;
} }
export async function updateDomain(
id: number,
data: { clickTracking?: boolean; openTracking?: boolean }
) {
return db.domain.update({
where: { id },
data,
});
}

View File

@@ -1,6 +1,7 @@
import { EmailContent } from "~/types"; import { EmailContent } from "~/types";
import { db } from "../db"; import { db } from "../db";
import { sendEmailThroughSes } from "../ses"; import { sendEmailThroughSes } from "../ses";
import { APP_SETTINGS } from "~/utils/constants";
export async function sendEmail( export async function sendEmail(
emailContent: EmailContent & { teamId: number } emailContent: EmailContent & { teamId: number }
@@ -30,6 +31,10 @@ export async function sendEmail(
text, text,
html, html,
region: domain.region, region: domain.region,
configurationSetName: getConfigurationSetName(
domain.clickTracking,
domain.openTracking
),
}); });
if (messageId) { if (messageId) {
@@ -47,3 +52,20 @@ export async function sendEmail(
}); });
} }
} }
function getConfigurationSetName(
clickTracking: boolean,
openTracking: boolean
) {
if (clickTracking && openTracking) {
return APP_SETTINGS.SES_CONFIGURATION_FULL;
}
if (clickTracking) {
return APP_SETTINGS.SES_CONFIGURATION_CLICK_TRACKING;
}
if (openTracking) {
return APP_SETTINGS.SES_CONFIGURATION_OPEN_TRACKING;
}
return APP_SETTINGS.SES_CONFIGURATION_GENERAL;
}

View File

@@ -97,8 +97,10 @@ export async function sendEmailThroughSes({
text, text,
html, html,
region = "us-east-1", region = "us-east-1",
configurationSetName,
}: EmailContent & { }: EmailContent & {
region?: string; region?: string;
configurationSetName: string;
}) { }) {
const sesClient = getSesClient(region); const sesClient = getSesClient(region);
const command = new SendEmailCommand({ const command = new SendEmailCommand({
@@ -128,6 +130,7 @@ export async function sendEmailThroughSes({
}, },
}, },
}, },
ConfigurationSetName: configurationSetName,
}); });
try { try {

View File

@@ -5,4 +5,5 @@ export const APP_SETTINGS = {
SES_CONFIGURATION_GENERAL: `SES_CONFIGURATION_GENERAL_${env.NODE_ENV}`, SES_CONFIGURATION_GENERAL: `SES_CONFIGURATION_GENERAL_${env.NODE_ENV}`,
SES_CONFIGURATION_CLICK_TRACKING: `SES_CONFIGURATION_CLICK_TRACKING_${env.NODE_ENV}`, SES_CONFIGURATION_CLICK_TRACKING: `SES_CONFIGURATION_CLICK_TRACKING_${env.NODE_ENV}`,
SES_CONFIGURATION_OPEN_TRACKING: `SES_CONFIGURATION_OPEN_TRACKING_${env.NODE_ENV}`, SES_CONFIGURATION_OPEN_TRACKING: `SES_CONFIGURATION_OPEN_TRACKING_${env.NODE_ENV}`,
SES_CONFIGURATION_FULL: `SES_CONFIGURATION_FULL_${env.NODE_ENV}`,
}; };