2026-02-14 15:26:42 +01:00
import nodemailer from 'nodemailer'
import type { Transporter } from 'nodemailer'
import { prisma } from '@/lib/prisma'
// Cached transporter and config hash to detect changes
let cachedTransporter : Transporter | null = null
let cachedConfigHash = ''
let cachedFrom = ''
/ * *
* Get SMTP transporter using database settings with env var fallback .
* Caches the transporter and rebuilds it when settings change .
* /
async function getTransporter ( ) : Promise < { transporter : Transporter ; from : string } > {
// Read DB settings
const dbSettings = await prisma . systemSettings . findMany ( {
where : {
key : { in : [ 'smtp_host' , 'smtp_port' , 'smtp_user' , 'smtp_password' , 'email_from_name' , 'email_from' ] } ,
} ,
select : { key : true , value : true } ,
} )
const db : Record < string , string > = { }
for ( const s of dbSettings ) {
db [ s . key ] = s . value
}
// DB settings take priority, env vars as fallback
const host = db . smtp_host || process . env . SMTP_HOST || 'localhost'
const port = db . smtp_port || process . env . SMTP_PORT || '587'
const user = db . smtp_user || process . env . SMTP_USER || ''
const pass = db . smtp_password || process . env . SMTP_PASS || ''
// Combine sender name and email into "Name <email>" format
const fromName = db . email_from_name || 'MOPC Portal'
const rawFromEmail = db . email_from || process . env . EMAIL_FROM || 'noreply@monaco-opc.com'
// Strip "Name <...>" wrapper if present — extract just the email address
const fromEmail = rawFromEmail . includes ( '<' )
? rawFromEmail . match ( /<([^>]+)>/ ) ? . [ 1 ] || rawFromEmail
: rawFromEmail
const from = ` ${ fromName } < ${ fromEmail } > `
// Check if config changed since last call
const configHash = ` ${ host } : ${ port } : ${ user } : ${ pass } : ${ from } `
if ( cachedTransporter && configHash === cachedConfigHash ) {
return { transporter : cachedTransporter , from : cachedFrom }
}
// Create new transporter
cachedTransporter = nodemailer . createTransport ( {
host ,
port : parseInt ( port ) ,
secure : port === '465' ,
auth : { user , pass } ,
} )
cachedConfigHash = configHash
cachedFrom = from
return { transporter : cachedTransporter , from : cachedFrom }
}
// Legacy references for backward compat — default sender from env
const defaultFrom = process . env . EMAIL_FROM || 'MOPC Portal <noreply@monaco-opc.com>'
2026-02-23 14:27:58 +01:00
// =============================================================================
// Helpers
// =============================================================================
/ * *
* Get the base URL for links in emails .
* Uses NEXTAUTH_URL with a safe production fallback .
* /
export function getBaseUrl ( ) : string {
return process . env . NEXTAUTH_URL || 'https://portal.monaco-opc.com'
}
/ * *
* Ensure a URL is absolute ( has protocol + host ) .
* Converts relative paths like "/jury/competitions" to full URLs .
* /
export function ensureAbsoluteUrl ( url : string | undefined ) : string | undefined {
if ( ! url ) return undefined
if ( url . startsWith ( 'http://' ) || url . startsWith ( 'https://' ) ) return url
if ( url . startsWith ( '/' ) ) return ` ${ getBaseUrl ( ) } ${ url } `
return ` ${ getBaseUrl ( ) } / ${ url } `
}
2026-02-14 15:26:42 +01:00
// =============================================================================
// Brand Colors & Logo URLs
// =============================================================================
const BRAND = {
red : '#de0f1e' ,
redHover : '#b91c1c' ,
darkBlue : '#053d57' ,
teal : '#557f8c' ,
white : '#fefefe' ,
lightGray : '#f5f5f5' ,
textDark : '#1f2937' ,
textMuted : '#6b7280' ,
}
const getSmallLogoUrl = ( ) = > 'https://s3.monaco-opc.com/public/MOPC-blue-small.png'
const getBigLogoUrl = ( ) = > 'https://s3.monaco-opc.com/public/MOPC-blue-long.png'
// =============================================================================
// Email Template Wrapper & Helpers
// =============================================================================
/ * *
* Wrap email content with consistent branding :
* - Small logo at top of white content box
* - Big logo in dark blue footer
* /
function getEmailWrapper ( content : string ) : string {
return `
< ! DOCTYPE html >
< html lang = "en" >
< head >
< meta charset = "utf-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< meta http-equiv = "X-UA-Compatible" content = "IE=edge" >
< title > MOPC < / title >
<!-- [if mso]>
< noscript >
< xml >
< o : OfficeDocumentSettings >
< o : PixelsPerInch > 96 < / o : P i x e l s P e r I n c h >
< / o : O f f i c e D o c u m e n t S e t t i n g s >
< / xml >
< / noscript >
< ! [ endif ] -- >
< / head >
< body style = "margin: 0; padding: 0; background-color: ${BRAND.darkBlue}; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased;" >
< table role = "presentation" width = "100%" cellspacing = "0" cellpadding = "0" border = "0" style = "background-image: url('https://s3.monaco-opc.com/public/ocean.png'); background-size: cover; background-position: center top; background-repeat: no-repeat; background-color: ${BRAND.darkBlue};" >
< tr >
< td align = "center" style = "padding: 40px 20px;" >
<!-- Main Container -->
< table role = "presentation" width = "600" cellspacing = "0" cellpadding = "0" border = "0" style = "max-width: 600px; width: 100%;" >
<!-- Content Box -->
< tr >
< td style = "background-color: ${BRAND.white}; border-radius: 12px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);" >
< table role = "presentation" width = "100%" cellspacing = "0" cellpadding = "0" border = "0" >
<!-- Small Logo Header -->
< tr >
< td align = "center" style = "padding: 32px 40px 24px 40px;" >
< img src = "${getSmallLogoUrl()}" alt = "MOPC" width = "100" height = "auto" style = "display: block; border: 0; max-width: 100px;" >
< / td >
< / tr >
<!-- Email Content -->
< tr >
< td style = "padding: 0 40px 32px 40px;" >
$ { content }
< / td >
< / tr >
<!-- Footer -->
< tr >
< td style = "padding: 0 40px 32px 40px; border-top: 1px solid #e5e7eb;" >
< table role = "presentation" width = "100%" cellspacing = "0" cellpadding = "0" border = "0" >
< tr >
< td align = "center" style = "padding-top: 24px;" >
< img src = "${getBigLogoUrl()}" alt = "Monaco Ocean Protection Challenge" width = "180" height = "auto" style = "display: block; border: 0; max-width: 180px; margin-bottom: 12px;" >
< / td >
< / tr >
< tr >
< td align = "center" >
< p style = "color: ${BRAND.textMuted}; font-size: 13px; margin: 0; line-height: 1.5;" >
Together for a healthier ocean
< / p >
< p style = "color: ${BRAND.textMuted}; font-size: 12px; margin: 8px 0 0 0;" >
& copy ; $ { new Date ( ) . getFullYear ( ) } Monaco Ocean Protection Challenge
< / p >
< / td >
< / tr >
< / table >
< / td >
< / tr >
< / table >
< / td >
< / tr >
< / table >
< / td >
< / tr >
< / table >
< / body >
< / html >
`
}
/ * *
* Generate a styled CTA button
* /
function ctaButton ( url : string , text : string ) : string {
2026-02-23 14:27:58 +01:00
// Ensure URL is always absolute for email clients
const safeUrl = ensureAbsoluteUrl ( url ) || url
2026-02-14 15:26:42 +01:00
return `
< table role = "presentation" width = "100%" cellspacing = "0" cellpadding = "0" border = "0" style = "margin: 24px 0;" >
< tr >
< td align = "center" >
2026-02-23 14:27:58 +01:00
< table role = "presentation" cellspacing = "0" cellpadding = "0" border = "0" >
< tr >
< td align = "center" bgcolor = "${BRAND.red}" style = "background-color: ${BRAND.red}; border-radius: 8px; mso-border-alt: none;" >
< a href = "${safeUrl}" target = "_blank" style = "display: inline-block; padding: 16px 40px; color: #ffffff; text-decoration: none; font-weight: 600; font-size: 16px; font-family: Helvetica, Arial, sans-serif; mso-line-height-rule: exactly; line-height: 20px;" >
$ { text }
< / a >
< / td >
< / tr >
< / table >
< / td >
< / tr >
< tr >
< td align = "center" style = "padding-top: 8px;" >
< p style = "margin: 0; font-size: 12px; color: ${BRAND.textMuted};" >
If the button doesn ' t work : < a href = "${safeUrl}" target = "_blank" style = "color: ${BRAND.teal}; word-break: break-all;" > $ { safeUrl } < / a >
< / p >
2026-02-14 15:26:42 +01:00
< / td >
< / tr >
< / table >
`
}
/ * *
* Generate styled section title
* /
function sectionTitle ( text : string ) : string {
return ` <h2 style="color: ${ BRAND . darkBlue } ; margin: 0 0 16px 0; font-size: 22px; font-weight: 600; line-height: 1.3;"> ${ text } </h2> `
}
/ * *
* Generate styled paragraph
* /
function paragraph ( text : string ) : string {
return ` <p style="color: ${ BRAND . textDark } ; margin: 0 0 16px 0; font-size: 15px; line-height: 1.6;"> ${ text } </p> `
}
/ * *
* Generate styled info box
* /
function infoBox ( content : string , variant : 'warning' | 'info' | 'success' = 'info' ) : string {
const colors = {
warning : { bg : '#fef3c7' , border : '#f59e0b' , text : '#92400e' } ,
info : { bg : '#e0f2fe' , border : '#0ea5e9' , text : '#0369a1' } ,
success : { bg : '#dcfce7' , border : '#22c55e' , text : '#166534' } ,
}
const c = colors [ variant ]
return `
< table role = "presentation" width = "100%" cellspacing = "0" cellpadding = "0" border = "0" style = "margin: 20px 0;" >
< tr >
< td style = "background-color: ${c.bg}; border-left: 4px solid ${c.border}; border-radius: 0 8px 8px 0; padding: 16px 20px;" >
< p style = "color: ${c.text}; margin: 0; font-size: 14px; font-weight: 500;" > $ { content } < / p >
< / td >
< / tr >
< / table >
`
}
/ * *
* Generate styled stat card
* /
function statCard ( label : string , value : string | number ) : string {
return `
< 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; text-align: center;" >
< p style = "color: ${BRAND.textMuted}; margin: 0 0 8px 0; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px;" > $ { label } < / p >
< p style = "color: ${BRAND.darkBlue}; margin: 0; font-size: 42px; font-weight: 700; line-height: 1;" > $ { value } < / p >
< / td >
< / tr >
< / table >
`
}
// =============================================================================
// Email Templates
// =============================================================================
interface EmailTemplate {
subject : string
html : string
text : string
}
/ * *
* Generate magic link email template
* /
function getMagicLinkTemplate ( url : string , expiryMinutes : number = 15 ) : EmailTemplate {
const content = `
$ { sectionTitle ( 'Sign in to your account' ) }
$ { paragraph ( 'Click the button below to securely sign in to the MOPC Portal.' ) }
$ { infoBox ( ` <strong>This link expires in ${ expiryMinutes } minutes</strong> ` , 'warning' ) }
$ { ctaButton ( url , 'Sign In to MOPC' ) }
< p style = "color: ${BRAND.textMuted}; margin: 24px 0 0 0; font-size: 13px; text-align: center;" >
If you didn ' t request this email , you can safely ignore it .
< / p >
`
return {
subject : 'Sign in to MOPC Portal' ,
html : getEmailWrapper ( content ) ,
text : `
Sign in to MOPC Portal
=== === === === === === === === =
Click the link below to sign in to your account :
$ { url }
This link will expire in $ { expiryMinutes } minutes .
If you didn ' t request this email , you can safely ignore it .
-- -
Monaco Ocean Protection Challenge
Together for a healthier ocean .
` ,
}
}
/ * *
* Generate generic invitation email template ( not round - specific )
* /
2026-02-17 22:05:58 +01:00
function formatExpiryLabel ( hours : number ) : string {
if ( hours < 24 ) return ` ${ hours } hour ${ hours !== 1 ? 's' : '' } `
const days = Math . round ( hours / 24 )
return ` ${ days } day ${ days !== 1 ? 's' : '' } `
}
2026-02-14 15:26:42 +01:00
function getGenericInvitationTemplate (
name : string ,
url : string ,
2026-02-17 22:05:58 +01:00
role : string ,
expiryHours : number = 72
2026-02-14 15:26:42 +01:00
) : EmailTemplate {
const roleLabel = role === 'JURY_MEMBER' ? 'jury member' : role . toLowerCase ( ) . replace ( '_' , ' ' )
const article = /^[aeiou]/i . test ( roleLabel ) ? 'an' : 'a'
const greeting = name ? ` Hello ${ name } , ` : 'Hello,'
2026-02-17 22:05:58 +01:00
const expiryLabel = formatExpiryLabel ( expiryHours )
2026-02-14 15:26:42 +01:00
const content = `
$ { sectionTitle ( greeting ) }
$ { paragraph ( ` You've been invited to join the Monaco Ocean Protection Challenge platform as ${ article } <strong> ${ roleLabel } </strong>. ` ) }
$ { paragraph ( 'Click the button below to set up your account and get started.' ) }
$ { ctaButton ( url , 'Accept Invitation' ) }
2026-02-17 22:05:58 +01:00
$ { infoBox ( ` This link will expire in ${ expiryLabel } . ` , 'info' ) }
2026-02-14 15:26:42 +01:00
`
return {
subject : "You're invited to join the MOPC Portal" ,
html : getEmailWrapper ( content ) ,
text : `
$ { greeting }
You ' ve been invited to join the Monaco Ocean Protection Challenge platform as $ { article } $ { roleLabel } .
Click the link below to set up your account and get started :
$ { url }
2026-02-17 22:05:58 +01:00
This link will expire in $ { expiryLabel } .
2026-02-14 15:26:42 +01:00
-- -
Monaco Ocean Protection Challenge
Together for a healthier ocean .
` ,
}
}
/ * *
* Generate evaluation reminder email template
* /
function getEvaluationReminderTemplate (
name : string ,
pendingCount : number ,
roundName : string ,
deadline : string ,
assignmentsUrl : string
) : EmailTemplate {
const greeting = name ? ` Hello ${ name } , ` : 'Hello,'
// Deadline alert box (styled differently from info box)
const deadlineBox = `
< 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 ( ` This is a friendly reminder about your pending evaluations for <strong style="color: ${ BRAND . darkBlue } ;"> ${ roundName } </strong>. ` ) }
$ { statCard ( 'Pending Evaluations' , pendingCount ) }
$ { deadlineBox }
$ { paragraph ( 'Your expert evaluation helps identify the most promising ocean conservation projects. Please complete your reviews before the deadline.' ) }
$ { ctaButton ( assignmentsUrl , 'View My Assignments' ) }
`
return {
subject : ` Reminder: ${ pendingCount } evaluation ${ pendingCount !== 1 ? 's' : '' } awaiting your review ` ,
html : getEmailWrapper ( content ) ,
text : `
$ { greeting }
This is a friendly reminder that you have $ { pendingCount } pending evaluation $ { pendingCount !== 1 ? 's' : '' } for $ { roundName } .
Deadline : $ { deadline }
Please complete your evaluations before the deadline to ensure your feedback is included in the selection process .
View your assignments : $ { assignmentsUrl }
-- -
Monaco Ocean Protection Challenge
Together for a healthier ocean .
` ,
}
}
/ * *
* Generate announcement email template
* /
function getAnnouncementTemplate (
name : string ,
title : string ,
message : string ,
ctaText? : string ,
ctaUrl? : string
) : EmailTemplate {
const greeting = name ? ` Hello ${ name } , ` : 'Hello,'
const ctaTextPlain = ctaText && ctaUrl ? ` \ n ${ ctaText } : ${ ctaUrl } \ n ` : ''
// Escape HTML in message but preserve line breaks
const formattedMessage = message
. replace ( /&/g , '&' )
. replace ( /</g , '<' )
. replace ( />/g , '>' )
. replace ( /\n/g , '<br>' )
// Title card with success styling
const titleCard = `
< table role = "presentation" width = "100%" cellspacing = "0" cellpadding = "0" border = "0" style = "margin: 20px 0;" >
< tr >
< td style = "background-color: #ecfdf5; border-left: 4px solid #059669; border-radius: 0 12px 12px 0; padding: 20px 24px;" >
< h3 style = "color: #065f46; margin: 0; font-size: 18px; font-weight: 700;" > $ { title } < / h3 >
< / td >
< / tr >
< / table >
`
const content = `
$ { sectionTitle ( greeting ) }
$ { titleCard }
< div style = "color: ${BRAND.textDark}; font-size: 15px; line-height: 1.7; margin: 20px 0;" >
$ { formattedMessage }
< / div >
$ { ctaText && ctaUrl ? ctaButton ( ctaUrl , ctaText ) : '' }
`
return {
subject : title ,
html : getEmailWrapper ( content ) ,
text : `
$ { greeting }
$ { title }
$ { message }
$ { ctaTextPlain }
-- -
Monaco Ocean Protection Challenge
Together for a healthier ocean .
` ,
}
}
/ * *
* Generate jury invitation email template
* /
function getJuryInvitationTemplate (
name : string ,
url : string ,
roundName : string
) : EmailTemplate {
const greeting = name ? ` Hello ${ name } , ` : 'Hello,'
const content = `
$ { sectionTitle ( greeting ) }
$ { paragraph ( ` You've been invited to serve as a jury member for <strong style="color: ${ BRAND . darkBlue } ;"> ${ roundName } </strong>. ` ) }
$ { paragraph ( 'As a jury member, you\'ll evaluate innovative ocean protection projects and help select the most promising initiatives.' ) }
$ { ctaButton ( url , 'Accept Invitation' ) }
< p style = "color: ${BRAND.textMuted}; margin: 20px 0 0 0; font-size: 13px; text-align: center;" >
This link will allow you to access the platform and view your assigned projects .
< / p >
`
return {
subject : ` You're invited to evaluate projects for ${ roundName } ` ,
html : getEmailWrapper ( content ) ,
text : `
$ { greeting }
You ' ve been invited to serve as a jury member for $ { roundName } .
As a jury member , you ' ll evaluate innovative ocean protection projects and help select the most promising initiatives .
Click the link below to accept your invitation :
$ { url }
-- -
Monaco Ocean Protection Challenge
Together for a healthier ocean .
` ,
}
}
/ * *
* Generate application confirmation email template
* /
function getApplicationConfirmationTemplate (
name : string ,
projectName : string ,
programName : string ,
customMessage? : string
) : EmailTemplate {
const greeting = name ? ` Hello ${ name } , ` : 'Hello,'
const customMessageHtml = customMessage
? ` <div style="color: ${ BRAND . textDark } ; font-size: 15px; line-height: 1.7; margin: 20px 0; padding: 20px; background-color: ${ BRAND . lightGray } ; border-radius: 8px;"> ${ customMessage . replace ( /\n/g , '<br>' ) } </div> `
: ''
const content = `
$ { sectionTitle ( greeting ) }
$ { paragraph ( ` Thank you for submitting your application to <strong style="color: ${ BRAND . darkBlue } ;"> ${ programName } </strong>! ` ) }
$ { infoBox ( ` Your project "<strong> ${ projectName } </strong>" has been successfully received. ` , 'success' ) }
$ { customMessageHtml }
$ { paragraph ( 'Our team will review your submission and get back to you soon. In the meantime, if you have any questions, please don\'t hesitate to reach out.' ) }
< p style = "color: ${BRAND.textMuted}; margin: 24px 0 0 0; font-size: 13px; text-align: center;" >
You will receive email updates about your application status .
< / p >
`
return {
subject : ` Application Received - ${ projectName } ` ,
html : getEmailWrapper ( content ) ,
text : `
$ { greeting }
Thank you for submitting your application to $ { programName } !
Your project "${projectName}" has been successfully received .
$ { customMessage || '' }
Our team will review your submission and get back to you soon . In the meantime , if you have any questions , please don ' t hesitate to reach out .
-- -
Monaco Ocean Protection Challenge
Together for a healthier ocean .
` ,
}
}
/ * *
* Generate team member invite email template
* /
function getTeamMemberInviteTemplate (
name : string ,
projectName : string ,
teamLeadName : string ,
inviteUrl : string
) : EmailTemplate {
const greeting = name ? ` Hello ${ name } , ` : 'Hello,'
const content = `
$ { sectionTitle ( greeting ) }
$ { paragraph ( ` <strong> ${ teamLeadName } </strong> has invited you to join their team for the project "<strong style="color: ${ BRAND . darkBlue } ;"> ${ projectName } </strong>" on the Monaco Ocean Protection Challenge platform. ` ) }
$ { paragraph ( 'Click the button below to accept the invitation and set up your account.' ) }
$ { ctaButton ( inviteUrl , 'Accept Invitation' ) }
$ { infoBox ( 'This invitation link will expire in 30 days.' , 'info' ) }
< p style = "color: ${BRAND.textMuted}; margin: 24px 0 0 0; font-size: 13px; text-align: center;" >
If you weren ' t expecting this invitation , you can safely ignore this email .
< / p >
`
return {
subject : ` You've been invited to join " ${ projectName } " ` ,
html : getEmailWrapper ( content ) ,
text : `
$ { greeting }
$ { teamLeadName } has invited you to join their team for the project "${projectName}" on the Monaco Ocean Protection Challenge platform .
Click the link below to accept the invitation and set up your account :
$ { inviteUrl }
This invitation link will expire in 30 days .
If you weren ' t expecting this invitation , you can safely ignore this email .
-- -
Monaco Ocean Protection Challenge
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;" > & # 127942 ; < / 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 .
` ,
}
}
2026-02-23 14:56:30 +01:00
/ * *
* Generate "COI Reassignment" email template ( for jury receiving a reassigned project )
* /
function getCOIReassignedTemplate (
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: #fef3c7; border-left: 4px solid #f59e0b; 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;" > Reassigned Project < / 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 ( ` A project has been <strong>reassigned to you</strong> for evaluation in <strong style="color: ${ BRAND . darkBlue } ;"> ${ roundName } </strong>, because the previously assigned juror declared a conflict of interest. ` ) }
$ { projectCard }
$ { deadlineBox }
$ { paragraph ( 'Please review the project materials and submit your evaluation before the deadline. This is an additional project on top of your existing assignments.' ) }
$ { assignmentsUrl ? ctaButton ( assignmentsUrl , 'View Assignment' ) : '' }
`
return {
subject : ` Project Reassigned to You: " ${ projectName } " - ${ roundName } ` ,
html : getEmailWrapper ( content ) ,
text : `
$ { greeting }
A project has been reassigned to you for evaluation in $ { roundName } , because the previously assigned juror declared a conflict of interest .
Project : $ { projectName }
$ { deadline ? ` Deadline: ${ deadline } ` : '' }
Please review the project materials and submit your evaluation before the deadline . This is an additional project on top of your existing assignments .
$ { assignmentsUrl ? ` View assignment: ${ assignmentsUrl } ` : '' }
-- -
Monaco Ocean Protection Challenge
Together for a healthier ocean .
` ,
}
}
/ * *
* Generate "Manual Reassignment" email template ( for jury )
* Sent when an admin manually transfers a project assignment to a juror .
* /
function getManualReassignedTemplate (
name : string ,
projectNames : string [ ] ,
roundName : string ,
deadline? : string ,
assignmentsUrl? : string
) : EmailTemplate {
const greeting = name ? ` Hello ${ name } , ` : 'Hello,'
const count = projectNames . length
const isSingle = count === 1
const projectList = projectNames . map ( ( p ) = > `
< table role = "presentation" width = "100%" cellspacing = "0" cellpadding = "0" border = "0" style = "margin: 8px 0;" >
< tr >
< td style = "background-color: #eff6ff; border-left: 4px solid ${BRAND.darkBlue}; border-radius: 0 8px 8px 0; padding: 14px 20px;" >
< p style = "color: ${BRAND.darkBlue}; margin: 0; font-size: 16px; font-weight: 700;" > $ { p } < / p >
< / td >
< / tr >
< / table >
` ).join('')
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 ( ` An administrator has <strong>reassigned ${ isSingle ? 'a project' : ` ${ count } projects ` } </strong> to you for evaluation in <strong style="color: ${ BRAND . darkBlue } ;"> ${ roundName } </strong>. ` ) }
$ { projectList }
$ { deadlineBox }
$ { paragraph ( ` Please review the project material ${ isSingle ? '' : 's' } and submit your evaluation ${ isSingle ? '' : 's' } before the deadline. ` ) }
$ { assignmentsUrl ? ctaButton ( assignmentsUrl , 'View Assignments' ) : '' }
`
const projectListText = projectNames . map ( ( p ) = > ` - ${ p } ` ) . join ( '\n' )
return {
subject : ` Project ${ isSingle ? '' : 's' } Reassigned to You - ${ roundName } ` ,
html : getEmailWrapper ( content ) ,
text : `
$ { greeting }
An administrator has reassigned $ { isSingle ? 'a project' : ` ${ count } projects ` } to you for evaluation in $ { roundName } .
$ { isSingle ? ` Project: ${ projectNames [ 0 ] } ` : ` Projects: \ n ${ projectListText } ` }
$ { deadline ? ` Deadline: ${ deadline } ` : '' }
Please review the project material $ { isSingle ? '' : 's' } and submit your evaluation $ { isSingle ? '' : 's' } before the deadline .
$ { assignmentsUrl ? ` View assignments: ${ assignmentsUrl } ` : '' }
-- -
Monaco Ocean Protection Challenge
Together for a healthier ocean .
` ,
}
}
2026-02-23 16:08:46 +01:00
/ * *
* Generate "Dropout Reassignment" email template ( for jury )
* Sent when a juror drops out and their projects are redistributed .
* /
function getDropoutReassignedTemplate (
name : string ,
projectNames : string [ ] ,
roundName : string ,
droppedJurorName : string ,
deadline? : string ,
assignmentsUrl? : string
) : EmailTemplate {
const greeting = name ? ` Hello ${ name } , ` : 'Hello,'
const count = projectNames . length
const isSingle = count === 1
const projectList = projectNames . map ( ( p ) = > `
< table role = "presentation" width = "100%" cellspacing = "0" cellpadding = "0" border = "0" style = "margin: 8px 0;" >
< tr >
< td style = "background-color: #fef3c7; border-left: 4px solid #f59e0b; border-radius: 0 8px 8px 0; padding: 14px 20px;" >
< p style = "color: ${BRAND.darkBlue}; margin: 0; font-size: 16px; font-weight: 700;" > $ { p } < / p >
< / td >
< / tr >
< / table >
` ).join('')
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 ( ` Due to a juror becoming unavailable, ${ isSingle ? 'a project has' : ` ${ count } projects have ` } been <strong>reassigned to you</strong> for evaluation in <strong style="color: ${ BRAND . darkBlue } ;"> ${ roundName } </strong>. ` ) }
$ { projectList }
$ { deadlineBox }
$ { paragraph ( ` ${ isSingle ? 'This project was' : 'These projects were' } previously assigned to ${ droppedJurorName } , who is no longer available. Please review the project material ${ isSingle ? '' : 's' } and submit your evaluation ${ isSingle ? '' : 's' } before the deadline. ` ) }
$ { assignmentsUrl ? ctaButton ( assignmentsUrl , 'View Assignments' ) : '' }
`
const projectListText = projectNames . map ( ( p ) = > ` - ${ p } ` ) . join ( '\n' )
return {
subject : ` Project ${ isSingle ? '' : 's' } Reassigned to You (Juror Unavailable) - ${ roundName } ` ,
html : getEmailWrapper ( content ) ,
text : `
$ { greeting }
Due to a juror becoming unavailable , $ { isSingle ? 'a project has' : ` ${ count } projects have ` } been reassigned to you for evaluation in $ { roundName } .
$ { isSingle ? ` Project: ${ projectNames [ 0 ] } ` : ` Projects: \ n ${ projectListText } ` }
$ { deadline ? ` Deadline: ${ deadline } ` : '' }
$ { isSingle ? 'This project was' : 'These projects were' } previously assigned to $ { droppedJurorName } , who is no longer available . Please review the project material $ { isSingle ? '' : 's' } and submit your evaluation $ { isSingle ? '' : 's' } before the deadline .
$ { assignmentsUrl ? ` View assignments: ${ assignmentsUrl } ` : '' }
-- -
Monaco Ocean Protection Challenge
Together for a healthier ocean .
` ,
}
}
2026-02-14 15:26:42 +01:00
/ * *
* 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;" > & # 9888 ; 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 .
` ,
}
}
2026-02-19 12:59:35 +01:00
/ * *
* Generate "3 Days Remaining" email template ( for jury )
* /
function getReminder3DaysTemplate (
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;" > & # 9888 ; 3 Days 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 3 days. ` ) }
$ { statCard ( 'Pending Evaluations' , pendingCount ) }
$ { infoBox ( ` <strong>Deadline:</strong> ${ deadline } ` , 'warning' ) }
$ { paragraph ( 'Please plan to 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 3 days ` ,
html : getEmailWrapper ( content ) ,
text : `
$ { greeting }
This is a reminder that $ { roundName } closes in 3 days .
You have $ { pendingCount } pending evaluation $ { pendingCount !== 1 ? 's' : '' } .
Deadline : $ { deadline }
Please plan to complete your remaining evaluations before the deadline .
$ { assignmentsUrl ? ` Complete evaluations: ${ assignmentsUrl } ` : '' }
-- -
Monaco Ocean Protection Challenge
Together for a healthier ocean .
` ,
}
}
2026-02-14 15:26:42 +01:00
/ * *
* 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;" > & # 127942 ; < / 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;" > & # 127942 ; < / 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
) ,
2026-02-23 14:56:30 +01:00
COI_REASSIGNED : ( ctx ) = >
getCOIReassignedTemplate (
ctx . name || '' ,
( ctx . metadata ? . projectName as string ) || 'Project' ,
( ctx . metadata ? . roundName as string ) || 'this round' ,
ctx . metadata ? . deadline as string | undefined ,
ctx . linkUrl
) ,
MANUAL_REASSIGNED : ( ctx ) = >
getManualReassignedTemplate (
ctx . name || '' ,
( ctx . metadata ? . projectNames as string [ ] ) || [ ( ctx . metadata ? . projectName as string ) || 'Project' ] ,
( ctx . metadata ? . roundName as string ) || 'this round' ,
ctx . metadata ? . deadline as string | undefined ,
ctx . linkUrl
) ,
2026-02-23 16:08:46 +01:00
DROPOUT_REASSIGNED : ( ctx ) = >
getDropoutReassignedTemplate (
ctx . name || '' ,
( ctx . metadata ? . projectNames as string [ ] ) || [ ( ctx . metadata ? . projectName as string ) || 'Project' ] ,
( ctx . metadata ? . roundName as string ) || 'this round' ,
( ctx . metadata ? . droppedJurorName as string ) || 'a fellow juror' ,
ctx . metadata ? . deadline as string | undefined ,
ctx . linkUrl
) ,
2026-02-14 15:26:42 +01:00
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
) ,
2026-02-19 12:59:35 +01:00
REMINDER_3_DAYS : ( ctx ) = >
getReminder3DaysTemplate (
ctx . name || '' ,
( ctx . metadata ? . pendingCount as number ) || 0 ,
( ctx . metadata ? . roundName as string ) || 'this round' ,
( ctx . metadata ? . deadline as string ) || 'Soon' ,
ctx . linkUrl
) ,
2026-02-14 15:26:42 +01:00
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 > {
2026-02-23 14:27:58 +01:00
// Safety net: always ensure linkUrl is absolute before passing to templates
const safeContext = {
. . . context ,
linkUrl : ensureAbsoluteUrl ( context . linkUrl ) ,
}
2026-02-14 15:26:42 +01:00
const templateGenerator = NOTIFICATION_EMAIL_TEMPLATES [ type ]
let template : EmailTemplate
if ( templateGenerator ) {
// Use styled template
2026-02-23 14:27:58 +01:00
template = templateGenerator ( { . . . safeContext , name } )
2026-02-14 15:26:42 +01:00
// Apply subject override if provided
if ( subjectOverride ) {
template . subject = subjectOverride
}
} else {
// Fall back to generic template
template = getNotificationEmailTemplate (
name ,
2026-02-23 14:27:58 +01:00
subjectOverride || safeContext . title ,
safeContext . message ,
safeContext . linkUrl
2026-02-14 15:26:42 +01:00
)
}
const { transporter , from } = await getTransporter ( )
await transporter . sendMail ( {
from ,
to : email ,
subject : template.subject ,
text : template.text ,
html : template.html ,
} )
}
// =============================================================================
// Email Sending Functions
// =============================================================================
/ * *
* Send magic link email for authentication
* /
export async function sendMagicLinkEmail (
email : string ,
url : string
) : Promise < void > {
const expiryMinutes = parseInt ( process . env . MAGIC_LINK_EXPIRY || '900' ) / 60
const template = getMagicLinkTemplate ( url , expiryMinutes )
const { transporter , from } = await getTransporter ( )
await transporter . sendMail ( {
from ,
to : email ,
subject : template.subject ,
text : template.text ,
html : template.html ,
} )
}
/ * *
* Send generic invitation email ( not round - specific )
* /
export async function sendInvitationEmail (
email : string ,
name : string | null ,
url : string ,
2026-02-17 22:05:58 +01:00
role : string ,
expiryHours? : number
2026-02-14 15:26:42 +01:00
) : Promise < void > {
2026-02-17 22:05:58 +01:00
const template = getGenericInvitationTemplate ( name || '' , url , role , expiryHours )
2026-02-14 15:26:42 +01:00
const { transporter , from } = await getTransporter ( )
await transporter . sendMail ( {
from ,
to : email ,
subject : template.subject ,
text : template.text ,
html : template.html ,
} )
}
/ * *
* Send jury invitation email ( round - specific )
* /
export async function sendJuryInvitationEmail (
email : string ,
name : string | null ,
url : string ,
roundName : string
) : Promise < void > {
const template = getJuryInvitationTemplate ( name || '' , url , roundName )
const { transporter , from } = await getTransporter ( )
await transporter . sendMail ( {
from ,
to : email ,
subject : template.subject ,
text : template.text ,
html : template.html ,
} )
}
/ * *
* Send evaluation reminder email
* /
export async function sendEvaluationReminderEmail (
email : string ,
name : string | null ,
pendingCount : number ,
roundName : string ,
deadline : string ,
assignmentsUrl : string
) : Promise < void > {
const template = getEvaluationReminderTemplate (
name || '' ,
pendingCount ,
roundName ,
deadline ,
assignmentsUrl
)
const { transporter , from } = await getTransporter ( )
await transporter . sendMail ( {
from ,
to : email ,
subject : template.subject ,
text : template.text ,
html : template.html ,
} )
}
/ * *
* Send announcement email
* /
export async function sendAnnouncementEmail (
email : string ,
name : string | null ,
title : string ,
message : string ,
ctaText? : string ,
ctaUrl? : string
) : Promise < void > {
const template = getAnnouncementTemplate (
name || '' ,
title ,
message ,
ctaText ,
ctaUrl
)
const { transporter , from } = await getTransporter ( )
await transporter . sendMail ( {
from ,
to : email ,
subject : template.subject ,
text : template.text ,
html : template.html ,
} )
}
/ * *
* Send a test email to verify SMTP configuration
* /
export async function sendTestEmail ( toEmail : string ) : Promise < boolean > {
try {
const content = `
$ { sectionTitle ( 'Test Email' ) }
$ { paragraph ( 'This is a test email from the MOPC Portal.' ) }
$ { infoBox ( 'If you received this, your email configuration is working correctly!' , 'success' ) }
< p style = "color: ${BRAND.textMuted}; margin: 20px 0 0 0; font-size: 13px; text-align: center;" >
Sent at $ { new Date ( ) . toISOString ( ) }
< / p >
`
const { transporter , from } = await getTransporter ( )
await transporter . sendMail ( {
from ,
to : toEmail ,
subject : 'MOPC Portal - Test Email' ,
text : 'This is a test email from the MOPC Portal. If you received this, your email configuration is working correctly.' ,
html : getEmailWrapper ( content ) ,
} )
return true
} catch {
return false
}
}
/ * *
* Verify SMTP connection
* /
export async function verifyEmailConnection ( ) : Promise < boolean > {
try {
const { transporter } = await getTransporter ( )
await transporter . verify ( )
return true
} catch {
return false
}
}
/ * *
* Send application confirmation email to applicant
* /
export async function sendApplicationConfirmationEmail (
email : string ,
applicantName : string ,
projectName : string ,
programName : string ,
customMessage? : string
) : Promise < void > {
const template = getApplicationConfirmationTemplate (
applicantName ,
projectName ,
programName ,
customMessage
)
const { transporter , from } = await getTransporter ( )
await transporter . sendMail ( {
from ,
to : email ,
subject : template.subject ,
text : template.text ,
html : template.html ,
} )
}
/ * *
* Send team member invite email
* /
export async function sendTeamMemberInviteEmail (
email : string ,
memberName : string ,
projectName : string ,
teamLeadName : string ,
inviteUrl : string
) : Promise < void > {
const template = getTeamMemberInviteTemplate (
memberName ,
projectName ,
teamLeadName ,
inviteUrl
)
const { transporter , from } = await getTransporter ( )
await transporter . sendMail ( {
from ,
to : email ,
subject : template.subject ,
text : template.text ,
html : template.html ,
} )
}
/ * *
* Generate notification email template
* /
function getNotificationEmailTemplate (
name : string ,
subject : string ,
body : string ,
linkUrl? : string
) : EmailTemplate {
const greeting = name ? ` Hello ${ name } , ` : 'Hello,'
// Format body text preserving line breaks
const formattedBody = body
. replace ( /&/g , '&' )
. replace ( /</g , '<' )
. replace ( />/g , '>' )
. replace ( /\n/g , '<br>' )
const content = `
$ { sectionTitle ( greeting ) }
< div style = "color: ${BRAND.textDark}; font-size: 15px; line-height: 1.7; margin: 20px 0;" >
$ { formattedBody }
< / div >
$ { linkUrl ? ctaButton ( linkUrl , 'View Details' ) : '' }
< p style = "color: ${BRAND.textMuted}; margin: 24px 0 0 0; font-size: 13px; text-align: center;" >
You received this email because of your notification preferences on the MOPC Portal .
< / p >
`
return {
subject ,
html : getEmailWrapper ( content ) ,
text : `
$ { greeting }
$ { body }
$ { linkUrl ? ` View details: ${ linkUrl } ` : '' }
-- -
Monaco Ocean Protection Challenge
Together for a healthier ocean .
` ,
}
}
/ * *
* Send notification email ( triggered by in - app notification system )
* /
export async function sendNotificationEmail (
email : string ,
name : string ,
subject : string ,
body : string ,
linkUrl? : string
) : Promise < void > {
2026-02-23 14:27:58 +01:00
const template = getNotificationEmailTemplate ( name , subject , body , ensureAbsoluteUrl ( linkUrl ) )
2026-02-14 15:26:42 +01:00
const { transporter , from } = await getTransporter ( )
await transporter . sendMail ( {
from ,
to : email ,
subject : template.subject ,
text : template.text ,
html : template.html ,
} )
}