Add click and open tracking
This commit is contained in:
@@ -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
|
||||||
|
@@ -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,
|
||||||
|
});
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
@@ -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(
|
||||||
|
@@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@@ -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;
|
||||||
|
}
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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}`,
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user