feat(finalization): winner email + UI for terminal rounds
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m7s
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m7s
When finalizing a round with no further round to advance to, passing teams
are winners — not advancers. Detected for both special-award terminal rounds
(label = award name) and the main competition's terminal round (label =
competition name). Wording uses "a winner" so it works for both single-winner
awards and top-N main-track outcomes.
Adds AWARD_WINNER_NOTIFICATION email type + template ("Your project has won!"
with "our team will reach out about next steps" copy). Routes through the
notification dispatch table the same way ADVANCEMENT_NOTIFICATION does.
The FinalizationSummary gains a `winnerContext` field; the admin finalization
tab uses it to swap "X projects will advance to Y" → "X winners will be
notified for [label]" and renames "Advancement Message" → "Winner Message"
in the custom-message field. The email-preview button shows the winner
template when applicable.
In-app notification (bell icon) gets matching winner copy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1992,6 +1992,84 @@ Together for a healthier ocean.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate "Award Winner" notification email template — used when finalizing
|
||||
* the terminal round of a special award (no further rounds to advance to).
|
||||
*/
|
||||
export function getAwardWinnerNotificationTemplate(
|
||||
name: string,
|
||||
projectName: string,
|
||||
winnerLabel: string,
|
||||
customMessage?: string,
|
||||
accountUrl?: string,
|
||||
fullCustomBody?: boolean,
|
||||
): EmailTemplate {
|
||||
const greeting = name ? `Congratulations ${name}!` : 'Congratulations!'
|
||||
|
||||
const escapedMessage = customMessage
|
||||
? escapeHtml(customMessage).replace(/\n/g, '<br>')
|
||||
: null
|
||||
|
||||
if (fullCustomBody && escapedMessage) {
|
||||
const content = `
|
||||
${sectionTitle(greeting)}
|
||||
<div style="color: ${BRAND.textDark}; font-size: 15px; line-height: 1.7; margin: 20px 0;">${escapedMessage}</div>
|
||||
${accountUrl ? ctaButton(accountUrl, 'Create Your Account') : ctaButton('/applicant', 'View Your Dashboard')}
|
||||
`
|
||||
return {
|
||||
subject: `Your project has won: "${projectName}"`,
|
||||
html: getEmailWrapper(content),
|
||||
text: `${greeting}\n\n${customMessage}\n\n${accountUrl ? `Create your account: ${getBaseUrl()}${accountUrl}` : `Visit your dashboard: ${getBaseUrl()}/applicant`}\n\n---\nMonaco Ocean Protection Challenge\nTogether for a healthier ocean.`,
|
||||
}
|
||||
}
|
||||
|
||||
const winnerBanner = `
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="margin: 20px 0;">
|
||||
<tr>
|
||||
<td style="background: linear-gradient(135deg, #b45309 0%, #f59e0b 100%); border-radius: 12px; padding: 28px; text-align: center;">
|
||||
<p style="color: #ffffff; margin: 0 0 8px 0; font-size: 14px; font-weight: 600; text-transform: uppercase; letter-spacing: 2px;">Winner Announcement</p>
|
||||
<h2 style="color: #ffffff; margin: 0; font-size: 28px; font-weight: 700;">Your project has won!</h2>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
`
|
||||
|
||||
const content = `
|
||||
${sectionTitle(greeting)}
|
||||
${winnerBanner}
|
||||
${infoBox(`<strong>"${escapeHtml(projectName)}"</strong> has been selected as a winner of <strong>${escapeHtml(winnerLabel)}</strong>.`, 'success')}
|
||||
${
|
||||
escapedMessage
|
||||
? `<div style="background-color: #f5f5f5; border-radius: 8px; padding: 20px; margin: 20px 0; color: ${BRAND.textDark}; font-size: 15px; line-height: 1.7;">${escapedMessage}</div>`
|
||||
: paragraph('Our team will reach out shortly with details on the next steps, including award presentation, communications, and any administrative requirements.')
|
||||
}
|
||||
${accountUrl ? ctaButton(accountUrl, 'Create Your Account') : ctaButton('/applicant', 'View Your Dashboard')}
|
||||
`
|
||||
|
||||
return {
|
||||
subject: `Your project has won: "${projectName}"`,
|
||||
html: getEmailWrapper(content),
|
||||
text: `
|
||||
${greeting}
|
||||
|
||||
Your project has won!
|
||||
|
||||
Project: ${projectName}
|
||||
${winnerLabel}
|
||||
|
||||
"${projectName}" has been selected as a winner of ${winnerLabel}.
|
||||
|
||||
${customMessage || 'Our team will reach out shortly with details on the next steps, including award presentation, communications, and any administrative requirements.'}
|
||||
|
||||
${accountUrl ? `Create your account: ${getBaseUrl()}${accountUrl}` : `Visit your dashboard: ${getBaseUrl()}/applicant`}
|
||||
|
||||
---
|
||||
Monaco Ocean Protection Challenge
|
||||
Together for a healthier ocean.
|
||||
`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate "Under Consideration for Special Award" notification email template
|
||||
*/
|
||||
@@ -2273,6 +2351,16 @@ export const NOTIFICATION_EMAIL_TEMPLATES: Record<string, TemplateGenerator> = {
|
||||
ctx.metadata?.fullCustomBody as boolean | undefined,
|
||||
),
|
||||
|
||||
AWARD_WINNER_NOTIFICATION: (ctx) =>
|
||||
getAwardWinnerNotificationTemplate(
|
||||
ctx.name || '',
|
||||
(ctx.metadata?.projectName as string) || 'Your Project',
|
||||
(ctx.metadata?.awardName as string) || 'Special Award',
|
||||
ctx.metadata?.customMessage as string | undefined,
|
||||
ctx.metadata?.accountUrl as string | undefined,
|
||||
ctx.metadata?.fullCustomBody as boolean | undefined,
|
||||
),
|
||||
|
||||
AWARD_SELECTION_NOTIFICATION: (ctx) =>
|
||||
getAwardSelectionNotificationTemplate(
|
||||
ctx.name || '',
|
||||
|
||||
Reference in New Issue
Block a user