Skip to content

Commit da52ccb

Browse files
authored
Prepare Send an email Campaign notification (#2615)
* I18N * Update filters.ts * Update messages.json * add CampaignButton to extras * Create CampaignButton.vue * campaign edge function * Update MiningTable.vue * fix edge func * Update email.ts * Update CampaignButton.vue * refactor template and use hono cors * use standalone campaign edge function * Update CampaignButton.vue * Update CampaignButton.vue * remove campaign feat
1 parent 155a3cb commit da52ccb

File tree

11 files changed

+185
-139
lines changed

11 files changed

+185
-139
lines changed

frontend/src/components/mining/table/MiningTable.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@
9090
/>
9191
</div>
9292

93+
<div>
94+
<CampaignButton :contacts-count="implicitlySelectedContactsLength" />
95+
</div>
9396
<div
9497
v-tooltip.top="
9598
(isExportDisabled || !selectedContactsLength) &&
@@ -860,7 +863,7 @@ import type {
860863
DataTableFilterEvent,
861864
DataTableSelectAllChangeEvent,
862865
} from 'primevue/datatable';
863-
866+
import { CampaignButton } from '@/utils/extras';
864867
import { useFiltersStore } from '@/stores/filters';
865868
import type { Contact } from '@/types/contact';
866869
import NormalizedLocation from '~/components/icons/NormalizedLocation.vue';

frontend/src/i18n/messages.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
"the_plural": "the",
2727
"of": "of",
2828
"email_required": "Email is required",
29-
"password_required": "Password is required"
29+
"password_required": "Password is required",
30+
"close": "Close"
3031
},
3132
"auth": {
3233
"sign_in": "Sign in",
@@ -169,7 +170,8 @@
169170
"the_plural": "les",
170171
"of": "sur",
171172
"email_required": "Email est obligatoire",
172-
"password_required": "Mot de passe est obligatoire"
173+
"password_required": "Mot de passe est obligatoire",
174+
"close": "Fermer"
173175
},
174176
"auth": {
175177
"sign_in": "Se connecter",

frontend/src/utils/extras.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { NormalizedLocation } from '~/types/contact';
22

33
export const PrivacyPolicyButton = null;
44
export const AcceptNewsLetter = null;
5+
export const CampaignButton = null;
56
// skipcq: JS-0321
67
export function startMiningNotification() {}
78
export function getLocationUrl(location: NormalizedLocation) {

supabase/functions/mail/utils/email.ts renamed to supabase/functions/_shared/mailing/email.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1-
import nodemailer from "nodemailer";
1+
import nodemailer from "npm:nodemailer@^7.0.5";
22

33
const host = Deno.env.get("SMTP_HOST");
44
const port = Deno.env.get("SMTP_PORT");
55
const user = Deno.env.get("SMTP_USER");
66
const pass = Deno.env.get("SMTP_PASS");
77
const from = `"leadminer" <${user}>`;
88

9-
export async function sendEmail(to: string, subject: string, html: string) {
9+
export async function sendEmail(
10+
to: string,
11+
subject: string,
12+
html: string,
13+
replyTo?: string,
14+
) {
1015
const transporter = nodemailer.createTransport({
1116
host,
1217
port,
@@ -21,6 +26,7 @@ export async function sendEmail(to: string, subject: string, html: string) {
2126
to,
2227
subject,
2328
html,
29+
replyTo,
2430
});
2531

2632
console.log("Email sent:", { to, messageId: info.messageId });
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"en": {
3+
"footer": {
4+
"line1": "You received this email as a notification about your recent activity on",
5+
"brand": "leadminer",
6+
"line2": "Extract, clean, and enrich your contacts — effortlessly."
7+
}
8+
},
9+
"fr": {
10+
"footer": {
11+
"line1": "Vous recevez cet e-mail en tant que notification concernant votre récente activité sur",
12+
"brand": "leadminer",
13+
"line2": "Extrayez, nettoyez et enrichissez vos contacts — sans effort."
14+
}
15+
}
16+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import i18nData from "./messages.json" with { type: "json" };
2+
3+
const LOGO_URL = Deno.env.get("LOGO_URL");
4+
const FRONTEND_HOST = Deno.env.get("FRONTEND_HOST");
5+
6+
/**
7+
* Simple template interpolator: replaces {var} with provided values
8+
*/
9+
export function t(
10+
template: string,
11+
vars: Record<string, string | number> = {},
12+
): string {
13+
return template.replace(/{(\w+)}/g, (_, key) => String(vars[key] ?? ""));
14+
}
15+
16+
export function fillTemplate(
17+
headerTitle: string,
18+
bodyContent: string,
19+
headerSubtitle = "",
20+
language: "en" | "fr" = "en",
21+
) {
22+
const i18n = (i18nData as any)[language];
23+
24+
return `
25+
<!DOCTYPE html>
26+
<html lang="${language}">
27+
<head>
28+
<meta charset="UTF-8" />
29+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
30+
<title>${headerTitle}</title>
31+
<link
32+
href="https://fonts.googleapis.com/css2?family=Lexend+Deca:wght@400;600;700&family=Merriweather:wght@700&display=swap"
33+
rel="stylesheet"
34+
/>
35+
</head>
36+
<body
37+
style="
38+
margin: 0;
39+
background-color: #f9fafb;
40+
color: #111827;
41+
font-family: 'Lexend Deca', Arial, sans-serif;
42+
-webkit-font-smoothing: antialiased;
43+
"
44+
>
45+
<table width="100%" cellpadding="0" cellspacing="0" role="presentation" style="padding: 40px 0">
46+
<tr>
47+
<td align="center">
48+
<table
49+
width="600"
50+
cellpadding="0"
51+
cellspacing="0"
52+
role="presentation"
53+
style="
54+
background: #ffffff;
55+
border-radius: 12px;
56+
overflow: hidden;
57+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
58+
"
59+
>
60+
<!-- Logo -->
61+
<tr>
62+
<td align="center" style="padding: 20px 0">
63+
<a href="${FRONTEND_HOST}" target="_blank" style="display: inline-block; text-decoration: none">
64+
<img
65+
src="${LOGO_URL}"
66+
alt="Leadminer"
67+
title="Leadminer Logo"
68+
border="0"
69+
style="display: block; height: 3.125rem; width: auto"
70+
/>
71+
</a>
72+
</td>
73+
</tr>
74+
75+
<!-- Header -->
76+
<tr>
77+
<td style="text-align: center">
78+
<h1
79+
style="
80+
margin: 0;
81+
font-size: 26px;
82+
font-family: 'Merriweather', serif;
83+
font-weight: 700;
84+
"
85+
>
86+
${headerTitle}
87+
</h1>
88+
<p style="margin: 10px 0 0; font-size: 16px; color: #374151">
89+
${headerSubtitle}
90+
</p>
91+
</td>
92+
</tr>
93+
94+
<!-- Body -->
95+
<tr>
96+
<td style="padding: 36px 32px">
97+
${bodyContent}
98+
</td>
99+
</tr>
100+
101+
<!-- Footer -->
102+
<tr>
103+
<td
104+
style="
105+
background: #f3f4f6;
106+
text-align: center;
107+
padding: 28px;
108+
font-size: 13px;
109+
color: #6b7280;
110+
"
111+
>
112+
<p style="margin: 0 0 4px">
113+
${i18n.footer.line1}
114+
<strong>
115+
<a
116+
style="color: #6b7280; text-decoration: none"
117+
href="${FRONTEND_HOST}"
118+
>
119+
${i18n.footer.brand}
120+
</a>
121+
</strong>
122+
.
123+
</p>
124+
<p style="margin: 6px 0 0; color: #9ca3af">
125+
${i18n.footer.line2}
126+
</p>
127+
</td>
128+
</tr>
129+
</table>
130+
</td>
131+
</tr>
132+
</table>
133+
</body>
134+
</html>
135+
`;
136+
}

supabase/functions/mail/deno.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
{
22
"imports": {
33
"hono": "https://esm.sh/hono@4.7.4",
4-
"supabase": "https://esm.sh/@supabase/supabase-js@2",
5-
"nodemailer": "npm:nodemailer@^7.0.5"
4+
"supabase": "https://esm.sh/@supabase/supabase-js@2"
65
}
76
}

supabase/functions/mail/index.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { Context, Hono } from "hono";
2-
import { verifyServiceRole } from "../_shared/middlewares.ts";
32
import mailMiningComplete from "./mining-complete/index.ts";
3+
import { verifyServiceRole } from "../_shared/middlewares.ts";
44

55
const functionName = "mail";
66
const app = new Hono().basePath(`/${functionName}`);
77

8-
app.use("*", verifyServiceRole);
9-
10-
app.post("/mining-complete", async (c: Context) => {
8+
app.options("/mining-complete", verifyServiceRole); // From Backend only
9+
app.post("/mining-complete", verifyServiceRole, async (c: Context) => {
1110
const { miningId } = await c.req.json();
1211

1312
if (!miningId) {
@@ -23,4 +22,4 @@ app.post("/mining-complete", async (c: Context) => {
2322
}
2423
});
2524

26-
Deno.serve((req) => app.fetch(req));
25+
Deno.serve((req: Request) => app.fetch(req));

supabase/functions/mail/mining-complete/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { getMiningStats, getUserEmail, getUserLanguage } from "../utils/db.ts";
2-
import { sendEmail } from "../utils/email.ts";
2+
import { sendEmail } from "../../_shared/mailing/email.ts";
33
import buildEmail from "./template.ts";
44

55
export default async function mailMiningComplete(
@@ -12,7 +12,7 @@ export default async function mailMiningComplete(
1212
total_reachable,
1313
total_with_phone,
1414
total_with_company,
15-
total_with_location
15+
total_with_location,
1616
} = await getMiningStats(miningId);
1717

1818
const to = await getUserEmail(user_id);

0 commit comments

Comments
 (0)