Add notification bell system and MOPC onboarding form

Notification System:
- Add InAppNotification and NotificationEmailSetting database models
- Create notification service with 60+ notification types for all user roles
- Add notification router with CRUD endpoints
- Build NotificationBell UI component with dropdown and unread count
- Integrate bell into admin, jury, mentor, and observer navs
- Add notification email settings admin UI in Settings > Notifications
- Add notification triggers to filtering router (complete/failed)
- Add sendNotificationEmail function to email library
- Add formatRelativeTime utility function

MOPC Onboarding Form:
- Create /apply landing page with auto-redirect for single form
- Create seed script for MOPC 2026 application form (6 steps)
- Create seed script for default notification email settings

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-03 21:30:25 +01:00
parent e1968d45df
commit 0277768ed7
18 changed files with 2344 additions and 13 deletions

View File

@@ -780,3 +780,71 @@ export async function sendTeamMemberInviteEmail(
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.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 Platform.
</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> {
const template = getNotificationEmailTemplate(name, subject, body, linkUrl)
const { transporter, from } = await getTransporter()
await transporter.sendMail({
from,
to: email,
subject: template.subject,
text: template.text,
html: template.html,
})
}

View File

@@ -59,3 +59,25 @@ export function daysUntil(date: Date | string): number {
const now = new Date()
return Math.ceil((target.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
}
export function formatRelativeTime(date: Date | string): string {
const now = new Date()
const target = new Date(date)
const diffMs = now.getTime() - target.getTime()
const diffSec = Math.floor(diffMs / 1000)
const diffMin = Math.floor(diffSec / 60)
const diffHour = Math.floor(diffMin / 60)
const diffDay = Math.floor(diffHour / 24)
const diffWeek = Math.floor(diffDay / 7)
if (diffSec < 60) return 'just now'
if (diffMin < 60) return `${diffMin}m ago`
if (diffHour < 24) return `${diffHour}h ago`
if (diffDay < 7) return `${diffDay}d ago`
if (diffWeek < 4) return `${diffWeek}w ago`
return new Intl.DateTimeFormat('en-US', {
month: 'short',
day: 'numeric',
}).format(target)
}