Add styled notification emails and round-attached notifications
- Add 15+ styled email templates matching existing invite email design - Wire up notification triggers in all routers (assignment, round, project, mentor, application, onboarding) - Add test email button for each notification type in admin settings - Add round-attached notifications: admins can configure which notification to send when projects enter a round - Fall back to status-based notifications when round has no configured notification Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
979
src/lib/email.ts
979
src/lib/email.ts
@@ -566,6 +566,985 @@ Together for a healthier ocean.
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Notification Email Templates
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Context passed to notification email templates
|
||||
*/
|
||||
export interface NotificationEmailContext {
|
||||
name?: string
|
||||
title: string
|
||||
message: string
|
||||
linkUrl?: string
|
||||
linkLabel?: string
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate "Advanced to Semi-Finals" email template
|
||||
*/
|
||||
function getAdvancedSemifinalTemplate(
|
||||
name: string,
|
||||
projectName: string,
|
||||
programName: string,
|
||||
nextSteps?: string
|
||||
): EmailTemplate {
|
||||
const greeting = name ? `Congratulations ${name}!` : 'Congratulations!'
|
||||
|
||||
const celebrationBanner = `
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="margin: 20px 0;">
|
||||
<tr>
|
||||
<td style="background: linear-gradient(135deg, #059669 0%, #0d9488 100%); border-radius: 12px; padding: 24px; text-align: center;">
|
||||
<p style="color: #ffffff; margin: 0 0 8px 0; font-size: 14px; font-weight: 600; text-transform: uppercase; letter-spacing: 2px;">Exciting News</p>
|
||||
<h2 style="color: #ffffff; margin: 0; font-size: 28px; font-weight: 700;">You're a Semi-Finalist!</h2>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
`
|
||||
|
||||
const content = `
|
||||
${sectionTitle(greeting)}
|
||||
${celebrationBanner}
|
||||
${paragraph(`Your project <strong style="color: ${BRAND.darkBlue};">"${projectName}"</strong> has been selected to advance to the semi-finals of ${programName}.`)}
|
||||
${infoBox('Your innovative approach to ocean protection stood out among hundreds of submissions.', 'success')}
|
||||
${nextSteps ? paragraph(`<strong>Next Steps:</strong> ${nextSteps}`) : paragraph('Our team will be in touch shortly with details about the next phase of the competition.')}
|
||||
`
|
||||
|
||||
return {
|
||||
subject: `Congratulations! "${projectName}" advances to Semi-Finals`,
|
||||
html: getEmailWrapper(content),
|
||||
text: `
|
||||
${greeting}
|
||||
|
||||
Your project "${projectName}" has been selected to advance to the semi-finals of ${programName}.
|
||||
|
||||
Your innovative approach to ocean protection stood out among hundreds of submissions.
|
||||
|
||||
${nextSteps || 'Our team will be in touch shortly with details about the next phase of the competition.'}
|
||||
|
||||
---
|
||||
Monaco Ocean Protection Challenge
|
||||
Together for a healthier ocean.
|
||||
`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate "Selected as Finalist" email template
|
||||
*/
|
||||
function getAdvancedFinalTemplate(
|
||||
name: string,
|
||||
projectName: string,
|
||||
programName: string,
|
||||
nextSteps?: string
|
||||
): EmailTemplate {
|
||||
const greeting = name ? `Incredible news, ${name}!` : 'Incredible news!'
|
||||
|
||||
const celebrationBanner = `
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="margin: 20px 0;">
|
||||
<tr>
|
||||
<td style="background: linear-gradient(135deg, ${BRAND.darkBlue} 0%, #0891b2 100%); border-radius: 12px; padding: 24px; text-align: center;">
|
||||
<p style="color: #ffffff; margin: 0 0 8px 0; font-size: 14px; font-weight: 600; text-transform: uppercase; letter-spacing: 2px;">Outstanding Achievement</p>
|
||||
<h2 style="color: #ffffff; margin: 0; font-size: 28px; font-weight: 700;">You're a Finalist!</h2>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
`
|
||||
|
||||
const content = `
|
||||
${sectionTitle(greeting)}
|
||||
${celebrationBanner}
|
||||
${paragraph(`Your project <strong style="color: ${BRAND.darkBlue};">"${projectName}"</strong> has been selected as a <strong>Finalist</strong> in ${programName}.`)}
|
||||
${infoBox('You are now among the top projects competing for the grand prize!', 'success')}
|
||||
${nextSteps ? paragraph(`<strong>What Happens Next:</strong> ${nextSteps}`) : paragraph('Prepare for the final stage of the competition. Our team will contact you with details about the finalist presentations and judging.')}
|
||||
`
|
||||
|
||||
return {
|
||||
subject: `You're a Finalist! "${projectName}" selected for finals`,
|
||||
html: getEmailWrapper(content),
|
||||
text: `
|
||||
${greeting}
|
||||
|
||||
Your project "${projectName}" has been selected as a Finalist in ${programName}.
|
||||
|
||||
You are now among the top projects competing for the grand prize!
|
||||
|
||||
${nextSteps || 'Prepare for the final stage of the competition. Our team will contact you with details about the finalist presentations and judging.'}
|
||||
|
||||
---
|
||||
Monaco Ocean Protection Challenge
|
||||
Together for a healthier ocean.
|
||||
`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate "Mentor Assigned" email template (for team)
|
||||
*/
|
||||
function getMentorAssignedTemplate(
|
||||
name: string,
|
||||
projectName: string,
|
||||
mentorName: string,
|
||||
mentorBio?: string
|
||||
): EmailTemplate {
|
||||
const greeting = name ? `Hello ${name},` : 'Hello,'
|
||||
|
||||
const mentorCard = `
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="margin: 20px 0;">
|
||||
<tr>
|
||||
<td style="background-color: ${BRAND.lightGray}; border-radius: 12px; padding: 24px;">
|
||||
<p style="color: ${BRAND.textMuted}; margin: 0 0 8px 0; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px;">Your Mentor</p>
|
||||
<p style="color: ${BRAND.darkBlue}; margin: 0 0 12px 0; font-size: 20px; font-weight: 700;">${mentorName}</p>
|
||||
${mentorBio ? `<p style="color: ${BRAND.textDark}; margin: 0; font-size: 14px; line-height: 1.5;">${mentorBio}</p>` : ''}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
`
|
||||
|
||||
const content = `
|
||||
${sectionTitle(greeting)}
|
||||
${paragraph(`Great news! A mentor has been assigned to support your project <strong style="color: ${BRAND.darkBlue};">"${projectName}"</strong>.`)}
|
||||
${mentorCard}
|
||||
${paragraph('Your mentor will provide guidance, feedback, and support as you develop your ocean protection initiative. They will reach out to you shortly to introduce themselves and schedule your first meeting.')}
|
||||
${infoBox('Mentorship is a valuable opportunity - make the most of their expertise!', 'info')}
|
||||
`
|
||||
|
||||
return {
|
||||
subject: `A mentor has been assigned to "${projectName}"`,
|
||||
html: getEmailWrapper(content),
|
||||
text: `
|
||||
${greeting}
|
||||
|
||||
Great news! A mentor has been assigned to support your project "${projectName}".
|
||||
|
||||
Your Mentor: ${mentorName}
|
||||
${mentorBio || ''}
|
||||
|
||||
Your mentor will provide guidance, feedback, and support as you develop your ocean protection initiative. They will reach out to you shortly to introduce themselves and schedule your first meeting.
|
||||
|
||||
---
|
||||
Monaco Ocean Protection Challenge
|
||||
Together for a healthier ocean.
|
||||
`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate "Not Selected" email template
|
||||
*/
|
||||
function getNotSelectedTemplate(
|
||||
name: string,
|
||||
projectName: string,
|
||||
roundName: string,
|
||||
feedbackUrl?: string,
|
||||
encouragement?: string
|
||||
): EmailTemplate {
|
||||
const greeting = name ? `Dear ${name},` : 'Dear Applicant,'
|
||||
|
||||
const content = `
|
||||
${sectionTitle(greeting)}
|
||||
${paragraph(`Thank you for participating in ${roundName} with your project <strong>"${projectName}"</strong>.`)}
|
||||
${paragraph('After careful consideration by our jury, we regret to inform you that your project was not selected to advance to the next round.')}
|
||||
${infoBox('This decision was incredibly difficult given the high quality of submissions we received this year.', 'info')}
|
||||
${feedbackUrl ? ctaButton(feedbackUrl, 'View Jury Feedback') : ''}
|
||||
${paragraph(encouragement || 'We encourage you to continue developing your ocean protection initiative and to apply again in future editions. Your commitment to protecting our oceans is valuable and appreciated.')}
|
||||
<p style="color: ${BRAND.textMuted}; margin: 24px 0 0 0; font-size: 14px; text-align: center;">
|
||||
Thank you for being part of the Monaco Ocean Protection Challenge community.
|
||||
</p>
|
||||
`
|
||||
|
||||
return {
|
||||
subject: `Update on your application: "${projectName}"`,
|
||||
html: getEmailWrapper(content),
|
||||
text: `
|
||||
${greeting}
|
||||
|
||||
Thank you for participating in ${roundName} with your project "${projectName}".
|
||||
|
||||
After careful consideration by our jury, we regret to inform you that your project was not selected to advance to the next round.
|
||||
|
||||
This decision was incredibly difficult given the high quality of submissions we received this year.
|
||||
|
||||
${feedbackUrl ? `View jury feedback: ${feedbackUrl}` : ''}
|
||||
|
||||
${encouragement || 'We encourage you to continue developing your ocean protection initiative and to apply again in future editions.'}
|
||||
|
||||
Thank you for being part of the Monaco Ocean Protection Challenge community.
|
||||
|
||||
---
|
||||
Monaco Ocean Protection Challenge
|
||||
Together for a healthier ocean.
|
||||
`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate "Winner Announcement" email template
|
||||
*/
|
||||
function getWinnerAnnouncementTemplate(
|
||||
name: string,
|
||||
projectName: string,
|
||||
awardName: string,
|
||||
prizeDetails?: string
|
||||
): EmailTemplate {
|
||||
const greeting = name ? `Congratulations ${name}!` : 'Congratulations!'
|
||||
|
||||
const trophyBanner = `
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="margin: 20px 0;">
|
||||
<tr>
|
||||
<td style="background: linear-gradient(135deg, #f59e0b 0%, #eab308 100%); border-radius: 12px; padding: 32px; text-align: center;">
|
||||
<p style="color: #ffffff; font-size: 48px; margin: 0 0 12px 0;">🏆</p>
|
||||
<p style="color: #ffffff; margin: 0 0 8px 0; font-size: 14px; font-weight: 600; text-transform: uppercase; letter-spacing: 2px;">Winner</p>
|
||||
<h2 style="color: #ffffff; margin: 0; font-size: 24px; font-weight: 700;">${awardName}</h2>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
`
|
||||
|
||||
const content = `
|
||||
${sectionTitle(greeting)}
|
||||
${trophyBanner}
|
||||
${paragraph(`We are thrilled to announce that your project <strong style="color: ${BRAND.darkBlue};">"${projectName}"</strong> has been selected as the winner of the <strong>${awardName}</strong>!`)}
|
||||
${infoBox('Your outstanding work in ocean protection has made a lasting impression on our jury.', 'success')}
|
||||
${prizeDetails ? paragraph(`<strong>Your Prize:</strong> ${prizeDetails}`) : ''}
|
||||
${paragraph('Our team will be in touch shortly with details about the award ceremony and next steps.')}
|
||||
`
|
||||
|
||||
return {
|
||||
subject: `You Won! "${projectName}" wins ${awardName}`,
|
||||
html: getEmailWrapper(content),
|
||||
text: `
|
||||
${greeting}
|
||||
|
||||
We are thrilled to announce that your project "${projectName}" has been selected as the winner of the ${awardName}!
|
||||
|
||||
Your outstanding work in ocean protection has made a lasting impression on our jury.
|
||||
|
||||
${prizeDetails ? `Your Prize: ${prizeDetails}` : ''}
|
||||
|
||||
Our team will be in touch shortly with details about the award ceremony and next steps.
|
||||
|
||||
---
|
||||
Monaco Ocean Protection Challenge
|
||||
Together for a healthier ocean.
|
||||
`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate "Assigned to Project" email template (for jury)
|
||||
*/
|
||||
function getAssignedToProjectTemplate(
|
||||
name: string,
|
||||
projectName: string,
|
||||
roundName: string,
|
||||
deadline?: string,
|
||||
assignmentsUrl?: string
|
||||
): EmailTemplate {
|
||||
const greeting = name ? `Hello ${name},` : 'Hello,'
|
||||
|
||||
const projectCard = `
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="margin: 20px 0;">
|
||||
<tr>
|
||||
<td style="background-color: ${BRAND.lightGray}; border-left: 4px solid ${BRAND.teal}; border-radius: 0 12px 12px 0; padding: 20px 24px;">
|
||||
<p style="color: ${BRAND.textMuted}; margin: 0 0 4px 0; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px;">New Assignment</p>
|
||||
<p style="color: ${BRAND.darkBlue}; margin: 0; font-size: 18px; font-weight: 700;">${projectName}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
`
|
||||
|
||||
const deadlineBox = deadline ? `
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="margin: 20px 0;">
|
||||
<tr>
|
||||
<td style="background-color: #fef2f2; border-left: 4px solid ${BRAND.red}; border-radius: 0 8px 8px 0; padding: 16px 20px;">
|
||||
<p style="color: #991b1b; margin: 0 0 4px 0; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px;">Deadline</p>
|
||||
<p style="color: #7f1d1d; margin: 0; font-size: 16px; font-weight: 700;">${deadline}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
` : ''
|
||||
|
||||
const content = `
|
||||
${sectionTitle(greeting)}
|
||||
${paragraph(`You have been assigned a new project to evaluate for <strong style="color: ${BRAND.darkBlue};">${roundName}</strong>.`)}
|
||||
${projectCard}
|
||||
${deadlineBox}
|
||||
${paragraph('Please review the project materials and submit your evaluation before the deadline.')}
|
||||
${assignmentsUrl ? ctaButton(assignmentsUrl, 'View Assignment') : ''}
|
||||
`
|
||||
|
||||
return {
|
||||
subject: `New Assignment: "${projectName}" - ${roundName}`,
|
||||
html: getEmailWrapper(content),
|
||||
text: `
|
||||
${greeting}
|
||||
|
||||
You have been assigned a new project to evaluate for ${roundName}.
|
||||
|
||||
Project: ${projectName}
|
||||
${deadline ? `Deadline: ${deadline}` : ''}
|
||||
|
||||
Please review the project materials and submit your evaluation before the deadline.
|
||||
|
||||
${assignmentsUrl ? `View assignment: ${assignmentsUrl}` : ''}
|
||||
|
||||
---
|
||||
Monaco Ocean Protection Challenge
|
||||
Together for a healthier ocean.
|
||||
`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate "Batch Assigned" email template (for jury)
|
||||
*/
|
||||
function getBatchAssignedTemplate(
|
||||
name: string,
|
||||
projectCount: number,
|
||||
roundName: string,
|
||||
deadline?: string,
|
||||
assignmentsUrl?: string
|
||||
): EmailTemplate {
|
||||
const greeting = name ? `Hello ${name},` : 'Hello,'
|
||||
|
||||
const deadlineBox = deadline ? `
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="margin: 20px 0;">
|
||||
<tr>
|
||||
<td style="background-color: #fef2f2; border-left: 4px solid ${BRAND.red}; border-radius: 0 8px 8px 0; padding: 16px 20px;">
|
||||
<p style="color: #991b1b; margin: 0 0 4px 0; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px;">Deadline</p>
|
||||
<p style="color: #7f1d1d; margin: 0; font-size: 16px; font-weight: 700;">${deadline}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
` : ''
|
||||
|
||||
const content = `
|
||||
${sectionTitle(greeting)}
|
||||
${paragraph(`You have been assigned projects to evaluate for <strong style="color: ${BRAND.darkBlue};">${roundName}</strong>.`)}
|
||||
${statCard('Projects Assigned', projectCount)}
|
||||
${deadlineBox}
|
||||
${paragraph('Please review each project and submit your evaluations before the deadline. Your expert assessment is crucial to identifying the most promising ocean protection initiatives.')}
|
||||
${assignmentsUrl ? ctaButton(assignmentsUrl, 'View All Assignments') : ''}
|
||||
`
|
||||
|
||||
return {
|
||||
subject: `${projectCount} Projects Assigned - ${roundName}`,
|
||||
html: getEmailWrapper(content),
|
||||
text: `
|
||||
${greeting}
|
||||
|
||||
You have been assigned ${projectCount} project${projectCount !== 1 ? 's' : ''} to evaluate for ${roundName}.
|
||||
|
||||
${deadline ? `Deadline: ${deadline}` : ''}
|
||||
|
||||
Please review each project and submit your evaluations before the deadline.
|
||||
|
||||
${assignmentsUrl ? `View assignments: ${assignmentsUrl}` : ''}
|
||||
|
||||
---
|
||||
Monaco Ocean Protection Challenge
|
||||
Together for a healthier ocean.
|
||||
`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate "Round Now Open" email template (for jury)
|
||||
*/
|
||||
function getRoundNowOpenTemplate(
|
||||
name: string,
|
||||
roundName: string,
|
||||
projectCount: number,
|
||||
deadline?: string,
|
||||
assignmentsUrl?: string
|
||||
): EmailTemplate {
|
||||
const greeting = name ? `Hello ${name},` : 'Hello,'
|
||||
|
||||
const openBanner = `
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="margin: 20px 0;">
|
||||
<tr>
|
||||
<td style="background: linear-gradient(135deg, ${BRAND.teal} 0%, #0891b2 100%); border-radius: 12px; padding: 24px; text-align: center;">
|
||||
<p style="color: #ffffff; margin: 0 0 8px 0; font-size: 14px; font-weight: 600; text-transform: uppercase; letter-spacing: 2px;">Evaluation Round</p>
|
||||
<h2 style="color: #ffffff; margin: 0; font-size: 24px; font-weight: 700;">${roundName} is Now Open</h2>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
`
|
||||
|
||||
const deadlineBox = deadline ? `
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="margin: 20px 0;">
|
||||
<tr>
|
||||
<td style="background-color: #fef2f2; border-left: 4px solid ${BRAND.red}; border-radius: 0 8px 8px 0; padding: 16px 20px;">
|
||||
<p style="color: #991b1b; margin: 0 0 4px 0; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px;">Deadline</p>
|
||||
<p style="color: #7f1d1d; margin: 0; font-size: 16px; font-weight: 700;">${deadline}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
` : ''
|
||||
|
||||
const content = `
|
||||
${sectionTitle(greeting)}
|
||||
${openBanner}
|
||||
${statCard('Projects to Evaluate', projectCount)}
|
||||
${deadlineBox}
|
||||
${paragraph('The evaluation round is now open. Please log in to the MOPC Portal to begin reviewing your assigned projects.')}
|
||||
${assignmentsUrl ? ctaButton(assignmentsUrl, 'Start Evaluating') : ''}
|
||||
`
|
||||
|
||||
return {
|
||||
subject: `${roundName} is Now Open - ${projectCount} Projects Await`,
|
||||
html: getEmailWrapper(content),
|
||||
text: `
|
||||
${greeting}
|
||||
|
||||
${roundName} is now open for evaluation.
|
||||
|
||||
You have ${projectCount} project${projectCount !== 1 ? 's' : ''} to evaluate.
|
||||
${deadline ? `Deadline: ${deadline}` : ''}
|
||||
|
||||
Please log in to the MOPC Portal to begin reviewing your assigned projects.
|
||||
|
||||
${assignmentsUrl ? `Start evaluating: ${assignmentsUrl}` : ''}
|
||||
|
||||
---
|
||||
Monaco Ocean Protection Challenge
|
||||
Together for a healthier ocean.
|
||||
`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate "24 Hour Reminder" email template (for jury)
|
||||
*/
|
||||
function getReminder24HTemplate(
|
||||
name: string,
|
||||
pendingCount: number,
|
||||
roundName: string,
|
||||
deadline: string,
|
||||
assignmentsUrl?: string
|
||||
): EmailTemplate {
|
||||
const greeting = name ? `Hello ${name},` : 'Hello,'
|
||||
|
||||
const urgentBox = `
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="margin: 20px 0;">
|
||||
<tr>
|
||||
<td style="background-color: #fef3c7; border-left: 4px solid #f59e0b; border-radius: 0 8px 8px 0; padding: 16px 20px;">
|
||||
<p style="color: #92400e; margin: 0; font-size: 14px; font-weight: 600;">⚠ 24 Hours Remaining</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
`
|
||||
|
||||
const content = `
|
||||
${sectionTitle(greeting)}
|
||||
${urgentBox}
|
||||
${paragraph(`This is a reminder that <strong style="color: ${BRAND.darkBlue};">${roundName}</strong> closes in 24 hours.`)}
|
||||
${statCard('Pending Evaluations', pendingCount)}
|
||||
${infoBox(`<strong>Deadline:</strong> ${deadline}`, 'warning')}
|
||||
${paragraph('Please complete your remaining evaluations before the deadline to ensure your feedback is included in the selection process.')}
|
||||
${assignmentsUrl ? ctaButton(assignmentsUrl, 'Complete Evaluations') : ''}
|
||||
`
|
||||
|
||||
return {
|
||||
subject: `Reminder: ${pendingCount} evaluation${pendingCount !== 1 ? 's' : ''} due in 24 hours`,
|
||||
html: getEmailWrapper(content),
|
||||
text: `
|
||||
${greeting}
|
||||
|
||||
This is a reminder that ${roundName} closes in 24 hours.
|
||||
|
||||
You have ${pendingCount} pending evaluation${pendingCount !== 1 ? 's' : ''}.
|
||||
Deadline: ${deadline}
|
||||
|
||||
Please complete your remaining evaluations before the deadline.
|
||||
|
||||
${assignmentsUrl ? `Complete evaluations: ${assignmentsUrl}` : ''}
|
||||
|
||||
---
|
||||
Monaco Ocean Protection Challenge
|
||||
Together for a healthier ocean.
|
||||
`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate "1 Hour Reminder" email template (for jury)
|
||||
*/
|
||||
function getReminder1HTemplate(
|
||||
name: string,
|
||||
pendingCount: number,
|
||||
roundName: string,
|
||||
deadline: string,
|
||||
assignmentsUrl?: string
|
||||
): EmailTemplate {
|
||||
const greeting = name ? `${name},` : 'Attention,'
|
||||
|
||||
const urgentBanner = `
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="margin: 20px 0;">
|
||||
<tr>
|
||||
<td style="background: linear-gradient(135deg, ${BRAND.red} 0%, #dc2626 100%); border-radius: 12px; padding: 24px; text-align: center;">
|
||||
<p style="color: #ffffff; margin: 0 0 8px 0; font-size: 14px; font-weight: 600; text-transform: uppercase; letter-spacing: 2px;">Urgent</p>
|
||||
<h2 style="color: #ffffff; margin: 0; font-size: 24px; font-weight: 700;">1 Hour Remaining</h2>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
`
|
||||
|
||||
const content = `
|
||||
${sectionTitle(greeting)}
|
||||
${urgentBanner}
|
||||
${paragraph(`<strong style="color: ${BRAND.red};">${roundName}</strong> closes in <strong>1 hour</strong>.`)}
|
||||
${statCard('Evaluations Still Pending', pendingCount)}
|
||||
${paragraph('Please submit your remaining evaluations immediately to ensure they are counted.')}
|
||||
${assignmentsUrl ? ctaButton(assignmentsUrl, 'Submit Now') : ''}
|
||||
`
|
||||
|
||||
return {
|
||||
subject: `URGENT: ${pendingCount} evaluation${pendingCount !== 1 ? 's' : ''} due in 1 hour`,
|
||||
html: getEmailWrapper(content),
|
||||
text: `
|
||||
URGENT
|
||||
|
||||
${roundName} closes in 1 hour!
|
||||
|
||||
You have ${pendingCount} evaluation${pendingCount !== 1 ? 's' : ''} still pending.
|
||||
|
||||
Please submit your remaining evaluations immediately.
|
||||
|
||||
${assignmentsUrl ? `Submit now: ${assignmentsUrl}` : ''}
|
||||
|
||||
---
|
||||
Monaco Ocean Protection Challenge
|
||||
Together for a healthier ocean.
|
||||
`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate "Award Voting Open" email template (for jury)
|
||||
*/
|
||||
function getAwardVotingOpenTemplate(
|
||||
name: string,
|
||||
awardName: string,
|
||||
finalistCount: number,
|
||||
deadline?: string,
|
||||
votingUrl?: string
|
||||
): EmailTemplate {
|
||||
const greeting = name ? `Hello ${name},` : 'Hello,'
|
||||
|
||||
const awardBanner = `
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="margin: 20px 0;">
|
||||
<tr>
|
||||
<td style="background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%); border-radius: 12px; padding: 24px; text-align: center;">
|
||||
<p style="color: #ffffff; font-size: 36px; margin: 0 0 8px 0;">🏆</p>
|
||||
<p style="color: #ffffff; margin: 0 0 8px 0; font-size: 14px; font-weight: 600; text-transform: uppercase; letter-spacing: 2px;">Special Award</p>
|
||||
<h2 style="color: #ffffff; margin: 0; font-size: 22px; font-weight: 700;">${awardName}</h2>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
`
|
||||
|
||||
const content = `
|
||||
${sectionTitle(greeting)}
|
||||
${awardBanner}
|
||||
${paragraph(`Voting is now open for the <strong style="color: ${BRAND.darkBlue};">${awardName}</strong>.`)}
|
||||
${statCard('Finalists', finalistCount)}
|
||||
${deadline ? infoBox(`<strong>Voting closes:</strong> ${deadline}`, 'warning') : ''}
|
||||
${paragraph('Please review the finalist projects and cast your vote for the most deserving recipient.')}
|
||||
${votingUrl ? ctaButton(votingUrl, 'Cast Your Vote') : ''}
|
||||
`
|
||||
|
||||
return {
|
||||
subject: `Vote Now: ${awardName}`,
|
||||
html: getEmailWrapper(content),
|
||||
text: `
|
||||
${greeting}
|
||||
|
||||
Voting is now open for the ${awardName}.
|
||||
|
||||
${finalistCount} finalists are competing for this award.
|
||||
${deadline ? `Voting closes: ${deadline}` : ''}
|
||||
|
||||
Please review the finalist projects and cast your vote.
|
||||
|
||||
${votingUrl ? `Cast your vote: ${votingUrl}` : ''}
|
||||
|
||||
---
|
||||
Monaco Ocean Protection Challenge
|
||||
Together for a healthier ocean.
|
||||
`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate "Mentee Assigned" email template (for mentor)
|
||||
*/
|
||||
function getMenteeAssignedTemplate(
|
||||
name: string,
|
||||
projectName: string,
|
||||
teamLeadName: string,
|
||||
teamLeadEmail?: string,
|
||||
projectUrl?: string
|
||||
): EmailTemplate {
|
||||
const greeting = name ? `Hello ${name},` : 'Hello,'
|
||||
|
||||
const projectCard = `
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="margin: 20px 0;">
|
||||
<tr>
|
||||
<td style="background-color: ${BRAND.lightGray}; border-radius: 12px; padding: 24px;">
|
||||
<p style="color: ${BRAND.textMuted}; margin: 0 0 8px 0; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px;">Your New Mentee</p>
|
||||
<p style="color: ${BRAND.darkBlue}; margin: 0 0 12px 0; font-size: 20px; font-weight: 700;">${projectName}</p>
|
||||
<p style="color: ${BRAND.textDark}; margin: 0; font-size: 14px;">
|
||||
<strong>Team Lead:</strong> ${teamLeadName}${teamLeadEmail ? ` (${teamLeadEmail})` : ''}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
`
|
||||
|
||||
const content = `
|
||||
${sectionTitle(greeting)}
|
||||
${paragraph('You have been assigned as a mentor to a new project in the Monaco Ocean Protection Challenge.')}
|
||||
${projectCard}
|
||||
${paragraph('As a mentor, you play a crucial role in guiding this team toward success. Please reach out to introduce yourself and schedule your first meeting.')}
|
||||
${infoBox('Your expertise and guidance can make a significant impact on their ocean protection initiative.', 'info')}
|
||||
${projectUrl ? ctaButton(projectUrl, 'View Project Details') : ''}
|
||||
`
|
||||
|
||||
return {
|
||||
subject: `New Mentee Assignment: "${projectName}"`,
|
||||
html: getEmailWrapper(content),
|
||||
text: `
|
||||
${greeting}
|
||||
|
||||
You have been assigned as a mentor to a new project in the Monaco Ocean Protection Challenge.
|
||||
|
||||
Project: ${projectName}
|
||||
Team Lead: ${teamLeadName}${teamLeadEmail ? ` (${teamLeadEmail})` : ''}
|
||||
|
||||
Please reach out to introduce yourself and schedule your first meeting.
|
||||
|
||||
${projectUrl ? `View project: ${projectUrl}` : ''}
|
||||
|
||||
---
|
||||
Monaco Ocean Protection Challenge
|
||||
Together for a healthier ocean.
|
||||
`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate "Mentee Advanced" email template (for mentor)
|
||||
*/
|
||||
function getMenteeAdvancedTemplate(
|
||||
name: string,
|
||||
projectName: string,
|
||||
roundName: string,
|
||||
nextRoundName?: string
|
||||
): EmailTemplate {
|
||||
const greeting = name ? `Hello ${name},` : 'Hello,'
|
||||
|
||||
const content = `
|
||||
${sectionTitle(greeting)}
|
||||
${infoBox('Great news about your mentee!', 'success')}
|
||||
${paragraph(`Your mentee project <strong style="color: ${BRAND.darkBlue};">"${projectName}"</strong> has advanced to the next stage!`)}
|
||||
${statCard('Advanced From', roundName)}
|
||||
${nextRoundName ? paragraph(`They will now compete in <strong>${nextRoundName}</strong>.`) : ''}
|
||||
${paragraph('Your guidance is making a difference. Continue supporting the team as they progress in the competition.')}
|
||||
`
|
||||
|
||||
return {
|
||||
subject: `Your Mentee Advanced: "${projectName}"`,
|
||||
html: getEmailWrapper(content),
|
||||
text: `
|
||||
${greeting}
|
||||
|
||||
Great news! Your mentee project "${projectName}" has advanced to the next stage.
|
||||
|
||||
Advanced from: ${roundName}
|
||||
${nextRoundName ? `Now competing in: ${nextRoundName}` : ''}
|
||||
|
||||
Your guidance is making a difference. Continue supporting the team as they progress.
|
||||
|
||||
---
|
||||
Monaco Ocean Protection Challenge
|
||||
Together for a healthier ocean.
|
||||
`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate "Mentee Won" email template (for mentor)
|
||||
*/
|
||||
function getMenteeWonTemplate(
|
||||
name: string,
|
||||
projectName: string,
|
||||
awardName: string
|
||||
): EmailTemplate {
|
||||
const greeting = name ? `Congratulations ${name}!` : 'Congratulations!'
|
||||
|
||||
const trophyBanner = `
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="margin: 20px 0;">
|
||||
<tr>
|
||||
<td style="background: linear-gradient(135deg, #f59e0b 0%, #eab308 100%); border-radius: 12px; padding: 24px; text-align: center;">
|
||||
<p style="color: #ffffff; font-size: 48px; margin: 0 0 12px 0;">🏆</p>
|
||||
<p style="color: #ffffff; margin: 0; font-size: 16px; font-weight: 600;">Your Mentee Won!</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
`
|
||||
|
||||
const content = `
|
||||
${sectionTitle(greeting)}
|
||||
${trophyBanner}
|
||||
${paragraph(`Your mentee project <strong style="color: ${BRAND.darkBlue};">"${projectName}"</strong> has won the <strong>${awardName}</strong>!`)}
|
||||
${infoBox('Your mentorship played a vital role in their success.', 'success')}
|
||||
${paragraph('Thank you for your dedication and support. The impact of your guidance extends beyond this competition.')}
|
||||
`
|
||||
|
||||
return {
|
||||
subject: `Your Mentee Won: "${projectName}" - ${awardName}`,
|
||||
html: getEmailWrapper(content),
|
||||
text: `
|
||||
${greeting}
|
||||
|
||||
Your mentee project "${projectName}" has won the ${awardName}!
|
||||
|
||||
Your mentorship played a vital role in their success.
|
||||
|
||||
Thank you for your dedication and support.
|
||||
|
||||
---
|
||||
Monaco Ocean Protection Challenge
|
||||
Together for a healthier ocean.
|
||||
`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate "New Application" email template (for admins)
|
||||
*/
|
||||
function getNewApplicationTemplate(
|
||||
projectName: string,
|
||||
applicantName: string,
|
||||
applicantEmail: string,
|
||||
programName: string,
|
||||
reviewUrl?: string
|
||||
): EmailTemplate {
|
||||
const applicationCard = `
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="margin: 20px 0;">
|
||||
<tr>
|
||||
<td style="background-color: ${BRAND.lightGray}; border-left: 4px solid ${BRAND.teal}; border-radius: 0 12px 12px 0; padding: 20px 24px;">
|
||||
<p style="color: ${BRAND.textMuted}; margin: 0 0 4px 0; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px;">New Application</p>
|
||||
<p style="color: ${BRAND.darkBlue}; margin: 0 0 12px 0; font-size: 18px; font-weight: 700;">${projectName}</p>
|
||||
<p style="color: ${BRAND.textDark}; margin: 0; font-size: 14px;">
|
||||
<strong>Applicant:</strong> ${applicantName} (${applicantEmail})
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
`
|
||||
|
||||
const content = `
|
||||
${sectionTitle('New Application Received')}
|
||||
${paragraph(`A new application has been submitted to <strong style="color: ${BRAND.darkBlue};">${programName}</strong>.`)}
|
||||
${applicationCard}
|
||||
${reviewUrl ? ctaButton(reviewUrl, 'Review Application') : ''}
|
||||
`
|
||||
|
||||
return {
|
||||
subject: `New Application: "${projectName}"`,
|
||||
html: getEmailWrapper(content),
|
||||
text: `
|
||||
New Application Received
|
||||
|
||||
A new application has been submitted to ${programName}.
|
||||
|
||||
Project: ${projectName}
|
||||
Applicant: ${applicantName} (${applicantEmail})
|
||||
|
||||
${reviewUrl ? `Review application: ${reviewUrl}` : ''}
|
||||
|
||||
---
|
||||
Monaco Ocean Protection Challenge
|
||||
Together for a healthier ocean.
|
||||
`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Template registry mapping notification types to template generators
|
||||
*/
|
||||
type TemplateGenerator = (context: NotificationEmailContext) => EmailTemplate
|
||||
|
||||
export const NOTIFICATION_EMAIL_TEMPLATES: Record<string, TemplateGenerator> = {
|
||||
// Team/Applicant templates
|
||||
ADVANCED_SEMIFINAL: (ctx) =>
|
||||
getAdvancedSemifinalTemplate(
|
||||
ctx.name || '',
|
||||
(ctx.metadata?.projectName as string) || 'Your Project',
|
||||
(ctx.metadata?.programName as string) || 'MOPC',
|
||||
ctx.metadata?.nextSteps as string | undefined
|
||||
),
|
||||
ADVANCED_FINAL: (ctx) =>
|
||||
getAdvancedFinalTemplate(
|
||||
ctx.name || '',
|
||||
(ctx.metadata?.projectName as string) || 'Your Project',
|
||||
(ctx.metadata?.programName as string) || 'MOPC',
|
||||
ctx.metadata?.nextSteps as string | undefined
|
||||
),
|
||||
MENTOR_ASSIGNED: (ctx) =>
|
||||
getMentorAssignedTemplate(
|
||||
ctx.name || '',
|
||||
(ctx.metadata?.projectName as string) || 'Your Project',
|
||||
(ctx.metadata?.mentorName as string) || 'Your Mentor',
|
||||
ctx.metadata?.mentorBio as string | undefined
|
||||
),
|
||||
NOT_SELECTED: (ctx) =>
|
||||
getNotSelectedTemplate(
|
||||
ctx.name || '',
|
||||
(ctx.metadata?.projectName as string) || 'Your Project',
|
||||
(ctx.metadata?.roundName as string) || 'this round',
|
||||
ctx.linkUrl,
|
||||
ctx.metadata?.encouragement as string | undefined
|
||||
),
|
||||
WINNER_ANNOUNCEMENT: (ctx) =>
|
||||
getWinnerAnnouncementTemplate(
|
||||
ctx.name || '',
|
||||
(ctx.metadata?.projectName as string) || 'Your Project',
|
||||
(ctx.metadata?.awardName as string) || 'the Award',
|
||||
ctx.metadata?.prizeDetails as string | undefined
|
||||
),
|
||||
|
||||
// Jury templates
|
||||
ASSIGNED_TO_PROJECT: (ctx) =>
|
||||
getAssignedToProjectTemplate(
|
||||
ctx.name || '',
|
||||
(ctx.metadata?.projectName as string) || 'Project',
|
||||
(ctx.metadata?.roundName as string) || 'this round',
|
||||
ctx.metadata?.deadline as string | undefined,
|
||||
ctx.linkUrl
|
||||
),
|
||||
BATCH_ASSIGNED: (ctx) =>
|
||||
getBatchAssignedTemplate(
|
||||
ctx.name || '',
|
||||
(ctx.metadata?.projectCount as number) || 1,
|
||||
(ctx.metadata?.roundName as string) || 'this round',
|
||||
ctx.metadata?.deadline as string | undefined,
|
||||
ctx.linkUrl
|
||||
),
|
||||
ROUND_NOW_OPEN: (ctx) =>
|
||||
getRoundNowOpenTemplate(
|
||||
ctx.name || '',
|
||||
(ctx.metadata?.roundName as string) || 'Evaluation Round',
|
||||
(ctx.metadata?.projectCount as number) || 0,
|
||||
ctx.metadata?.deadline as string | undefined,
|
||||
ctx.linkUrl
|
||||
),
|
||||
REMINDER_24H: (ctx) =>
|
||||
getReminder24HTemplate(
|
||||
ctx.name || '',
|
||||
(ctx.metadata?.pendingCount as number) || 0,
|
||||
(ctx.metadata?.roundName as string) || 'this round',
|
||||
(ctx.metadata?.deadline as string) || 'Soon',
|
||||
ctx.linkUrl
|
||||
),
|
||||
REMINDER_1H: (ctx) =>
|
||||
getReminder1HTemplate(
|
||||
ctx.name || '',
|
||||
(ctx.metadata?.pendingCount as number) || 0,
|
||||
(ctx.metadata?.roundName as string) || 'this round',
|
||||
(ctx.metadata?.deadline as string) || 'Very Soon',
|
||||
ctx.linkUrl
|
||||
),
|
||||
AWARD_VOTING_OPEN: (ctx) =>
|
||||
getAwardVotingOpenTemplate(
|
||||
ctx.name || '',
|
||||
(ctx.metadata?.awardName as string) || 'Special Award',
|
||||
(ctx.metadata?.finalistCount as number) || 0,
|
||||
ctx.metadata?.deadline as string | undefined,
|
||||
ctx.linkUrl
|
||||
),
|
||||
|
||||
// Mentor templates
|
||||
MENTEE_ASSIGNED: (ctx) =>
|
||||
getMenteeAssignedTemplate(
|
||||
ctx.name || '',
|
||||
(ctx.metadata?.projectName as string) || 'Project',
|
||||
(ctx.metadata?.teamLeadName as string) || 'Team Lead',
|
||||
ctx.metadata?.teamLeadEmail as string | undefined,
|
||||
ctx.linkUrl
|
||||
),
|
||||
MENTEE_ADVANCED: (ctx) =>
|
||||
getMenteeAdvancedTemplate(
|
||||
ctx.name || '',
|
||||
(ctx.metadata?.projectName as string) || 'Project',
|
||||
(ctx.metadata?.roundName as string) || 'this round',
|
||||
ctx.metadata?.nextRoundName as string | undefined
|
||||
),
|
||||
MENTEE_WON: (ctx) =>
|
||||
getMenteeWonTemplate(
|
||||
ctx.name || '',
|
||||
(ctx.metadata?.projectName as string) || 'Project',
|
||||
(ctx.metadata?.awardName as string) || 'Award'
|
||||
),
|
||||
|
||||
// Admin templates
|
||||
NEW_APPLICATION: (ctx) =>
|
||||
getNewApplicationTemplate(
|
||||
(ctx.metadata?.projectName as string) || 'New Project',
|
||||
(ctx.metadata?.applicantName as string) || 'Applicant',
|
||||
(ctx.metadata?.applicantEmail as string) || '',
|
||||
(ctx.metadata?.programName as string) || 'MOPC',
|
||||
ctx.linkUrl
|
||||
),
|
||||
}
|
||||
|
||||
/**
|
||||
* Send styled notification email using the appropriate template
|
||||
*/
|
||||
export async function sendStyledNotificationEmail(
|
||||
email: string,
|
||||
name: string,
|
||||
type: string,
|
||||
context: NotificationEmailContext,
|
||||
subjectOverride?: string
|
||||
): Promise<void> {
|
||||
const templateGenerator = NOTIFICATION_EMAIL_TEMPLATES[type]
|
||||
|
||||
let template: EmailTemplate
|
||||
|
||||
if (templateGenerator) {
|
||||
// Use styled template
|
||||
template = templateGenerator({ ...context, name })
|
||||
// Apply subject override if provided
|
||||
if (subjectOverride) {
|
||||
template.subject = subjectOverride
|
||||
}
|
||||
} else {
|
||||
// Fall back to generic template
|
||||
template = getNotificationEmailTemplate(
|
||||
name,
|
||||
subjectOverride || context.title,
|
||||
context.message,
|
||||
context.linkUrl
|
||||
)
|
||||
}
|
||||
|
||||
const { transporter, from } = await getTransporter()
|
||||
|
||||
await transporter.sendMail({
|
||||
from,
|
||||
to: email,
|
||||
subject: template.subject,
|
||||
text: template.text,
|
||||
html: template.html,
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Email Sending Functions
|
||||
// =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user