Award shortlist UX improvements + configurable invite link expiry
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m30s

Award shortlist:
- Expandable reasoning text (click to toggle, hover hint)
- Bulk select/deselect all checkbox in header
- Top N projects highlighted with amber background
- New bulkToggleShortlisted backend mutation

Invite link expiry:
- New "Invitation Link Expiry (hours)" field in Security Settings
- Reads from systemSettings `invite_link_expiry_hours` (default 72h / 3 days)
- Email template dynamically shows "X hours" or "X days" based on setting
- All 3 invite paths (bulk create, single invite, bulk resend) use setting

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-17 22:05:58 +01:00
parent 8a7da0fd93
commit d02b0b91b9
6 changed files with 156 additions and 24 deletions

View File

@@ -289,21 +289,29 @@ Together for a healthier ocean.
/**
* Generate generic invitation email template (not round-specific)
*/
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' : ''}`
}
function getGenericInvitationTemplate(
name: string,
url: string,
role: string
role: string,
expiryHours: number = 72
): 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,'
const expiryLabel = formatExpiryLabel(expiryHours)
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')}
${infoBox('This link will expire in 7 days.', 'info')}
${infoBox(`This link will expire in ${expiryLabel}.`, 'info')}
`
return {
@@ -318,7 +326,7 @@ Click the link below to set up your account and get started:
${url}
This link will expire in 7 days.
This link will expire in ${expiryLabel}.
---
Monaco Ocean Protection Challenge
@@ -1581,9 +1589,10 @@ export async function sendInvitationEmail(
email: string,
name: string | null,
url: string,
role: string
role: string,
expiryHours?: number
): Promise<void> {
const template = getGenericInvitationTemplate(name || '', url, role)
const template = getGenericInvitationTemplate(name || '', url, role, expiryHours)
const { transporter, from } = await getTransporter()
await transporter.sendMail({