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,
|
||||
projectTitle: string,
|
||||
mentors: { name: string | null; email: string }[],
|
||||
workspaceUrl: string,
|
||||
teammates?: { name: string | null; email: string }[],
|
||||
customNote?: string,
|
||||
): EmailTemplate {
|
||||
const count = mentors.length
|
||||
const subject =
|
||||
@@ -2846,40 +2848,73 @@ function getTeamMentorIntroductionTemplate(
|
||||
const greeting = recipientName ? `Hi ${recipientName},` : 'Hi there,'
|
||||
|
||||
const mentorTextLines = mentors
|
||||
.map(
|
||||
(m) => ` • ${m.name ?? 'Mentor'} — ${m.email}`,
|
||||
)
|
||||
.map((m) => ` • ${m.name ?? 'Mentor'} — ${m.email}`)
|
||||
.join('\n')
|
||||
const teammateTextLines =
|
||||
teammates && teammates.length > 0
|
||||
? ['', 'Your team:', ...teammates.map((t) => ` • ${t.name ?? 'Team member'} — ${t.email}`)]
|
||||
: []
|
||||
|
||||
const text = [
|
||||
greeting,
|
||||
'',
|
||||
...(customNote ? [customNote, ''] : []),
|
||||
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 ${count} mentors:`,
|
||||
'',
|
||||
mentorTextLines,
|
||||
...teammateTextLines,
|
||||
'',
|
||||
'You can chat with them, share files, and track milestones in your mentor workspace:',
|
||||
workspaceUrl,
|
||||
'Working with your mentor:',
|
||||
' - 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',
|
||||
].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
|
||||
.map(
|
||||
(m) => `
|
||||
<tr>
|
||||
<td style="padding:6px 0;font-weight:600;color:#0f172a;">${escapeHtml(m.name ?? 'Mentor')}</td>
|
||||
<td style="padding:6px 0;">
|
||||
<a href="mailto:${escapeHtml(m.email)}" style="color:#053d57;text-decoration:none;">${escapeHtml(m.email)}</a>
|
||||
</td>
|
||||
<td style="padding:6px 0;"><a href="mailto:${escapeHtml(m.email)}" style="color:#053d57;text-decoration:none;">${escapeHtml(m.email)}</a></td>
|
||||
</tr>`,
|
||||
)
|
||||
.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 = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
@@ -2890,16 +2925,16 @@ function getTeamMentorIntroductionTemplate(
|
||||
</div>
|
||||
<div style="padding:24px 28px;line-height:1.5;font-size:14px;">
|
||||
<p style="margin-top:0;">${recipientName ? `Hi ${escapeHtml(recipientName)},` : 'Hi there,'}</p>
|
||||
${customNoteHtml}
|
||||
<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 <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;">
|
||||
<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 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 style="padding:16px 28px;background:#f1f5f9;color:#64748b;font-size:12px;text-align:center;">
|
||||
Monaco Ocean Protection Challenge
|
||||
@@ -2946,10 +2981,15 @@ export async function sendTeamMentorIntroductionEmail(
|
||||
}
|
||||
}
|
||||
|
||||
function getMentorBulkAssignmentTemplate(
|
||||
export function getMentorBulkAssignmentTemplate(
|
||||
name: string,
|
||||
projects: { title: string; url: string }[],
|
||||
projects: {
|
||||
title: string
|
||||
url: string
|
||||
teamMembers?: { name: string | null; email: string }[]
|
||||
}[],
|
||||
mentorDashboardUrl: string,
|
||||
customNote?: string,
|
||||
): EmailTemplate {
|
||||
const count = projects.length
|
||||
const subject =
|
||||
@@ -2958,32 +2998,65 @@ function getMentorBulkAssignmentTemplate(
|
||||
: `You've been assigned to ${count} new MOPC projects`
|
||||
const greeting = name ? `Hi ${name},` : 'Hi there,'
|
||||
|
||||
const textLines = projects
|
||||
.map((p) => ` • ${p.title} — ${p.url}`)
|
||||
.join('\n')
|
||||
const textBlocks = projects.map((p) => {
|
||||
const members =
|
||||
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 = [
|
||||
greeting,
|
||||
'',
|
||||
...(customNote ? [customNote, ''] : []),
|
||||
count === 1
|
||||
? `You have been assigned as a mentor to a new project:`
|
||||
: `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}`,
|
||||
'',
|
||||
'The MOPC team',
|
||||
].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
|
||||
.map(
|
||||
(p) =>
|
||||
`<li style="margin:6px 0;"><a href="${p.url}" style="color:#053d57;text-decoration:none;font-weight:600;">${escapeHtml(p.title)}</a></li>`,
|
||||
)
|
||||
.map((p) => {
|
||||
const members =
|
||||
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('')
|
||||
|
||||
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 = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
@@ -2994,14 +3067,13 @@ function getMentorBulkAssignmentTemplate(
|
||||
</div>
|
||||
<div style="padding:24px 28px;line-height:1.5;font-size:14px;">
|
||||
<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>
|
||||
<ul style="padding-left:20px;margin:12px 0 20px;">${htmlList}</ul>
|
||||
${instructionsHtml}
|
||||
<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>
|
||||
</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 style="padding:16px 28px;background:#f1f5f9;color:#64748b;font-size:12px;text-align:center;">
|
||||
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