feat: observer UX overhaul — reports, projects, charts, session & email
All checks were successful
Build and Push Docker Image / build (push) Successful in 11m2s
All checks were successful
Build and Push Docker Image / build (push) Successful in 11m2s
- Observer projects: default sort by status (rejected last), sortable status column - Observer projects: search by country, institution, geographic zone - Observer project detail: vertical timeline connectors between rounds - Fix React key warning in ExpandableJurorTable and FilteringReportTabs - Fix ScoreBadge text always white for better contrast on all backgrounds - Remove misleading /30 denominator from heatmap juror reviewed count - INTAKE stats: show Start-ups, Business Concepts, Countries (not States/Categories) - DiversityMetrics: extractCountry() for country-only display in charts - Fix nested button hydration error in filtering report mobile view - Color project titles by outcome in filtering report (green/red/amber) - Redesign CrossStageComparisonChart: funnel viz + metrics table with attrition % - Center doughnut chart in StatusBreakdownChart - Remove redundant RoundTypeStatsCards from evaluation report - Move evaluation tab bar below overview header, rename to "Juror Assignments" - Dev email override system (DEV_EMAIL_OVERRIDE env var) - Session refresh on role change without re-login - Role switcher in user dropdown menu - formatCategory() utility for consistent category display - Activity feed max height constraint Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
118
src/lib/email.ts
118
src/lib/email.ts
@@ -2,6 +2,19 @@ import nodemailer from 'nodemailer'
|
||||
import type { Transporter } from 'nodemailer'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
/**
|
||||
* Dev email override: when DEV_EMAIL_OVERRIDE is set, ALL outgoing emails
|
||||
* are redirected to that address. The original recipient is noted in the subject.
|
||||
*/
|
||||
const DEV_EMAIL_OVERRIDE = process.env.DEV_EMAIL_OVERRIDE || ''
|
||||
|
||||
async function sendEmail(opts: { to: string; subject: string; text: string; html: string }): Promise<void> {
|
||||
const { transporter, from } = await getTransporter()
|
||||
const to = DEV_EMAIL_OVERRIDE || opts.to
|
||||
const subject = DEV_EMAIL_OVERRIDE ? `[DEV → ${opts.to}] ${opts.subject}` : opts.subject
|
||||
await transporter.sendMail({ from, to, subject, text: opts.text, html: opts.html })
|
||||
}
|
||||
|
||||
// Cached transporter and config hash to detect changes
|
||||
let cachedTransporter: Transporter | null = null
|
||||
let cachedConfigHash = ''
|
||||
@@ -2253,15 +2266,7 @@ export async function sendStyledNotificationEmail(
|
||||
)
|
||||
}
|
||||
|
||||
const { transporter, from } = await getTransporter()
|
||||
|
||||
await transporter.sendMail({
|
||||
from,
|
||||
to: email,
|
||||
subject: template.subject,
|
||||
text: template.text,
|
||||
html: template.html,
|
||||
})
|
||||
await sendEmail({ to: email, subject: template.subject, text: template.text, html: template.html })
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -2277,15 +2282,7 @@ export async function sendPasswordResetEmail(
|
||||
expiryMinutes: number = 30
|
||||
): Promise<void> {
|
||||
const template = getPasswordResetTemplate(url, expiryMinutes)
|
||||
const { transporter, from } = await getTransporter()
|
||||
|
||||
await transporter.sendMail({
|
||||
from,
|
||||
to: email,
|
||||
subject: template.subject,
|
||||
text: template.text,
|
||||
html: template.html,
|
||||
})
|
||||
await sendEmail({ to: email, subject: template.subject, text: template.text, html: template.html })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2297,15 +2294,7 @@ export async function sendMagicLinkEmail(
|
||||
): 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,
|
||||
})
|
||||
await sendEmail({ to: email, subject: template.subject, text: template.text, html: template.html })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2319,15 +2308,7 @@ export async function sendInvitationEmail(
|
||||
expiryHours?: number
|
||||
): Promise<void> {
|
||||
const template = getGenericInvitationTemplate(name || '', url, role, expiryHours)
|
||||
const { transporter, from } = await getTransporter()
|
||||
|
||||
await transporter.sendMail({
|
||||
from,
|
||||
to: email,
|
||||
subject: template.subject,
|
||||
text: template.text,
|
||||
html: template.html,
|
||||
})
|
||||
await sendEmail({ to: email, subject: template.subject, text: template.text, html: template.html })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2340,15 +2321,7 @@ export async function sendJuryInvitationEmail(
|
||||
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,
|
||||
})
|
||||
await sendEmail({ to: email, subject: template.subject, text: template.text, html: template.html })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2369,15 +2342,7 @@ export async function sendEvaluationReminderEmail(
|
||||
deadline,
|
||||
assignmentsUrl
|
||||
)
|
||||
const { transporter, from } = await getTransporter()
|
||||
|
||||
await transporter.sendMail({
|
||||
from,
|
||||
to: email,
|
||||
subject: template.subject,
|
||||
text: template.text,
|
||||
html: template.html,
|
||||
})
|
||||
await sendEmail({ to: email, subject: template.subject, text: template.text, html: template.html })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2398,15 +2363,7 @@ export async function sendAnnouncementEmail(
|
||||
ctaText,
|
||||
ctaUrl
|
||||
)
|
||||
const { transporter, from } = await getTransporter()
|
||||
|
||||
await transporter.sendMail({
|
||||
from,
|
||||
to: email,
|
||||
subject: template.subject,
|
||||
text: template.text,
|
||||
html: template.html,
|
||||
})
|
||||
await sendEmail({ to: email, subject: template.subject, text: template.text, html: template.html })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2422,10 +2379,7 @@ export async function sendTestEmail(toEmail: string): Promise<boolean> {
|
||||
Sent at ${new Date().toISOString()}
|
||||
</p>
|
||||
`
|
||||
const { transporter, from } = await getTransporter()
|
||||
|
||||
await transporter.sendMail({
|
||||
from,
|
||||
await sendEmail({
|
||||
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.',
|
||||
@@ -2466,15 +2420,7 @@ export async function sendApplicationConfirmationEmail(
|
||||
programName,
|
||||
customMessage
|
||||
)
|
||||
const { transporter, from } = await getTransporter()
|
||||
|
||||
await transporter.sendMail({
|
||||
from,
|
||||
to: email,
|
||||
subject: template.subject,
|
||||
text: template.text,
|
||||
html: template.html,
|
||||
})
|
||||
await sendEmail({ to: email, subject: template.subject, text: template.text, html: template.html })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2493,15 +2439,7 @@ export async function sendTeamMemberInviteEmail(
|
||||
teamLeadName,
|
||||
inviteUrl
|
||||
)
|
||||
const { transporter, from } = await getTransporter()
|
||||
|
||||
await transporter.sendMail({
|
||||
from,
|
||||
to: email,
|
||||
subject: template.subject,
|
||||
text: template.text,
|
||||
html: template.html,
|
||||
})
|
||||
await sendEmail({ to: email, subject: template.subject, text: template.text, html: template.html })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2561,13 +2499,5 @@ export async function sendNotificationEmail(
|
||||
linkUrl?: string
|
||||
): Promise<void> {
|
||||
const template = getNotificationEmailTemplate(name, subject, body, ensureAbsoluteUrl(linkUrl))
|
||||
const { transporter, from } = await getTransporter()
|
||||
|
||||
await transporter.sendMail({
|
||||
from,
|
||||
to: email,
|
||||
subject: template.subject,
|
||||
text: template.text,
|
||||
html: template.html,
|
||||
})
|
||||
await sendEmail({ to: email, subject: template.subject, text: template.text, html: template.html })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user