Add styled notification emails and round-attached notifications

- Add 15+ styled email templates matching existing invite email design
- Wire up notification triggers in all routers (assignment, round, project, mentor, application, onboarding)
- Add test email button for each notification type in admin settings
- Add round-attached notifications: admins can configure which notification to send when projects enter a round
- Fall back to status-based notifications when round has no configured notification

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-04 00:10:51 +01:00
parent 3be6a743ed
commit b0189cad92
13 changed files with 1892 additions and 28 deletions

View File

@@ -8,7 +8,7 @@ import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { toast } from 'sonner'
import { Users, Scale, GraduationCap, Eye, Shield } from 'lucide-react'
import { Users, Scale, GraduationCap, Eye, Shield, Mail, Loader2 } from 'lucide-react'
// Category icons and labels
const CATEGORIES = {
@@ -29,6 +29,7 @@ type NotificationSetting = {
}
export function NotificationSettingsForm() {
const [testingType, setTestingType] = useState<string | null>(null)
const { data: settings, isLoading, refetch } = trpc.notification.getEmailSettings.useQuery()
const updateMutation = trpc.notification.updateEmailSetting.useMutation({
onSuccess: () => {
@@ -40,10 +41,34 @@ export function NotificationSettingsForm() {
},
})
const testMutation = trpc.notification.sendTestEmail.useMutation({
onSuccess: (data) => {
if (data.success) {
toast.success(data.message, {
description: data.hasStyledTemplate
? 'Using styled template'
: 'Using generic template',
})
} else {
toast.error('Failed to send test email', { description: data.message })
}
setTestingType(null)
},
onError: (error) => {
toast.error(`Failed to send: ${error.message}`)
setTestingType(null)
},
})
const handleToggle = (notificationType: string, sendEmail: boolean) => {
updateMutation.mutate({ notificationType, sendEmail })
}
const handleTest = (notificationType: string) => {
setTestingType(notificationType)
testMutation.mutate({ notificationType })
}
if (isLoading) {
return (
<div className="space-y-4">
@@ -112,7 +137,7 @@ export function NotificationSettingsForm() {
key={setting.id}
className="flex items-center justify-between rounded-lg border p-3"
>
<div className="space-y-0.5">
<div className="space-y-0.5 flex-1 min-w-0">
<Label className="text-sm font-medium">
{setting.label}
</Label>
@@ -122,13 +147,30 @@ export function NotificationSettingsForm() {
</p>
)}
</div>
<Switch
checked={setting.sendEmail}
onCheckedChange={(checked) =>
handleToggle(setting.notificationType, checked)
}
disabled={updateMutation.isPending}
/>
<div className="flex items-center gap-3 ml-4">
<Button
variant="ghost"
size="sm"
className="h-8 px-2 text-muted-foreground hover:text-foreground"
onClick={() => handleTest(setting.notificationType)}
disabled={testingType === setting.notificationType}
title="Send test email to yourself"
>
{testingType === setting.notificationType ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Mail className="h-4 w-4" />
)}
<span className="ml-1.5 text-xs">Test</span>
</Button>
<Switch
checked={setting.sendEmail}
onCheckedChange={(checked) =>
handleToggle(setting.notificationType, checked)
}
disabled={updateMutation.isPending}
/>
</div>
</div>
))}
</CardContent>