feat(email): instructions + contact emails + optional note in mentorship templates

Export getMentorBulkAssignmentTemplate and getTeamMentorIntroductionTemplate,
adding an always-on instructions block, optional team-member/teammate contact
lists, and an optional custom note to both. Covers TDD with 4 new unit tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt
2026-06-01 16:24:31 +02:00
parent 5a9821807a
commit a973b1316c
2 changed files with 166 additions and 28 deletions

View File

@@ -2832,11 +2832,13 @@ export async function sendMentorTeamAssignmentEmail(
} }
} }
function getTeamMentorIntroductionTemplate( export function getTeamMentorIntroductionTemplate(
recipientName: string | null, recipientName: string | null,
projectTitle: string, projectTitle: string,
mentors: { name: string | null; email: string }[], mentors: { name: string | null; email: string }[],
workspaceUrl: string, workspaceUrl: string,
teammates?: { name: string | null; email: string }[],
customNote?: string,
): EmailTemplate { ): EmailTemplate {
const count = mentors.length const count = mentors.length
const subject = const subject =
@@ -2846,40 +2848,73 @@ function getTeamMentorIntroductionTemplate(
const greeting = recipientName ? `Hi ${recipientName},` : 'Hi there,' const greeting = recipientName ? `Hi ${recipientName},` : 'Hi there,'
const mentorTextLines = mentors const mentorTextLines = mentors
.map( .map((m) => `${m.name ?? 'Mentor'}${m.email}`)
(m) => `${m.name ?? 'Mentor'}${m.email}`,
)
.join('\n') .join('\n')
const teammateTextLines =
teammates && teammates.length > 0
? ['', 'Your team:', ...teammates.map((t) => `${t.name ?? 'Team member'}${t.email}`)]
: []
const text = [ const text = [
greeting, greeting,
'', '',
...(customNote ? [customNote, ''] : []),
count === 1 count === 1
? `The mentoring round is now open, and your project "${projectTitle}" has a mentor:` ? `The mentoring round is now open, and your project "${projectTitle}" has a mentor:`
: `The mentoring round is now open, and your project "${projectTitle}" has ${count} mentors:`, : `The mentoring round is now open, and your project "${projectTitle}" has ${count} mentors:`,
'', '',
mentorTextLines, mentorTextLines,
...teammateTextLines,
'', '',
'You can chat with them, share files, and track milestones in your mentor workspace:', 'Working with your mentor:',
workspaceUrl, ' - Go to the Mentoring section of your applicant portal to message your mentor directly — they are notified by email when you write.',
' - Share documents and questions early; your mentor is here to help you sharpen your project before the finals.',
' - You can also email your mentor directly using the address above.',
'', '',
'Feel free to reach out to them directly by email as well.', `Open your mentoring page: ${workspaceUrl}`,
'', '',
'The MOPC team', 'The MOPC team',
].join('\n') ].join('\n')
const customNoteHtml = customNote
? `<div style="margin:0 0 16px;padding:12px 16px;background:#fff7ed;border-left:3px solid #de0f1e;border-radius:4px;color:#0f172a;">${escapeHtml(customNote)}</div>`
: ''
const mentorHtmlList = mentors const mentorHtmlList = mentors
.map( .map(
(m) => ` (m) => `
<tr> <tr>
<td style="padding:6px 0;font-weight:600;color:#0f172a;">${escapeHtml(m.name ?? 'Mentor')}</td> <td style="padding:6px 0;font-weight:600;color:#0f172a;">${escapeHtml(m.name ?? 'Mentor')}</td>
<td style="padding:6px 0;"> <td style="padding:6px 0;"><a href="mailto:${escapeHtml(m.email)}" style="color:#053d57;text-decoration:none;">${escapeHtml(m.email)}</a></td>
<a href="mailto:${escapeHtml(m.email)}" style="color:#053d57;text-decoration:none;">${escapeHtml(m.email)}</a>
</td>
</tr>`, </tr>`,
) )
.join('') .join('')
const teammatesHtml =
teammates && teammates.length > 0
? `
<h2 style="margin:24px 0 8px;color:#0f172a;font-size:15px;font-weight:600;">Your team</h2>
<table style="width:100%;border-collapse:collapse;margin:0 0 8px;font-size:14px;">${teammates
.map(
(t) => `
<tr>
<td style="padding:6px 0;color:#0f172a;">${escapeHtml(t.name ?? 'Team member')}</td>
<td style="padding:6px 0;"><a href="mailto:${escapeHtml(t.email)}" style="color:#053d57;text-decoration:none;">${escapeHtml(t.email)}</a></td>
</tr>`,
)
.join('')}</table>`
: ''
const instructionsHtml = `
<div style="margin:16px 0;padding:12px 16px;background:#eff6ff;border-left:3px solid #3b82f6;border-radius:4px;">
<strong style="color:#1e40af;">Working with your mentor</strong>
<ul style="margin:8px 0 0 20px;padding:0;color:#0f172a;font-size:13px;">
<li style="margin:4px 0;">Go to the Mentoring section of your applicant portal to message your mentor directly — they are notified by email when you write.</li>
<li style="margin:4px 0;">Share documents and questions early; your mentor is here to help you sharpen your project before the finals.</li>
<li style="margin:4px 0;">You can also email your mentor directly using the address above.</li>
</ul>
</div>`
const html = ` const html = `
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
@@ -2890,16 +2925,16 @@ function getTeamMentorIntroductionTemplate(
</div> </div>
<div style="padding:24px 28px;line-height:1.5;font-size:14px;"> <div style="padding:24px 28px;line-height:1.5;font-size:14px;">
<p style="margin-top:0;">${recipientName ? `Hi ${escapeHtml(recipientName)},` : 'Hi there,'}</p> <p style="margin-top:0;">${recipientName ? `Hi ${escapeHtml(recipientName)},` : 'Hi there,'}</p>
${customNoteHtml}
<p>${count === 1 <p>${count === 1
? `The mentoring round is now open and a mentor has been assigned to your project <strong>${escapeHtml(projectTitle)}</strong>:` ? `The mentoring round is now open and a mentor has been assigned to your project <strong>${escapeHtml(projectTitle)}</strong>:`
: `The mentoring round is now open and <strong>${count}</strong> mentors have been assigned to your project <strong>${escapeHtml(projectTitle)}</strong>:`}</p> : `The mentoring round is now open and <strong>${count}</strong> mentors have been assigned to your project <strong>${escapeHtml(projectTitle)}</strong>:`}</p>
<table style="width:100%;border-collapse:collapse;margin:12px 0 20px;font-size:14px;">${mentorHtmlList}</table> <table style="width:100%;border-collapse:collapse;margin:12px 0 8px;font-size:14px;">${mentorHtmlList}</table>
${teammatesHtml}
${instructionsHtml}
<p style="margin-top:24px;"> <p style="margin-top:24px;">
<a href="${workspaceUrl}" style="display:inline-block;padding:10px 20px;background:#de0f1e;color:#fff;text-decoration:none;border-radius:6px;font-weight:600;">Open Mentor Workspace</a> <a href="${workspaceUrl}" style="display:inline-block;padding:10px 20px;background:#de0f1e;color:#fff;text-decoration:none;border-radius:6px;font-weight:600;">Open Mentor Workspace</a>
</p> </p>
<p style="margin-top:24px;color:#64748b;font-size:12px;">
You can chat with them, share files, and track milestones in the workspace — or reach out to them directly by email.
</p>
</div> </div>
<div style="padding:16px 28px;background:#f1f5f9;color:#64748b;font-size:12px;text-align:center;"> <div style="padding:16px 28px;background:#f1f5f9;color:#64748b;font-size:12px;text-align:center;">
Monaco Ocean Protection Challenge Monaco Ocean Protection Challenge
@@ -2946,10 +2981,15 @@ export async function sendTeamMentorIntroductionEmail(
} }
} }
function getMentorBulkAssignmentTemplate( export function getMentorBulkAssignmentTemplate(
name: string, name: string,
projects: { title: string; url: string }[], projects: {
title: string
url: string
teamMembers?: { name: string | null; email: string }[]
}[],
mentorDashboardUrl: string, mentorDashboardUrl: string,
customNote?: string,
): EmailTemplate { ): EmailTemplate {
const count = projects.length const count = projects.length
const subject = const subject =
@@ -2958,32 +2998,65 @@ function getMentorBulkAssignmentTemplate(
: `You've been assigned to ${count} new MOPC projects` : `You've been assigned to ${count} new MOPC projects`
const greeting = name ? `Hi ${name},` : 'Hi there,' const greeting = name ? `Hi ${name},` : 'Hi there,'
const textLines = projects const textBlocks = projects.map((p) => {
.map((p) => `${p.title}${p.url}`) const members =
.join('\n') p.teamMembers && p.teamMembers.length > 0
? '\n' +
p.teamMembers
.map((m) => ` - ${m.name ?? 'Team member'}: ${m.email}`)
.join('\n')
: ''
return `${p.title}${p.url}${members}`
})
const text = [ const text = [
greeting, greeting,
'', '',
...(customNote ? [customNote, ''] : []),
count === 1 count === 1
? `You have been assigned as a mentor to a new project:` ? `You have been assigned as a mentor to a new project:`
: `You have been assigned as a mentor to ${count} new projects:`, : `You have been assigned as a mentor to ${count} new projects:`,
'', '',
textLines, ...textBlocks,
'', '',
'You may have co-mentors on these teams — you can collaborate together in each project workspace.', 'How to mentor on MOPC:',
' - Open each project workspace from your Mentor Dashboard to chat with the team, share files, and track milestones.',
' - Messages you send in the workspace notify the team by email automatically.',
' - You can also email team members directly using the addresses listed above.',
'', '',
`Open your mentor dashboard: ${mentorDashboardUrl}`, `Open your mentor dashboard: ${mentorDashboardUrl}`,
'', '',
'The MOPC team', 'The MOPC team',
].join('\n') ].join('\n')
const customNoteHtml = customNote
? `<div style="margin:0 0 16px;padding:12px 16px;background:#fff7ed;border-left:3px solid #de0f1e;border-radius:4px;color:#0f172a;">${escapeHtml(customNote)}</div>`
: ''
const htmlList = projects const htmlList = projects
.map( .map((p) => {
(p) => const members =
`<li style="margin:6px 0;"><a href="${p.url}" style="color:#053d57;text-decoration:none;font-weight:600;">${escapeHtml(p.title)}</a></li>`, p.teamMembers && p.teamMembers.length > 0
) ? `<table style="width:100%;border-collapse:collapse;margin:6px 0 0;font-size:13px;">${p.teamMembers
.map(
(m) =>
`<tr><td style="padding:3px 0;color:#0f172a;">${escapeHtml(m.name ?? 'Team member')}</td><td style="padding:3px 0;"><a href="mailto:${escapeHtml(m.email)}" style="color:#053d57;text-decoration:none;">${escapeHtml(m.email)}</a></td></tr>`,
)
.join('')}</table>`
: ''
return `<li style="margin:10px 0;"><a href="${p.url}" style="color:#053d57;text-decoration:none;font-weight:600;">${escapeHtml(p.title)}</a>${members}</li>`
})
.join('') .join('')
const instructionsHtml = `
<div style="margin:16px 0;padding:12px 16px;background:#eff6ff;border-left:3px solid #3b82f6;border-radius:4px;">
<strong style="color:#1e40af;">How to mentor on MOPC</strong>
<ul style="margin:8px 0 0 20px;padding:0;color:#0f172a;font-size:13px;">
<li style="margin:4px 0;">Open each project workspace from your Mentor Dashboard to chat with the team, share files, and track milestones.</li>
<li style="margin:4px 0;">Messages you send in the workspace notify the team by email automatically.</li>
<li style="margin:4px 0;">You can also email team members directly using the addresses listed above.</li>
</ul>
</div>`
const html = ` const html = `
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
@@ -2994,14 +3067,13 @@ function getMentorBulkAssignmentTemplate(
</div> </div>
<div style="padding:24px 28px;line-height:1.5;font-size:14px;"> <div style="padding:24px 28px;line-height:1.5;font-size:14px;">
<p style="margin-top:0;">${name ? `Hi ${escapeHtml(name)},` : 'Hi there,'}</p> <p style="margin-top:0;">${name ? `Hi ${escapeHtml(name)},` : 'Hi there,'}</p>
${customNoteHtml}
<p>${count === 1 ? 'You have been assigned as a mentor to a new project:' : `You have been assigned as a mentor to <strong>${count}</strong> new projects:`}</p> <p>${count === 1 ? 'You have been assigned as a mentor to a new project:' : `You have been assigned as a mentor to <strong>${count}</strong> new projects:`}</p>
<ul style="padding-left:20px;margin:12px 0 20px;">${htmlList}</ul> <ul style="padding-left:20px;margin:12px 0 20px;">${htmlList}</ul>
${instructionsHtml}
<p style="margin-top:24px;"> <p style="margin-top:24px;">
<a href="${mentorDashboardUrl}" style="display:inline-block;padding:10px 20px;background:#de0f1e;color:#fff;text-decoration:none;border-radius:6px;font-weight:600;">Open Mentor Dashboard</a> <a href="${mentorDashboardUrl}" style="display:inline-block;padding:10px 20px;background:#de0f1e;color:#fff;text-decoration:none;border-radius:6px;font-weight:600;">Open Mentor Dashboard</a>
</p> </p>
<p style="margin-top:24px;color:#64748b;font-size:12px;">
You may have co-mentors on these teams — you can collaborate together in each project workspace.
</p>
</div> </div>
<div style="padding:16px 28px;background:#f1f5f9;color:#64748b;font-size:12px;text-align:center;"> <div style="padding:16px 28px;background:#f1f5f9;color:#64748b;font-size:12px;text-align:center;">
Monaco Ocean Protection Challenge Monaco Ocean Protection Challenge

View File

@@ -0,0 +1,66 @@
import { describe, it, expect } from 'vitest'
import {
getMentorBulkAssignmentTemplate,
getTeamMentorIntroductionTemplate,
} from '@/lib/email'
describe('getMentorBulkAssignmentTemplate', () => {
it('includes team-member emails, the instructions block, and a custom note', () => {
const t = getMentorBulkAssignmentTemplate(
'Alice',
[
{
title: 'Reef Project',
url: 'https://x/mentor/workspace/1',
teamMembers: [{ name: 'Bob', email: 'bob@team.com' }],
},
],
'https://x/mentor',
'Please reach out this week.',
)
expect(t.html).toContain('bob@team.com')
expect(t.html).toContain('How to mentor on MOPC')
expect(t.html).toContain('Please reach out this week.')
expect(t.text).toContain('bob@team.com')
expect(t.text).toContain('How to mentor on MOPC')
})
it('renders without team members or note (backward compatible)', () => {
const t = getMentorBulkAssignmentTemplate(
'Alice',
[{ title: 'P', url: 'https://x/p' }],
'https://x/mentor',
)
expect(t.html).toContain('How to mentor on MOPC')
// Custom-note box uses the #fff7ed background; absent when no note passed.
expect(t.html).not.toContain('#fff7ed')
})
})
describe('getTeamMentorIntroductionTemplate', () => {
it('includes mentor + teammate emails, the instructions block, and a custom note', () => {
const t = getTeamMentorIntroductionTemplate(
'Bob',
'Reef Project',
[{ name: 'Alice', email: 'alice@mentor.com' }],
'https://x/applicant/mentor',
[{ name: 'Carol', email: 'carol@team.com' }],
'Welcome aboard!',
)
expect(t.html).toContain('alice@mentor.com')
expect(t.html).toContain('carol@team.com')
expect(t.html).toContain('Working with your mentor')
expect(t.html).toContain('Welcome aboard!')
})
it('renders without teammates or note (backward compatible)', () => {
const t = getTeamMentorIntroductionTemplate(
'Bob',
'Reef Project',
[{ name: 'Alice', email: 'alice@mentor.com' }],
'https://x/applicant/mentor',
)
expect(t.html).toContain('Working with your mentor')
expect(t.html).not.toContain('#fff7ed')
})
})