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:
@@ -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, '&')
|
||||
.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 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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user