placementt-core 1.300.751 → 1.300.752
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/constants.js +2 -3
- package/lib/constants.js.map +1 -1
- package/lib/emailSections.d.ts +4 -1
- package/lib/emailSections.js +205 -75
- package/lib/emailSections.js.map +1 -1
- package/lib/typeDefinitions.d.ts +3 -1
- package/lib/util.d.ts +6 -3
- package/lib/util.js +19 -14
- package/lib/util.js.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +2 -3
- package/src/emailSections.ts +235 -75
- package/src/typeDefinitions.ts +4 -1
- package/src/util.ts +20 -14
package/src/emailSections.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {where} from "firebase/firestore";
|
|
2
|
+
import FirebaseQuery from "./firebase/firebaseQuery";
|
|
3
|
+
import {convertDate} from "./firebase/util";
|
|
4
|
+
import {EmailSectionConfig, EmailSectionNames, EmailSectionRenderProps, ExternalEvent, ExternalEventAttendee, ExternalEventRequest, ProviderContactData} from "./typeDefinitions";
|
|
2
5
|
|
|
3
6
|
const PRIMARY_COLOUR = "#397DF2";
|
|
4
7
|
|
|
5
8
|
type SectionEvent = {
|
|
6
9
|
title: string,
|
|
10
|
+
description?: string,
|
|
7
11
|
startDate: string,
|
|
8
12
|
endDate?: string,
|
|
9
13
|
activityType: string,
|
|
@@ -41,16 +45,129 @@ function sectionStyles(color: string) {
|
|
|
41
45
|
return {
|
|
42
46
|
container: `margin: 16px 0; font-family: nunito, sans-serif;`,
|
|
43
47
|
title: `font-size: 16px; font-weight: bold; color: #333; margin: 0 0 8px 0; font-family: nunito, sans-serif;`,
|
|
44
|
-
table: `width: 100%; border-collapse: collapse; margin: 8px 0; font-family: nunito, sans-serif; font-size: 13px;`,
|
|
45
|
-
th: `background-color: ${color}; color: white; text-align: left; padding: 8px 12px; font-family: nunito, sans-serif; font-size: 13px;`,
|
|
46
|
-
td: `padding: 8px 12px; border-bottom: 1px solid #f0f0f0; font-family: nunito, sans-serif; font-size: 13px; color: #333;`,
|
|
47
|
-
tdAlt: `padding: 8px 12px; border-bottom: 1px solid #f0f0f0; background-color: #fafafa; font-family: nunito, sans-serif; font-size: 13px; color: #333;`,
|
|
48
48
|
footer: `font-size: 12px; color: #666; margin: 8px 0 0 0; font-family: nunito, sans-serif;`,
|
|
49
49
|
link: `color: ${color}; text-decoration: none;`,
|
|
50
50
|
emptyText: `font-family: nunito, sans-serif; color: #666; font-size: 13px; margin: 4px 0;`,
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
export const emailCampaignSectionDataFetcher = async (sectionName: EmailSectionNames, data: {[key: string]: unknown}): Promise<string> => {
|
|
55
|
+
const firebaseQuery = new FirebaseQuery();
|
|
56
|
+
const oId = data.oId as string;
|
|
57
|
+
const primaryColor = data.primaryColor as string | undefined;
|
|
58
|
+
const today = convertDate(new Date(), "dbstring") as string;
|
|
59
|
+
|
|
60
|
+
switch (sectionName) {
|
|
61
|
+
case "upcomingEventsSection": {
|
|
62
|
+
const eventsRaw = await firebaseQuery.getDocsWhere(
|
|
63
|
+
["externalEvents"],
|
|
64
|
+
[where("oId", "==", oId), where("startDate", ">=", today)]
|
|
65
|
+
) as {[key: string]: ExternalEvent};
|
|
66
|
+
|
|
67
|
+
const events: SectionEvent[] = Object.entries(eventsRaw).map(([id, e]) => ({
|
|
68
|
+
id,
|
|
69
|
+
title: e.name,
|
|
70
|
+
description: e.description,
|
|
71
|
+
startDate: convertDate(e.startDate, "visual") as string || e.startDate || "",
|
|
72
|
+
endDate: e.endDate ? convertDate(e.endDate, "visual") as string : undefined,
|
|
73
|
+
activityType: (e as any).activityType || e.activityId || "other",
|
|
74
|
+
location: e.locality,
|
|
75
|
+
image: (e as any).image,
|
|
76
|
+
}));
|
|
77
|
+
|
|
78
|
+
return upcomingEventsSection.render({placeholder: false, events: events as any, oId, primaryColor});
|
|
79
|
+
}
|
|
80
|
+
case "attendingEventsSection": {
|
|
81
|
+
const providerContactId = data.providerContactId as string;
|
|
82
|
+
|
|
83
|
+
const attendeesRaw = await firebaseQuery.getDocsWhere(
|
|
84
|
+
["externalEventAttendees"],
|
|
85
|
+
[where("providerContactId", "==", providerContactId), where("oId", "==", oId)]
|
|
86
|
+
) as {[key: string]: ExternalEventAttendee};
|
|
87
|
+
|
|
88
|
+
const events: SectionEvent[] = (await Promise.all(
|
|
89
|
+
Object.values(attendeesRaw).map(async (attendee) => {
|
|
90
|
+
const event = await firebaseQuery.getDocData(["externalEvents", attendee.eventId]) as ExternalEvent | null;
|
|
91
|
+
if (!event?.startDate || (event.endDate && event.endDate < today)) return null;
|
|
92
|
+
return {
|
|
93
|
+
title: event.name,
|
|
94
|
+
description: event.description,
|
|
95
|
+
startDate: convertDate(event.startDate, "visual") as string || event.startDate,
|
|
96
|
+
endDate: event.endDate ? convertDate(event.endDate, "visual") as string : undefined,
|
|
97
|
+
activityType: (event as any).activityType || event.activityId || "other",
|
|
98
|
+
location: event.locality,
|
|
99
|
+
} as SectionEvent;
|
|
100
|
+
})
|
|
101
|
+
)).filter(Boolean) as SectionEvent[];
|
|
102
|
+
|
|
103
|
+
return attendingEventsSection.render({placeholder: false, events: events as any, primaryColor});
|
|
104
|
+
}
|
|
105
|
+
case "sharedOpportunitiesSection": {
|
|
106
|
+
const providerContactId = data.providerContactId as string;
|
|
107
|
+
|
|
108
|
+
const oppsRaw = await firebaseQuery.getDocsWhere(
|
|
109
|
+
["externalEventRequests"],
|
|
110
|
+
[where("providerContactId", "==", providerContactId)]
|
|
111
|
+
) as {[key: string]: ExternalEventRequest};
|
|
112
|
+
|
|
113
|
+
const opportunities: SectionOpportunity[] = Object.entries(oppsRaw).map(([id, r]) => ({
|
|
114
|
+
id,
|
|
115
|
+
title: r.title || "",
|
|
116
|
+
description: r.description,
|
|
117
|
+
type: (r as any).activityType,
|
|
118
|
+
created: r.created,
|
|
119
|
+
}));
|
|
120
|
+
|
|
121
|
+
return sharedOpportunitiesSection.render({placeholder: false, opportunities: opportunities as any, oId, primaryColor});
|
|
122
|
+
}
|
|
123
|
+
case "employerDatabaseSummarySection": {
|
|
124
|
+
const contactsRaw = await firebaseQuery.getDocsWhere(
|
|
125
|
+
["providerContacts"],
|
|
126
|
+
[where(`savedBy.${oId}.exists`, "==", true)]
|
|
127
|
+
) as {[key: string]: ProviderContactData};
|
|
128
|
+
|
|
129
|
+
const contacts = Object.values(contactsRaw);
|
|
130
|
+
const sectors = new Set(contacts.map((c) => c.sector).filter(Boolean));
|
|
131
|
+
|
|
132
|
+
const consentedActivities: {[activity: string]: number} = {};
|
|
133
|
+
contacts.forEach((c) => {
|
|
134
|
+
const saved = c.savedBy?.[oId];
|
|
135
|
+
if (saved?.activities) {
|
|
136
|
+
saved.activities.forEach((activity) => {
|
|
137
|
+
consentedActivities[activity] = (consentedActivities[activity] || 0) + 1;
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const stats: SectionEmployerStats = {
|
|
143
|
+
totalContacts: contacts.length,
|
|
144
|
+
sectors: sectors.size,
|
|
145
|
+
consentedActivities,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
return employerDatabaseSummarySection.render({placeholder: false, stats, primaryColor});
|
|
149
|
+
}
|
|
150
|
+
case "sharedOpportunitiesForSchoolSection": {
|
|
151
|
+
const oppsRaw = await firebaseQuery.getDocsWhere(
|
|
152
|
+
["externalEventRequests"],
|
|
153
|
+
[where("schools", "array-contains", oId)]
|
|
154
|
+
) as {[key: string]: ExternalEventRequest};
|
|
155
|
+
|
|
156
|
+
const opportunities: SectionOpportunity[] = Object.entries(oppsRaw).map(([id, r]) => ({
|
|
157
|
+
id,
|
|
158
|
+
title: r.title || "",
|
|
159
|
+
description: r.description,
|
|
160
|
+
type: (r as any).activityType,
|
|
161
|
+
created: r.created,
|
|
162
|
+
}));
|
|
163
|
+
|
|
164
|
+
return sharedOpportunitiesForSchoolSection.render({placeholder: false, opportunities: opportunities as any, primaryColor});
|
|
165
|
+
}
|
|
166
|
+
default:
|
|
167
|
+
return "";
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
54
171
|
export const upcomingEventsSection: EmailSectionConfig = {
|
|
55
172
|
id: "upcomingEvents",
|
|
56
173
|
title: "Upcoming Events",
|
|
@@ -60,11 +177,12 @@ export const upcomingEventsSection: EmailSectionConfig = {
|
|
|
60
177
|
render: ({placeholder, events, oId, primaryColor: pc}: EmailSectionRenderProps) => {
|
|
61
178
|
const color = (pc as string) || PRIMARY_COLOUR;
|
|
62
179
|
const s = sectionStyles(color);
|
|
180
|
+
const eventOId = oId as string | undefined;
|
|
63
181
|
|
|
64
182
|
const displayEvents: SectionEvent[] = placeholder ? [
|
|
65
|
-
{title: "Mock Interviews Day", startDate: "15 May 2025", activityType: "mockInterviews", location: "School Main Hall"},
|
|
66
|
-
{title: "Annual Careers Fair", startDate: "22 May 2025", endDate: "23 May 2025", activityType: "workExperience", location: "Town Hall"},
|
|
67
|
-
{title: "Mentoring Programme", startDate: "5 June 2025", activityType: "mentoring"},
|
|
183
|
+
{title: "Mock Interviews Day", startDate: "15 May 2025", activityType: "mockInterviews", location: "School Main Hall", description: "Join us for a day of mock interview practice with our Year 11 students."},
|
|
184
|
+
{title: "Annual Careers Fair", startDate: "22 May 2025", endDate: "23 May 2025", activityType: "workExperience", location: "Town Hall", description: "A two-day careers fair featuring employers from across the region."},
|
|
185
|
+
{title: "Mentoring Programme", startDate: "5 June 2025", activityType: "mentoring", description: "Support students with one-to-one mentoring sessions over the summer term."},
|
|
68
186
|
{title: "Workplace Visit Day", startDate: "12 June 2025", activityType: "workplaceVisits", location: "City Centre"},
|
|
69
187
|
] : ((events as SectionEvent[]) || []);
|
|
70
188
|
|
|
@@ -88,17 +206,20 @@ export const upcomingEventsSection: EmailSectionConfig = {
|
|
|
88
206
|
const dateStr = event.endDate && event.endDate !== event.startDate
|
|
89
207
|
? `${event.startDate} – ${event.endDate}`
|
|
90
208
|
: event.startDate;
|
|
91
|
-
const eventLink =
|
|
209
|
+
const eventLink = eventOId ? `https://careerthread.co.uk/publicEvents/${eventOId}` : "#";
|
|
92
210
|
const imgStyle = event.image
|
|
93
|
-
? `height:
|
|
94
|
-
: `height:
|
|
211
|
+
? `height:120px;background-image:url('${event.image}');background-size:cover;background-position:center;`
|
|
212
|
+
: `height:120px;background-color:${bgColor};`;
|
|
95
213
|
|
|
96
214
|
return `
|
|
97
215
|
<div style="border-radius:12px;overflow:hidden;box-shadow:0 2px 10px rgba(0,0,0,0.12);background:#ffffff;">
|
|
98
216
|
<div style="${imgStyle}"></div>
|
|
99
217
|
<div style="padding:12px 14px 14px;">
|
|
218
|
+
<p style="font-size:11px;font-weight:bold;color:${color};text-transform:uppercase;letter-spacing:0.5px;margin:0 0 4px 0;font-family:nunito,sans-serif;">${activityTypeLabel(event.activityType)}</p>
|
|
100
219
|
<p style="font-weight:bold;font-size:14px;margin:0 0 3px 0;font-family:nunito,sans-serif;color:#333;line-height:1.3;">${event.title}</p>
|
|
101
|
-
<p style="font-size:12px;color:#888;margin:0 0
|
|
220
|
+
<p style="font-size:12px;color:#888;margin:0 0 4px 0;font-family:nunito,sans-serif;">${dateStr}</p>
|
|
221
|
+
${event.location ? `<p style="font-size:12px;color:#888;margin:0 0 4px 0;font-family:nunito,sans-serif;">${event.location}</p>` : ""}
|
|
222
|
+
${event.description ? `<p style="font-size:12px;color:#666;margin:0 0 10px 0;font-family:nunito,sans-serif;line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">${event.description}</p>` : `<div style="margin-bottom:10px;"></div>`}
|
|
102
223
|
<a href="${eventLink}" style="display:block;background-color:${color};color:#ffffff;text-align:center;padding:9px 12px;border-radius:6px;text-decoration:none;font-size:13px;font-family:nunito,sans-serif;font-weight:bold;">View Event</a>
|
|
103
224
|
</div>
|
|
104
225
|
</div>`;
|
|
@@ -125,7 +246,7 @@ export const upcomingEventsSection: EmailSectionConfig = {
|
|
|
125
246
|
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-collapse:collapse;">
|
|
126
247
|
${cardRows.join("")}
|
|
127
248
|
</table>
|
|
128
|
-
${
|
|
249
|
+
${eventOId ? `<p style="${s.footer}"><a href="https://careerthread.co.uk/publicEvents/${eventOId}" style="${s.link}">View all events and sign up →</a></p>` : ""}
|
|
129
250
|
</div>`;
|
|
130
251
|
},
|
|
131
252
|
};
|
|
@@ -141,8 +262,8 @@ export const attendingEventsSection: EmailSectionConfig = {
|
|
|
141
262
|
const s = sectionStyles(color);
|
|
142
263
|
|
|
143
264
|
const displayEvents: SectionEvent[] = placeholder ? [
|
|
144
|
-
{title: "Mock Interviews Day", startDate: "15 May 2025", activityType: "mockInterviews"},
|
|
145
|
-
{title: "Annual Careers Fair", startDate: "22 May 2025", activityType: "workExperience"},
|
|
265
|
+
{title: "Mock Interviews Day", startDate: "15 May 2025", activityType: "mockInterviews", description: "Join us for a day of mock interview practice with our Year 11 students."},
|
|
266
|
+
{title: "Annual Careers Fair", startDate: "22 May 2025", activityType: "workExperience", description: "A two-day careers fair featuring employers from across the region."},
|
|
146
267
|
] : ((events as SectionEvent[]) || []);
|
|
147
268
|
|
|
148
269
|
if (!placeholder && !displayEvents.length) {
|
|
@@ -153,25 +274,40 @@ export const attendingEventsSection: EmailSectionConfig = {
|
|
|
153
274
|
</div>`;
|
|
154
275
|
}
|
|
155
276
|
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
277
|
+
const makeCard = (event: SectionEvent) => {
|
|
278
|
+
const dateStr = event.endDate && event.endDate !== event.startDate
|
|
279
|
+
? `${event.startDate} – ${event.endDate}`
|
|
280
|
+
: event.startDate;
|
|
281
|
+
return `
|
|
282
|
+
<div style="border-radius:12px;box-shadow:0 2px 10px rgba(0,0,0,0.12);background:#ffffff;padding:16px;">
|
|
283
|
+
<p style="font-size:11px;font-weight:bold;color:${color};text-transform:uppercase;letter-spacing:0.5px;margin:0 0 6px 0;font-family:nunito,sans-serif;">${activityTypeLabel(event.activityType)}</p>
|
|
284
|
+
<p style="font-weight:bold;font-size:14px;margin:0 0 4px 0;font-family:nunito,sans-serif;color:#333;line-height:1.3;">${event.title}</p>
|
|
285
|
+
<p style="font-size:12px;color:#888;margin:0 0 4px 0;font-family:nunito,sans-serif;">${dateStr}</p>
|
|
286
|
+
${event.location ? `<p style="font-size:12px;color:#888;margin:0 0 4px 0;font-family:nunito,sans-serif;">${event.location}</p>` : ""}
|
|
287
|
+
${event.description ? `<p style="font-size:12px;color:#666;margin:0;font-family:nunito,sans-serif;line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">${event.description}</p>` : ""}
|
|
288
|
+
</div>`;
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const cardRows: string[] = [];
|
|
292
|
+
for (let i = 0; i < displayEvents.length; i += 2) {
|
|
293
|
+
const second = displayEvents[i + 1];
|
|
294
|
+
cardRows.push(`
|
|
295
|
+
<tr>
|
|
296
|
+
<td class="att-col" style="width:50%;vertical-align:top;padding:0 6px 16px 0;">
|
|
297
|
+
${makeCard(displayEvents[i])}
|
|
298
|
+
</td>
|
|
299
|
+
<td class="att-col" style="width:50%;vertical-align:top;padding:0 0 16px 6px;">
|
|
300
|
+
${second ? makeCard(second) : ""}
|
|
301
|
+
</td>
|
|
302
|
+
</tr>`);
|
|
303
|
+
}
|
|
162
304
|
|
|
163
305
|
return `
|
|
164
306
|
<div style="${s.container}">
|
|
307
|
+
<style>@media(max-width:600px){.att-col{width:100%!important;display:block!important;padding:0 0 12px 0!important;}}</style>
|
|
165
308
|
<p style="${s.title}">Your Upcoming Events</p>
|
|
166
|
-
<table style="
|
|
167
|
-
|
|
168
|
-
<tr>
|
|
169
|
-
<th style="${s.th}">Event</th>
|
|
170
|
-
<th style="${s.th}">Date</th>
|
|
171
|
-
<th style="${s.th}">Type</th>
|
|
172
|
-
</tr>
|
|
173
|
-
</thead>
|
|
174
|
-
<tbody>${rows}</tbody>
|
|
309
|
+
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-collapse:collapse;">
|
|
310
|
+
${cardRows.join("")}
|
|
175
311
|
</table>
|
|
176
312
|
</div>`;
|
|
177
313
|
},
|
|
@@ -186,6 +322,7 @@ export const sharedOpportunitiesSection: EmailSectionConfig = {
|
|
|
186
322
|
render: ({placeholder, opportunities, oId, primaryColor: pc}: EmailSectionRenderProps) => {
|
|
187
323
|
const color = (pc as string) || PRIMARY_COLOUR;
|
|
188
324
|
const s = sectionStyles(color);
|
|
325
|
+
const eventOId = oId as string | undefined;
|
|
189
326
|
|
|
190
327
|
const displayOps: SectionOpportunity[] = placeholder ? [
|
|
191
328
|
{title: "Mock Interview Sessions", description: "Offering 10 mock interview slots for Year 11 students", type: "mockInterviews"},
|
|
@@ -198,7 +335,7 @@ export const sharedOpportunitiesSection: EmailSectionConfig = {
|
|
|
198
335
|
<div style="${s.container}">
|
|
199
336
|
<p style="${s.title}">Share an Opportunity</p>
|
|
200
337
|
<p style="${s.emptyText}">We would love to hear from you! You can share work experience placements, mock interview sessions, workplace visits, and more with our students.</p>
|
|
201
|
-
${
|
|
338
|
+
${eventOId ? `<p style="font-family:nunito,sans-serif;margin:12px 0 0 0;"><a href="https://careerthread.co.uk/requestEvent/${eventOId}" style="background-color:${color};color:white;padding:8px 16px;border-radius:20px;text-decoration:none;font-size:14px;font-family:nunito,sans-serif;">Submit an Opportunity</a></p>` : ""}
|
|
202
339
|
</div>`;
|
|
203
340
|
}
|
|
204
341
|
|
|
@@ -206,7 +343,7 @@ export const sharedOpportunitiesSection: EmailSectionConfig = {
|
|
|
206
343
|
<div style="border-radius:12px;box-shadow:0 2px 10px rgba(0,0,0,0.12);background:#ffffff;padding:16px;">
|
|
207
344
|
<p style="font-size:11px;font-weight:bold;color:${color};text-transform:uppercase;letter-spacing:0.5px;margin:0 0 6px 0;font-family:nunito,sans-serif;">${op.type ? activityTypeLabel(op.type) : "Opportunity"}</p>
|
|
208
345
|
<p style="font-weight:bold;font-size:14px;margin:0 0 6px 0;font-family:nunito,sans-serif;color:#333;line-height:1.3;">${op.title}</p>
|
|
209
|
-
|
|
346
|
+
${op.description ? `<p style="font-size:12px;color:#888;margin:0;font-family:nunito,sans-serif;line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">${op.description}</p>` : ""}
|
|
210
347
|
</div>`;
|
|
211
348
|
|
|
212
349
|
const cardRows: string[] = [];
|
|
@@ -231,7 +368,7 @@ export const sharedOpportunitiesSection: EmailSectionConfig = {
|
|
|
231
368
|
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-collapse:collapse;">
|
|
232
369
|
${cardRows.join("")}
|
|
233
370
|
</table>
|
|
234
|
-
${
|
|
371
|
+
${eventOId ? `<p style="${s.footer}"><a href="https://careerthread.co.uk/requestEvent/${eventOId}" style="${s.link}">Submit another opportunity →</a></p>` : ""}
|
|
235
372
|
</div>`;
|
|
236
373
|
},
|
|
237
374
|
};
|
|
@@ -265,38 +402,53 @@ export const employerDatabaseSummarySection: EmailSectionConfig = {
|
|
|
265
402
|
other: "Other",
|
|
266
403
|
};
|
|
267
404
|
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
405
|
+
const activityEntries = Object.entries(displayStats.consentedActivities);
|
|
406
|
+
const activityCardRows: string[] = [];
|
|
407
|
+
for (let i = 0; i < activityEntries.length; i += 2) {
|
|
408
|
+
const [actA, countA] = activityEntries[i];
|
|
409
|
+
const second = activityEntries[i + 1];
|
|
410
|
+
activityCardRows.push(`
|
|
411
|
+
<tr>
|
|
412
|
+
<td class="act-col" style="width:50%;vertical-align:top;padding:0 6px 12px 0;">
|
|
413
|
+
<div style="border-radius:10px;box-shadow:0 2px 8px rgba(0,0,0,0.10);background:#ffffff;padding:14px 16px;text-align:center;">
|
|
414
|
+
<div style="font-size:22px;font-weight:bold;color:${color};font-family:nunito,sans-serif;">${countA}</div>
|
|
415
|
+
<div style="font-size:12px;color:#555;font-family:nunito,sans-serif;margin-top:4px;">${activityLabels[actA] || actA}</div>
|
|
416
|
+
</div>
|
|
417
|
+
</td>
|
|
418
|
+
<td class="act-col" style="width:50%;vertical-align:top;padding:0 0 12px 6px;">
|
|
419
|
+
${second ? `
|
|
420
|
+
<div style="border-radius:10px;box-shadow:0 2px 8px rgba(0,0,0,0.10);background:#ffffff;padding:14px 16px;text-align:center;">
|
|
421
|
+
<div style="font-size:22px;font-weight:bold;color:${color};font-family:nunito,sans-serif;">${second[1]}</div>
|
|
422
|
+
<div style="font-size:12px;color:#555;font-family:nunito,sans-serif;margin-top:4px;">${activityLabels[second[0]] || second[0]}</div>
|
|
423
|
+
</div>` : ""}
|
|
424
|
+
</td>
|
|
425
|
+
</tr>`);
|
|
426
|
+
}
|
|
273
427
|
|
|
274
428
|
return `
|
|
275
429
|
<div style="${s.container}">
|
|
430
|
+
<style>@media(max-width:600px){.act-col{width:100%!important;display:block!important;padding:0 0 8px 0!important;}}</style>
|
|
276
431
|
<p style="${s.title}">Your Employer Database</p>
|
|
277
|
-
<table
|
|
432
|
+
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-collapse:collapse;margin:8px 0;">
|
|
278
433
|
<tr>
|
|
279
|
-
<td
|
|
280
|
-
<div style="
|
|
281
|
-
|
|
434
|
+
<td class="act-col" style="width:50%;vertical-align:top;padding:0 6px 12px 0;">
|
|
435
|
+
<div style="padding:16px;background-color:${color};color:white;text-align:center;border-radius:10px;">
|
|
436
|
+
<div style="font-size:28px;font-weight:bold;font-family:nunito,sans-serif;">${displayStats.totalContacts}</div>
|
|
437
|
+
<div style="font-size:12px;font-family:nunito,sans-serif;opacity:0.9;margin-top:4px;">Total Contacts</div>
|
|
438
|
+
</div>
|
|
282
439
|
</td>
|
|
283
|
-
<td style="width:
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
440
|
+
<td class="act-col" style="width:50%;vertical-align:top;padding:0 0 12px 6px;">
|
|
441
|
+
<div style="padding:16px;background-color:${color};color:white;text-align:center;border-radius:10px;">
|
|
442
|
+
<div style="font-size:28px;font-weight:bold;font-family:nunito,sans-serif;">${displayStats.sectors}</div>
|
|
443
|
+
<div style="font-size:12px;font-family:nunito,sans-serif;opacity:0.9;margin-top:4px;">Sectors Represented</div>
|
|
444
|
+
</div>
|
|
287
445
|
</td>
|
|
288
446
|
</tr>
|
|
289
447
|
</table>
|
|
290
|
-
${
|
|
291
|
-
<p style="font-size:
|
|
292
|
-
<table style="
|
|
293
|
-
|
|
294
|
-
<tr>
|
|
295
|
-
<th style="${s.th}">Activity</th>
|
|
296
|
-
<th style="${s.th}">Consented Employers</th>
|
|
297
|
-
</tr>
|
|
298
|
-
</thead>
|
|
299
|
-
<tbody>${consentRows}</tbody>
|
|
448
|
+
${activityCardRows.length ? `
|
|
449
|
+
<p style="font-size:14px;font-weight:bold;font-family:nunito,sans-serif;margin:8px 0 4px 0;color:#333;">Consented to Activities</p>
|
|
450
|
+
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-collapse:collapse;">
|
|
451
|
+
${activityCardRows.join("")}
|
|
300
452
|
</table>` : ""}
|
|
301
453
|
</div>`;
|
|
302
454
|
},
|
|
@@ -305,7 +457,7 @@ export const employerDatabaseSummarySection: EmailSectionConfig = {
|
|
|
305
457
|
export const sharedOpportunitiesForSchoolSection: EmailSectionConfig = {
|
|
306
458
|
id: "sharedOpportunitiesForSchool",
|
|
307
459
|
title: "Opportunities Shared With Your School",
|
|
308
|
-
description: "A
|
|
460
|
+
description: "A grid of employer opportunities that have been shared with this school.",
|
|
309
461
|
emailInput: [{type: "heading-one", children: [{text: "Employer Opportunities"}]}, {type: "paragraph", children: [{text: "See below the opportunities that employers have shared with your students. Click on an opportunity to see more."}]}],
|
|
310
462
|
forTemplates: ["schoolsEmailCampaign"],
|
|
311
463
|
render: ({placeholder, opportunities, primaryColor: pc}: EmailSectionRenderProps) => {
|
|
@@ -313,9 +465,9 @@ export const sharedOpportunitiesForSchoolSection: EmailSectionConfig = {
|
|
|
313
465
|
const s = sectionStyles(color);
|
|
314
466
|
|
|
315
467
|
const displayOps: SectionOpportunity[] = placeholder ? [
|
|
316
|
-
{title: "Mock Interview Day", description: "Financial services firm offering interview practice", type: "mockInterviews"},
|
|
317
|
-
{title: "Finance Workplace Visit", description: "Tour of a London bank for up to 25 students", type: "workplaceVisits"},
|
|
318
|
-
{title: "Year 10 Work Experience", description: "1-week placement at a local engineering firm", type: "workExperience"},
|
|
468
|
+
{title: "Mock Interview Day", description: "Financial services firm offering interview practice for Year 11 students ahead of their GCSEs.", type: "mockInterviews"},
|
|
469
|
+
{title: "Finance Workplace Visit", description: "Tour of a London bank for up to 25 students, with Q&A from senior staff.", type: "workplaceVisits"},
|
|
470
|
+
{title: "Year 10 Work Experience", description: "1-week placement at a local engineering firm working alongside qualified engineers.", type: "workExperience"},
|
|
319
471
|
] : ((opportunities as SectionOpportunity[]) || []);
|
|
320
472
|
|
|
321
473
|
if (!placeholder && !displayOps.length) {
|
|
@@ -326,25 +478,33 @@ export const sharedOpportunitiesForSchoolSection: EmailSectionConfig = {
|
|
|
326
478
|
</div>`;
|
|
327
479
|
}
|
|
328
480
|
|
|
329
|
-
const
|
|
330
|
-
<
|
|
331
|
-
<
|
|
332
|
-
<
|
|
333
|
-
|
|
334
|
-
</
|
|
481
|
+
const makeCard = (op: SectionOpportunity) => `
|
|
482
|
+
<div style="border-radius:12px;box-shadow:0 2px 10px rgba(0,0,0,0.12);background:#ffffff;padding:16px;">
|
|
483
|
+
<p style="font-size:11px;font-weight:bold;color:${color};text-transform:uppercase;letter-spacing:0.5px;margin:0 0 6px 0;font-family:nunito,sans-serif;">${op.type ? activityTypeLabel(op.type) : "Opportunity"}</p>
|
|
484
|
+
<p style="font-weight:bold;font-size:14px;margin:0 0 6px 0;font-family:nunito,sans-serif;color:#333;line-height:1.3;">${op.title}</p>
|
|
485
|
+
${op.description ? `<p style="font-size:12px;color:#888;margin:0;font-family:nunito,sans-serif;line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">${op.description}</p>` : ""}
|
|
486
|
+
</div>`;
|
|
487
|
+
|
|
488
|
+
const cardRows: string[] = [];
|
|
489
|
+
for (let i = 0; i < displayOps.length; i += 2) {
|
|
490
|
+
const second = displayOps[i + 1];
|
|
491
|
+
cardRows.push(`
|
|
492
|
+
<tr>
|
|
493
|
+
<td class="sch-opp-col" style="width:50%;vertical-align:top;padding:0 6px 16px 0;">
|
|
494
|
+
${makeCard(displayOps[i])}
|
|
495
|
+
</td>
|
|
496
|
+
<td class="sch-opp-col" style="width:50%;vertical-align:top;padding:0 0 16px 6px;">
|
|
497
|
+
${second ? makeCard(second) : ""}
|
|
498
|
+
</td>
|
|
499
|
+
</tr>`);
|
|
500
|
+
}
|
|
335
501
|
|
|
336
502
|
return `
|
|
337
503
|
<div style="${s.container}">
|
|
504
|
+
<style>@media(max-width:600px){.sch-opp-col{width:100%!important;display:block!important;padding:0 0 12px 0!important;}}</style>
|
|
338
505
|
<p style="${s.title}">Opportunities Shared With Your School</p>
|
|
339
|
-
<table style="
|
|
340
|
-
|
|
341
|
-
<tr>
|
|
342
|
-
<th style="${s.th}">Opportunity</th>
|
|
343
|
-
<th style="${s.th}">Description</th>
|
|
344
|
-
<th style="${s.th}">Type</th>
|
|
345
|
-
</tr>
|
|
346
|
-
</thead>
|
|
347
|
-
<tbody>${rows}</tbody>
|
|
506
|
+
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-collapse:collapse;">
|
|
507
|
+
${cardRows.join("")}
|
|
348
508
|
</table>
|
|
349
509
|
</div>`;
|
|
350
510
|
},
|
package/src/typeDefinitions.ts
CHANGED
|
@@ -757,6 +757,7 @@ export type InstituteData = {
|
|
|
757
757
|
publicEventPage?: boolean,
|
|
758
758
|
discount?: string,
|
|
759
759
|
package: string,
|
|
760
|
+
marketingEmails: string[],
|
|
760
761
|
careersHubLinks?: {[oId: string]: {
|
|
761
762
|
exists: boolean,
|
|
762
763
|
invited: string,
|
|
@@ -1319,6 +1320,8 @@ export type EmailSectionConfig = {
|
|
|
1319
1320
|
render: (props: EmailSectionRenderProps) => string,
|
|
1320
1321
|
}
|
|
1321
1322
|
|
|
1323
|
+
export type EmailSectionNames = "upcomingEventsSection"|"attendingEventsSection"|"sharedOpportunitiesSection"|"employerDatabaseSummarySection"|"sharedOpportunitiesForSchoolSection"
|
|
1324
|
+
|
|
1322
1325
|
export type EmailTemplateConfig = {
|
|
1323
1326
|
description: string,
|
|
1324
1327
|
params: string[],
|
|
@@ -1326,7 +1329,7 @@ export type EmailTemplateConfig = {
|
|
|
1326
1329
|
secondaryButton?: {text: string, link: string},
|
|
1327
1330
|
optionalButtons?: {text: string, link: string}[],
|
|
1328
1331
|
type: "workflow"|"feedback"|"event"|"campaign"|"database",
|
|
1329
|
-
sections?:
|
|
1332
|
+
sections?: EmailSectionNames[],
|
|
1330
1333
|
}
|
|
1331
1334
|
|
|
1332
1335
|
export type EmailCampaign = {
|
package/src/util.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {where, WhereFilterOp} from "firebase/firestore";
|
|
2
2
|
import FirebaseQuery from "./firebase/firebaseQuery";
|
|
3
|
-
import {AlumniConversation, CohortData, DataViewerSelectionBackend, DataViewerSelectionWeb, EmailTemplateConfig, ExternalEvent, ExternalEventAttendee, ExternalEventCareersSchool, ExternalEventStudent, FlagCodes, InstituteData, ProviderContactData, StudentPlacementData, UserData, WorkflowStage, YearScoreData} from "./typeDefinitions";
|
|
3
|
+
import {AlumniConversation, CohortData, DataViewerSelectionBackend, DataViewerSelectionWeb, EmailSectionNames, EmailTemplateConfig, ExternalEvent, ExternalEventAttendee, ExternalEventCareersSchool, ExternalEventStudent, FlagCodes, InstituteData, ProviderContactData, StudentPlacementData, UserData, WorkflowStage, YearScoreData} from "./typeDefinitions";
|
|
4
4
|
import {capitalise, convertDate, getDateDiff} from "./firebase/util";
|
|
5
5
|
import {Descendant} from "slate";
|
|
6
6
|
import {PRIMARY_COLOUR} from "./constants";
|
|
@@ -204,7 +204,7 @@ export const getMostRecentAlumniMessage = (conversation: AlumniConversation) =>
|
|
|
204
204
|
};
|
|
205
205
|
|
|
206
206
|
|
|
207
|
-
export function buildEmailHTML({preheader, title, salutation, body, primaryColor=PRIMARY_COLOUR, primaryButton, secondaryButton, primaryImage="https://firebasestorage.googleapis.com/v0/b/placementt-dfa17.appspot.com/o/public%2FCareerThreadLogoForEmail.png?alt=media&token=046b5555-cd79-4fc9-bc89-90395e3fa351v", designatedStaffEmail, organisationName, params, data, sectionData}:{preheader: string, title: string, salutation?: string, body: string|Descendant[], primaryColor?: string, primaryButton?: {title: string, url: string}, secondaryButton?: {title: string, url: string}, primaryImage?: string, designatedStaffEmail?: string, organisationName?: string, params?: EmailTemplateConfig, data?: {[key: string]: string|undefined}, sectionData?: {[sectionId: string]: {[key: string]: unknown}}}) {
|
|
207
|
+
export async function buildEmailHTML({preheader, title, salutation, body, primaryColor=PRIMARY_COLOUR, primaryButton, secondaryButton, primaryImage="https://firebasestorage.googleapis.com/v0/b/placementt-dfa17.appspot.com/o/public%2FCareerThreadLogoForEmail.png?alt=media&token=046b5555-cd79-4fc9-bc89-90395e3fa351v", designatedStaffEmail, organisationName, params, data, sectionData, emailSectionRenderer}:{preheader: string, title: string, salutation?: string, body: string|Descendant[], primaryColor?: string, primaryButton?: {title: string, url: string}, secondaryButton?: {title: string, url: string}, primaryImage?: string, designatedStaffEmail?: string, organisationName?: string, params?: EmailTemplateConfig, data?: {[key: string]: string|undefined}, sectionData?: {[sectionId: string]: {[key: string]: unknown}}, emailSectionRenderer?: (sectionName: EmailSectionNames, data: {[key: string]: unknown}) => Promise<string>}) {
|
|
208
208
|
const serialiseSlate = (nodes: Descendant[]) => {
|
|
209
209
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
210
210
|
return nodes.map((n:any) => {
|
|
@@ -254,7 +254,7 @@ export function buildEmailHTML({preheader, title, salutation, body, primaryColor
|
|
|
254
254
|
const makeSecondaryButtonHTML = (url: string, title: string) =>
|
|
255
255
|
`<div style="text-align:center;padding:10px 0;"><a href="${url}" style="display:inline-block;font-weight:normal;color:#aeaeae;text-decoration:none;font-family:nunito,sans-serif;">${title}</a></div>`;
|
|
256
256
|
|
|
257
|
-
const getEmailContentFromTemplate = (template: string|Descendant[], params: EmailTemplateConfig, data: {[key: string]: string|undefined}) => {
|
|
257
|
+
const getEmailContentFromTemplate = async (template: string|Descendant[], params: EmailTemplateConfig, data: {[key: string]: string|undefined}) => {
|
|
258
258
|
let finalBody = typeof template === "string" ? template : serialiseSlate(template);
|
|
259
259
|
|
|
260
260
|
const allParams = [...params.params, ...Object.keys(data)];
|
|
@@ -289,17 +289,23 @@ export function buildEmailHTML({preheader, title, salutation, body, primaryColor
|
|
|
289
289
|
|
|
290
290
|
// Resolve sections
|
|
291
291
|
if (params.sections) {
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
};
|
|
292
|
+
const sectionIds = new Set<string>();
|
|
293
|
+
let m: RegExpExecArray | null;
|
|
294
|
+
const divRe = /<div[^>]*>\s*\{\{section:([^}]+)\}\}\s*<\/div>/g;
|
|
295
|
+
const inlineRe = /\{\{section:([^}]+)\}\}/g;
|
|
296
|
+
while ((m = divRe.exec(finalBody)) !== null) sectionIds.add(m[1]);
|
|
297
|
+
while ((m = inlineRe.exec(finalBody)) !== null) sectionIds.add(m[1]);
|
|
298
|
+
|
|
299
|
+
const resolvedMap: {[id: string]: string} = {};
|
|
300
|
+
await Promise.all([...sectionIds].map(async (id) => {
|
|
301
|
+
if (emailSectionRenderer) {
|
|
302
|
+
resolvedMap[id] = await emailSectionRenderer(id as EmailSectionNames, {...data, primaryColor});
|
|
303
|
+
}
|
|
304
|
+
}));
|
|
305
|
+
|
|
300
306
|
finalBody = finalBody
|
|
301
|
-
.replace(/<div[^>]*>\s*\{\{section:([^}]+)\}\}\s*<\/div>/g, (_, id) =>
|
|
302
|
-
.replace(/\{\{section:([^}]+)\}\}/g, (_, id) =>
|
|
307
|
+
.replace(/<div[^>]*>\s*\{\{section:([^}]+)\}\}\s*<\/div>/g, (_, id) => resolvedMap[id] ?? "")
|
|
308
|
+
.replace(/\{\{section:([^}]+)\}\}/g, (_, id) => resolvedMap[id] ?? "");
|
|
303
309
|
}
|
|
304
310
|
|
|
305
311
|
return finalBody;
|
|
@@ -310,7 +316,7 @@ export function buildEmailHTML({preheader, title, salutation, body, primaryColor
|
|
|
310
316
|
let formattedSecondaryButton = secondaryButton;
|
|
311
317
|
|
|
312
318
|
if (params && data) {
|
|
313
|
-
formattedBody = getEmailContentFromTemplate(body, params, data);
|
|
319
|
+
formattedBody = await getEmailContentFromTemplate(body, params, data);
|
|
314
320
|
}
|
|
315
321
|
|
|
316
322
|
const emailHeadAndStylesHTML = `
|