Skip to content

Commit 7ea7c34

Browse files
authored
Merge pull request #1845 from dubinc/wrapped
Dub Wrapped
2 parents 86648be + 9e23eb6 commit 7ea7c34

File tree

7 files changed

+665
-2
lines changed

7 files changed

+665
-2
lines changed
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { handleAndReturnErrorResponse } from "@/lib/api/errors";
2+
import { qstash } from "@/lib/cron";
3+
import { resend } from "@/lib/resend";
4+
import { prisma } from "@dub/prisma";
5+
import { APP_DOMAIN_WITH_NGROK } from "@dub/utils";
6+
import DubWrapped from "emails/dub-wrapped";
7+
8+
export const dynamic = "force-dynamic";
9+
10+
const BATCH_SIZE = 100;
11+
12+
// POST /api/cron/year-in-review
13+
export async function POST(req: Request) {
14+
try {
15+
if (process.env.VERCEL === "1") {
16+
return new Response("Not available in production.");
17+
}
18+
19+
if (!resend) {
20+
return new Response("Resend not initialized. Skipping...");
21+
}
22+
23+
const yearInReviews = await prisma.yearInReview.findMany({
24+
where: {
25+
sentAt: null,
26+
year: 2024,
27+
},
28+
select: {
29+
id: true,
30+
workspaceId: true,
31+
topCountries: true,
32+
topLinks: true,
33+
totalClicks: true,
34+
totalLinks: true,
35+
workspace: {
36+
select: {
37+
id: true,
38+
name: true,
39+
slug: true,
40+
logo: true,
41+
users: {
42+
select: {
43+
user: {
44+
select: {
45+
email: true,
46+
},
47+
},
48+
},
49+
},
50+
},
51+
},
52+
},
53+
take: 100,
54+
});
55+
56+
if (yearInReviews.length === 0) {
57+
return new Response("No jobs found. Skipping...");
58+
}
59+
60+
const emailData = yearInReviews.flatMap(
61+
({ workspace, totalClicks, totalLinks, topCountries, topLinks }) =>
62+
workspace.users
63+
.map(({ user }) => {
64+
if (!user.email) {
65+
return null;
66+
}
67+
68+
return {
69+
workspaceId: workspace.id,
70+
email: {
71+
from: "Steven from Dub.co <[email protected]>",
72+
to: user.email,
73+
reply_to: "[email protected]",
74+
subject: "Dub Year in Review 🎊",
75+
text: "Thank you for your support and here's to another year of your activity on Dub! Here's a look back at your activity in 2024.",
76+
react: DubWrapped({
77+
email: user.email,
78+
workspace: {
79+
logo: workspace.logo,
80+
name: workspace.name,
81+
slug: workspace.slug,
82+
},
83+
stats: {
84+
"Total Links": totalLinks,
85+
"Total Clicks": totalClicks,
86+
},
87+
// @ts-ignore
88+
topLinks,
89+
// @ts-ignore
90+
topCountries,
91+
}),
92+
},
93+
};
94+
})
95+
.filter((data) => data !== null),
96+
);
97+
98+
if (emailData.length === 0) {
99+
return new Response("No email data found. Skipping...");
100+
}
101+
102+
for (let i = 0; i < emailData.length; i += BATCH_SIZE) {
103+
const batch = emailData.slice(i, i + BATCH_SIZE);
104+
105+
console.log(
106+
`\n🚀 Sending batch ${Math.floor(i / BATCH_SIZE) + 1} of ${Math.ceil(emailData.length / BATCH_SIZE)}`,
107+
);
108+
109+
console.log(
110+
`📨 Recipients:`,
111+
// @ts-ignore
112+
batch.map((b) => b.email.to),
113+
);
114+
115+
if (batch.length === 0) {
116+
continue;
117+
}
118+
119+
const { data, error } = await resend.batch.send(
120+
// @ts-ignore
121+
batch.map((b) => b.email),
122+
);
123+
124+
console.log("🚀 ~ data:", data);
125+
if (error) {
126+
console.log("🚀 ~ error:", error);
127+
}
128+
}
129+
130+
await prisma.yearInReview.updateMany({
131+
where: {
132+
id: {
133+
in: yearInReviews.map(({ id }) => id),
134+
},
135+
},
136+
data: {
137+
sentAt: new Date(),
138+
},
139+
});
140+
141+
console.log(
142+
`Sent ${emailData.length} emails to ${yearInReviews.length} workspaces!`,
143+
);
144+
145+
await qstash.publishJSON({
146+
url: `${APP_DOMAIN_WITH_NGROK}/api/cron/year-in-review`,
147+
delay: 3,
148+
method: "POST",
149+
body: {},
150+
});
151+
152+
return new Response(
153+
`Sent ${emailData.length} emails to ${yearInReviews.length} workspaces!`,
154+
);
155+
} catch (error) {
156+
return handleAndReturnErrorResponse(error);
157+
}
158+
}

apps/web/emails/clicks-summary.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ export default function ClicksSummary({
142142
align="right"
143143
className="text-sm text-gray-600"
144144
>
145-
{nFormatter(clicks)}
145+
{nFormatter(clicks, { full: clicks < 99999 })}
146146
</Column>
147147
</Row>
148148
{index !== topLinks.length - 1 && (

apps/web/emails/components/footer.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ export default function Footer({
1515
<Hr className="mx-0 my-6 w-full border border-gray-200" />
1616
<Text className="text-[12px] leading-6 text-gray-500">
1717
We send out product update emails once a month – no spam, no nonsense.
18-
<br />
1918
Don't want to get these emails?{" "}
2019
<Link
2120
className="text-gray-700 underline"
@@ -24,6 +23,13 @@ export default function Footer({
2423
Unsubscribe here.
2524
</Link>
2625
</Text>
26+
<Text className="text-[12px] text-gray-500">
27+
Dub Technologies, Inc.
28+
<br />
29+
2261 Market Street STE 5906
30+
<br />
31+
San Francisco, CA 941114
32+
</Text>
2733
</Tailwind>
2834
);
2935
}
@@ -49,6 +55,13 @@ export default function Footer({
4955
</Link>
5056
</Text>
5157
)}
58+
<Text className="text-[12px] text-gray-500">
59+
Dub Technologies, Inc.
60+
<br />
61+
2261 Market Street STE 5906
62+
<br />
63+
San Francisco, CA 941114
64+
</Text>
5265
</Tailwind>
5366
);
5467
}

0 commit comments

Comments
 (0)