From a973b1316c9fb180acff32e9a8128557a6a0a2cc Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 1 Jun 2026 16:24:31 +0200 Subject: [PATCH] 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 --- src/lib/email.ts | 128 ++++++++++++++++++------ tests/unit/mentor-welcome-email.test.ts | 66 ++++++++++++ 2 files changed, 166 insertions(+), 28 deletions(-) create mode 100644 tests/unit/mentor-welcome-email.test.ts diff --git a/src/lib/email.ts b/src/lib/email.ts index d47b9a7..f6c11d5 100644 --- a/src/lib/email.ts +++ b/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 + ? `
${escapeHtml(customNote)}
` + : '' + const mentorHtmlList = mentors .map( (m) => ` ${escapeHtml(m.name ?? 'Mentor')} - - ${escapeHtml(m.email)} - + ${escapeHtml(m.email)} `, ) .join('') + const teammatesHtml = + teammates && teammates.length > 0 + ? ` +

Your team

+ ${teammates + .map( + (t) => ` + + + + `, + ) + .join('')}
${escapeHtml(t.name ?? 'Team member')}${escapeHtml(t.email)}
` + : '' + + const instructionsHtml = ` +
+ 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.
  • +
+
` + const html = ` @@ -2890,16 +2925,16 @@ function getTeamMentorIntroductionTemplate(

${recipientName ? `Hi ${escapeHtml(recipientName)},` : 'Hi there,'}

+ ${customNoteHtml}

${count === 1 ? `The mentoring round is now open and a mentor has been assigned to your project ${escapeHtml(projectTitle)}:` : `The mentoring round is now open and ${count} mentors have been assigned to your project ${escapeHtml(projectTitle)}:`}

- ${mentorHtmlList}
+ ${mentorHtmlList}
+ ${teammatesHtml} + ${instructionsHtml}

Open Mentor Workspace

-

- You can chat with them, share files, and track milestones in the workspace — or reach out to them directly by email. -

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 + ? `
${escapeHtml(customNote)}
` + : '' + const htmlList = projects - .map( - (p) => - `
  • ${escapeHtml(p.title)}
  • `, - ) + .map((p) => { + const members = + p.teamMembers && p.teamMembers.length > 0 + ? `${p.teamMembers + .map( + (m) => + ``, + ) + .join('')}
    ${escapeHtml(m.name ?? 'Team member')}${escapeHtml(m.email)}
    ` + : '' + return `
  • ${escapeHtml(p.title)}${members}
  • ` + }) .join('') + const instructionsHtml = ` +
    + 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.
    • +
    +
    ` + const html = ` @@ -2994,14 +3067,13 @@ function getMentorBulkAssignmentTemplate(

    ${name ? `Hi ${escapeHtml(name)},` : 'Hi there,'}

    + ${customNoteHtml}

    ${count === 1 ? 'You have been assigned as a mentor to a new project:' : `You have been assigned as a mentor to ${count} new projects:`}

      ${htmlList}
    + ${instructionsHtml}

    Open Mentor Dashboard

    -

    - You may have co-mentors on these teams — you can collaborate together in each project workspace. -

    Monaco Ocean Protection Challenge diff --git a/tests/unit/mentor-welcome-email.test.ts b/tests/unit/mentor-welcome-email.test.ts new file mode 100644 index 0000000..fbf601e --- /dev/null +++ b/tests/unit/mentor-welcome-email.test.ts @@ -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') + }) +})