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:
128
src/lib/email.ts
128
src/lib/email.ts
@@ -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
|
||||||
|
|||||||
66
tests/unit/mentor-welcome-email.test.ts
Normal file
66
tests/unit/mentor-welcome-email.test.ts
Normal 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')
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user