diff --git a/app/api/admin/hackathons/route.ts b/app/api/admin/hackathons/route.ts
index 7b257b4c..10ba8701 100644
--- a/app/api/admin/hackathons/route.ts
+++ b/app/api/admin/hackathons/route.ts
@@ -117,7 +117,7 @@ export async function PUT(request: NextRequest) {
return NextResponse.json({ error: 'Hackathon data is required' }, { status: 400 })
}
- const hackathon = await hackathonsService.updateHackathon(slug, data)
+ const hackathon = await hackathonsService.updateHackathon(slug, data, user.id)
return NextResponse.json(hackathon)
} catch (error) {
diff --git a/app/api/admin/moderation/events/[id]/approve/route.ts b/app/api/admin/moderation/events/[id]/approve/route.ts
index a453adfd..767b3fe1 100644
--- a/app/api/admin/moderation/events/[id]/approve/route.ts
+++ b/app/api/admin/moderation/events/[id]/approve/route.ts
@@ -40,20 +40,58 @@ export async function POST(
notes
)
- // Send notification email to company
- if (approvedEvent.company && approvedEvent.company.email) {
- const emailContent = getEventApprovedEmail({
- eventTitle: approvedEvent.title,
- companyName: approvedEvent.company.name,
- eventUrl: `${process.env.NEXT_PUBLIC_SITE_URL || 'https://codeunia.com'}/events/${approvedEvent.slug}`,
- publishDate: new Date().toLocaleDateString(),
- notes: notes || '',
+ // Get creator's email from profiles table
+ let creatorEmail: string | null = null
+ let creatorName: string | null = null
+
+ if (approvedEvent.created_by) {
+ const { createClient } = await import('@/lib/supabase/server')
+ const supabase = await createClient()
+ const { data: creatorProfile } = await supabase
+ .from('profiles')
+ .select('email, first_name, last_name')
+ .eq('id', approvedEvent.created_by)
+ .single()
+
+ if (creatorProfile) {
+ creatorEmail = creatorProfile.email
+ creatorName = creatorProfile.first_name
+ ? `${creatorProfile.first_name} ${creatorProfile.last_name || ''}`.trim()
+ : null
+ }
+ }
+
+ // Prepare email content
+ const emailContent = getEventApprovedEmail({
+ eventTitle: approvedEvent.title,
+ companyName: approvedEvent.company?.name || 'Your Company',
+ eventUrl: `${process.env.NEXT_PUBLIC_SITE_URL || 'https://codeunia.com'}/events/${approvedEvent.slug}`,
+ publishDate: new Date().toLocaleDateString(),
+ notes: notes || '',
+ creatorName: creatorName || undefined,
+ })
+
+ // Send notification email to event creator (primary)
+ if (creatorEmail) {
+ console.log(`📧 Sending event approval email to creator: ${creatorEmail}`)
+ await sendEmail({
+ to: creatorEmail,
+ subject: emailContent.subject,
+ html: emailContent.html,
+ }).catch(error => {
+ console.error('❌ Failed to send approval email to creator:', error)
})
+ }
+ // Also send to company email if different from creator
+ if (approvedEvent.company?.email && approvedEvent.company.email !== creatorEmail) {
+ console.log(`📧 Sending event approval email to company: ${approvedEvent.company.email}`)
await sendEmail({
to: approvedEvent.company.email,
subject: emailContent.subject,
html: emailContent.html,
+ }).catch(error => {
+ console.error('❌ Failed to send approval email to company:', error)
})
}
@@ -90,12 +128,19 @@ function getEventApprovedEmail(params: {
eventUrl: string
publishDate: string
notes: string
+ creatorName?: string
}) {
+ const greeting = params.creatorName ? `Hi ${params.creatorName},` : 'Hello,'
+
const content = `
🎉 Your Event is Live!
+
+ ${greeting}
+
+
Great news! Your event has been approved and is now live on CodeUnia.
diff --git a/app/api/admin/moderation/events/[id]/reject/route.ts b/app/api/admin/moderation/events/[id]/reject/route.ts
index b4adb9eb..e82d5e23 100644
--- a/app/api/admin/moderation/events/[id]/reject/route.ts
+++ b/app/api/admin/moderation/events/[id]/reject/route.ts
@@ -50,20 +50,58 @@ export async function POST(
reason
)
- // Send notification email to company
- if (rejectedEvent.company && rejectedEvent.company.email) {
- const emailContent = getEventRejectedEmail({
- eventTitle: rejectedEvent.title,
- companyName: rejectedEvent.company.name,
- rejectionReason: reason,
- editUrl: `${process.env.NEXT_PUBLIC_SITE_URL || 'https://codeunia.com'}/dashboard/company/events/${rejectedEvent.slug}/edit`,
- guidelines: `${process.env.NEXT_PUBLIC_SITE_URL || 'https://codeunia.com'}/guidelines`,
+ // Get creator's email from profiles table
+ let creatorEmail: string | null = null
+ let creatorName: string | null = null
+
+ if (rejectedEvent.created_by) {
+ const { createClient } = await import('@/lib/supabase/server')
+ const supabase = await createClient()
+ const { data: creatorProfile } = await supabase
+ .from('profiles')
+ .select('email, first_name, last_name')
+ .eq('id', rejectedEvent.created_by)
+ .single()
+
+ if (creatorProfile) {
+ creatorEmail = creatorProfile.email
+ creatorName = creatorProfile.first_name
+ ? `${creatorProfile.first_name} ${creatorProfile.last_name || ''}`.trim()
+ : null
+ }
+ }
+
+ // Prepare email content
+ const emailContent = getEventRejectedEmail({
+ eventTitle: rejectedEvent.title,
+ companyName: rejectedEvent.company?.name || 'Your Company',
+ rejectionReason: reason,
+ editUrl: `${process.env.NEXT_PUBLIC_SITE_URL || 'https://codeunia.com'}/dashboard/company/${rejectedEvent.company?.slug}/events`,
+ guidelines: `${process.env.NEXT_PUBLIC_SITE_URL || 'https://codeunia.com'}/guidelines`,
+ creatorName: creatorName || undefined,
+ })
+
+ // Send notification email to event creator (primary)
+ if (creatorEmail) {
+ console.log(`📧 Sending event rejection email to creator: ${creatorEmail}`)
+ await sendEmail({
+ to: creatorEmail,
+ subject: emailContent.subject,
+ html: emailContent.html,
+ }).catch(error => {
+ console.error('❌ Failed to send rejection email to creator:', error)
})
+ }
+ // Also send to company email if different from creator
+ if (rejectedEvent.company?.email && rejectedEvent.company.email !== creatorEmail) {
+ console.log(`📧 Sending event rejection email to company: ${rejectedEvent.company.email}`)
await sendEmail({
to: rejectedEvent.company.email,
subject: emailContent.subject,
html: emailContent.html,
+ }).catch(error => {
+ console.error('❌ Failed to send rejection email to company:', error)
})
}
@@ -100,14 +138,21 @@ function getEventRejectedEmail(params: {
rejectionReason: string
editUrl: string
guidelines: string
+ creatorName?: string
}) {
+ const greeting = params.creatorName ? `Hi ${params.creatorName},` : 'Hello,'
+
const content = `
Event Review Update
- Thank you for submitting your event to CodeUnia. After review, we're unable to approve your event at this time.
+ ${greeting}
+
+
+
+ Thank you for submitting your event to Codeunia. After review, we're unable to approve your event at this time.
@@ -158,7 +203,7 @@ function getEventRejectedEmail(params: {
-
Event Review Update - CodeUnia
+
Event Review Update - Codeunia
@@ -167,7 +212,7 @@ function getEventRejectedEmail(params: {
- CodeUnia
+ Codeunia
|
@@ -181,7 +226,7 @@ function getEventRejectedEmail(params: {
Need help? Visit our Help Center
- © ${new Date().getFullYear()} CodeUnia. All rights reserved.
+ © ${new Date().getFullYear()} Codeunia. All rights reserved.
diff --git a/app/api/companies/[slug]/members/[userId]/route.ts b/app/api/companies/[slug]/members/[userId]/route.ts
index c1e30786..030b58f9 100644
--- a/app/api/companies/[slug]/members/[userId]/route.ts
+++ b/app/api/companies/[slug]/members/[userId]/route.ts
@@ -5,7 +5,7 @@ import { companyMemberService } from '@/lib/services/company-member-service'
import { CompanyError } from '@/types/company'
import { UnifiedCache } from '@/lib/unified-cache-system'
import { z } from 'zod'
-import { getRoleChangeEmail, sendCompanyEmail } from '@/lib/email/company-emails'
+import { getRoleChangeEmail, getMemberRemovedEmail, sendCompanyEmail } from '@/lib/email/company-emails'
// Force Node.js runtime for API routes
export const runtime = 'nodejs'
@@ -263,9 +263,50 @@ export async function DELETE(
)
}
+ // Get member's profile information for email before removing
+ const { data: memberProfile } = await supabase
+ .from('profiles')
+ .select('email, first_name, last_name')
+ .eq('id', userId)
+ .single()
+
+ // Get requesting user's name for email
+ const { data: requestingUserProfile } = await supabase
+ .from('profiles')
+ .select('first_name, last_name')
+ .eq('id', user.id)
+ .single()
+
+ const removedByName = requestingUserProfile?.first_name
+ ? `${requestingUserProfile.first_name} ${requestingUserProfile.last_name || ''}`.trim()
+ : 'a team administrator'
+
// Remove member
await companyMemberService.removeMember(targetMember.id)
+ // Send removal notification email
+ if (memberProfile?.email) {
+ const memberName = memberProfile.first_name || memberProfile.email.split('@')[0]
+
+ const emailContent = getMemberRemovedEmail({
+ memberName,
+ companyName: company.name,
+ removedBy: removedByName,
+ role: targetMember.role,
+ })
+
+ // Send email asynchronously (don't wait for it)
+ console.log(`📧 Sending member removal email to ${memberProfile.email}`)
+ sendCompanyEmail({
+ to: memberProfile.email,
+ subject: emailContent.subject,
+ html: emailContent.html,
+ }).catch(error => {
+ console.error('❌ Failed to send member removal email:', error)
+ // Don't fail the request if email fails
+ })
+ }
+
// Invalidate cache
await UnifiedCache.purgeByTags(['content', 'api'])
diff --git a/app/api/hackathons/[id]/route.ts b/app/api/hackathons/[id]/route.ts
index 859ee2d6..d5ca61f0 100644
--- a/app/api/hackathons/[id]/route.ts
+++ b/app/api/hackathons/[id]/route.ts
@@ -96,7 +96,7 @@ export async function PUT(request: NextRequest, { params }: RouteContext) {
)
}
- const hackathon = await hackathonsService.updateHackathon(id, hackathonData)
+ const hackathon = await hackathonsService.updateHackathon(id, hackathonData, user.id)
return NextResponse.json({ hackathon })
} catch (error) {
diff --git a/app/companies/[slug]/events/page.tsx b/app/companies/[slug]/events/page.tsx
index 9a72069d..0ba7e505 100644
--- a/app/companies/[slug]/events/page.tsx
+++ b/app/companies/[slug]/events/page.tsx
@@ -162,7 +162,7 @@ export default function CompanyEventsPage() {
{/* Company Header */}
{company && (
-
+
+
-
+
{/* Banner */}
-
+
{/* Logo */}
diff --git a/components/notifications/notification-utils.tsx b/components/notifications/notification-utils.tsx
index 6e0b84f5..3b1ad82b 100644
--- a/components/notifications/notification-utils.tsx
+++ b/components/notifications/notification-utils.tsx
@@ -18,9 +18,13 @@ export function getNotificationIcon(type: NotificationType): LucideIcon {
event_approved: CheckCircle2,
event_rejected: XCircle,
event_changes_requested: AlertCircle,
+ event_updated: AlertCircle,
+ event_status_changed: AlertCircle,
hackathon_approved: CheckCircle2,
hackathon_rejected: XCircle,
hackathon_changes_requested: AlertCircle,
+ hackathon_updated: AlertCircle,
+ hackathon_status_changed: AlertCircle,
new_event_registration: Calendar,
new_hackathon_registration: Calendar,
team_member_invited: UserPlus,
@@ -40,9 +44,13 @@ export function getNotificationColor(type: NotificationType): string {
event_approved: 'text-green-500',
event_rejected: 'text-red-500',
event_changes_requested: 'text-yellow-500',
+ event_updated: 'text-orange-500',
+ event_status_changed: 'text-yellow-500',
hackathon_approved: 'text-green-500',
hackathon_rejected: 'text-red-500',
hackathon_changes_requested: 'text-yellow-500',
+ hackathon_updated: 'text-orange-500',
+ hackathon_status_changed: 'text-yellow-500',
new_event_registration: 'text-blue-500',
new_hackathon_registration: 'text-blue-500',
team_member_invited: 'text-blue-500',
diff --git a/lib/email/company-emails.ts b/lib/email/company-emails.ts
index 0f13d52e..34168d92 100644
--- a/lib/email/company-emails.ts
+++ b/lib/email/company-emails.ts
@@ -41,7 +41,7 @@ const getEmailTemplate = (content: string) => `
Need help? Contact us at support@codeunia.com
- © ${new Date().getFullYear()} CodeUnia. All rights reserved.
+ © ${new Date().getFullYear()} Codeunia. All rights reserved.
@@ -64,7 +64,7 @@ export const getCompanyVerificationApprovedEmail = (params: {
- Congratulations! Your company ${params.companyName} has been successfully verified on CodeUnia.
+ Congratulations! Your company ${params.companyName} has been successfully verified on Codeunia.
@@ -96,7 +96,7 @@ export const getCompanyVerificationApprovedEmail = (params: {
`
return {
- subject: `🎉 ${params.companyName} has been verified on CodeUnia!`,
+ subject: `🎉 ${params.companyName} has been verified on Codeunia!`,
html: getEmailTemplate(content)
}
}
@@ -113,7 +113,7 @@ export const getCompanyVerificationRejectedEmail = (params: {
- Thank you for your interest in hosting events on CodeUnia. After reviewing your company registration for ${params.companyName}, we need additional information before we can proceed with verification.
+ Thank you for your interest in hosting events on Codeunia. After reviewing your company registration for ${params.companyName}, we need additional information before we can proceed with verification.
@@ -165,7 +165,7 @@ export const getNewCompanyRegistrationNotification = (params: {
- A new company has registered on CodeUnia and is awaiting verification.
+ A new company has registered on Codeunia and is awaiting verification.
@@ -228,6 +228,63 @@ export const getNewCompanyRegistrationNotification = (params: {
}
}
+// Team member removed notification email
+export const getMemberRemovedEmail = (params: {
+ memberName: string
+ companyName: string
+ removedBy: string
+ role: string
+}) => {
+ const content = `
+
+ You've Been Removed from ${params.companyName}
+
+
+
+ Hi ${params.memberName},
+
+
+
+ We're writing to inform you that your access to ${params.companyName} on Codeunia has been removed by ${params.removedBy}.
+
+
+
+
+ Access Removed: You no longer have access to ${params.companyName}'s company dashboard, events, and team resources.
+
+
+
+
+ What this means:
+
+
+
+ - You can no longer access ${params.companyName}'s dashboard
+ - You won't receive notifications about company events
+ - Your previous role was: ${params.role.charAt(0).toUpperCase() + params.role.slice(1)}
+
+
+
+ Your personal Codeunia account remains active, and you can still:
+
+
+
+ - Browse and register for public events
+ - Access your profile and connections
+ - Join other companies if invited
+
+
+
+ If you believe this was done in error or have questions, please contact the company administrator directly.
+
+ `
+
+ return {
+ subject: `Access removed from ${params.companyName}`,
+ html: getEmailTemplate(content)
+ }
+}
+
// Role change notification email
export const getRoleChangeEmail = (params: {
memberName: string
@@ -340,7 +397,7 @@ export async function sendCompanyEmail(params: EmailParams) {
const resend = new Resend(process.env.RESEND_API_KEY)
const { data, error } = await resend.emails.send({
- from: process.env.COMPANY_FROM_EMAIL || 'CodeUnia
',
+ from: process.env.COMPANY_FROM_EMAIL || 'Codeunia ',
to: params.to,
subject: params.subject,
html: params.html,
diff --git a/lib/email/templates/event-rejected.tsx b/lib/email/templates/event-rejected.tsx
index 399c0f75..4c1ac719 100644
--- a/lib/email/templates/event-rejected.tsx
+++ b/lib/email/templates/event-rejected.tsx
@@ -14,7 +14,7 @@ const getEmailTemplate = (content: string) => `
- CodeUnia
+ Codeunia
@@ -23,7 +23,7 @@ const getEmailTemplate = (content: string) => `
- CodeUnia
+ Codeunia
|
@@ -37,7 +37,7 @@ const getEmailTemplate = (content: string) => `
Need help? Contact us at support@codeunia.com
- © ${new Date().getFullYear()} CodeUnia. All rights reserved.
+ © ${new Date().getFullYear()} Codeunia. All rights reserved.
diff --git a/lib/services/events.ts b/lib/services/events.ts
index a9d01750..be3febc8 100644
--- a/lib/services/events.ts
+++ b/lib/services/events.ts
@@ -401,14 +401,13 @@ class EventsService {
* Update an event
* @param id Event ID
* @param eventData Partial event data to update
- * @param _userId ID of the user updating the event (reserved for future use)
+ * @param userId ID of the user updating the event
* @returns Updated event
*/
async updateEvent(
id: number,
eventData: Partial,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- _userId: string
+ userId: string
): Promise {
const supabase = await createClient()
@@ -436,12 +435,24 @@ class EventsService {
} = eventData
/* eslint-enable @typescript-eslint/no-unused-vars */
+ // Check if this is an approved event being edited
+ // If so, reset to pending status for re-approval
+ const needsReapproval = existingEvent.approval_status === 'approved'
+
// Update the updated_at timestamp
- const updatePayload = {
+ const updatePayload: Record = {
...updateData,
updated_at: new Date().toISOString(),
}
+ // If event was approved, reset to pending for re-approval
+ if (needsReapproval) {
+ updatePayload.approval_status = 'pending'
+ updatePayload.approved_by = null
+ updatePayload.approved_at = null
+ console.log(`🔄 Event ${id} was approved, resetting to pending for re-approval`)
+ }
+
const { data: event, error } = await supabase
.from('events')
.update(updatePayload)
@@ -463,6 +474,62 @@ class EventsService {
throw new EventError('Failed to update event', EventErrorCodes.NOT_FOUND, 500)
}
+ // If event needed re-approval, create notifications and log
+ if (needsReapproval) {
+ // Import notification service dynamically to avoid circular dependencies
+ const { NotificationService } = await import('./notification-service')
+ const { moderationService } = await import('./moderation-service')
+
+ // Log the edit action
+ await moderationService.logModerationAction('edited', id, undefined, userId, 'Event edited after approval - requires re-approval')
+
+ // Notify admins about the updated event
+ // Get all admin users
+ const { data: adminUsers } = await supabase
+ .from('profiles')
+ .select('id')
+ .eq('role', 'admin')
+
+ if (adminUsers && adminUsers.length > 0) {
+ // Create notifications for all admins
+ const notifications = adminUsers.map(admin => ({
+ user_id: admin.id,
+ company_id: existingEvent.company_id,
+ type: 'event_updated' as const,
+ title: 'Event Updated - Requires Re-approval',
+ message: `"${existingEvent.title}" has been edited and requires re-approval`,
+ action_url: `/admin/moderation/events/${id}`,
+ action_label: 'Review Event',
+ event_id: id.toString(),
+ metadata: {
+ event_title: existingEvent.title,
+ event_slug: existingEvent.slug
+ }
+ }))
+
+ await supabase.from('notifications').insert(notifications)
+ console.log(`📧 Notified ${adminUsers.length} admin(s) about updated event`)
+ }
+
+ // Notify company members about the status change
+ if (existingEvent.company_id) {
+ await NotificationService.notifyCompanyMembers(existingEvent.company_id, {
+ type: 'event_status_changed',
+ title: 'Event Requires Re-approval',
+ message: `Your event "${existingEvent.title}" has been edited and requires re-approval`,
+ company_id: existingEvent.company_id,
+ action_url: `/dashboard/company/events/${existingEvent.slug}`,
+ action_label: 'View Event',
+ event_id: id.toString(),
+ metadata: {
+ event_title: existingEvent.title,
+ old_status: 'approved',
+ new_status: 'pending'
+ }
+ })
+ }
+ }
+
clearCache()
return event
}
diff --git a/lib/services/hackathons.ts b/lib/services/hackathons.ts
index ab6d5291..21005ef6 100644
--- a/lib/services/hackathons.ts
+++ b/lib/services/hackathons.ts
@@ -197,14 +197,44 @@ class HackathonsService {
return hackathon
}
- async updateHackathon(slug: string, hackathonData: Partial>): Promise {
+ async updateHackathon(
+ slug: string,
+ hackathonData: Partial>,
+ userId?: string
+ ): Promise {
const supabase = await createClient()
+ // Get existing hackathon first
+ const existingHackathon = await this.getHackathonBySlug(slug)
+ if (!existingHackathon) {
+ throw new Error('Hackathon not found')
+ }
+
+ // Check if this is an approved hackathon being edited
+ const needsReapproval = existingHackathon.approval_status === 'approved'
+
+ // Prepare update payload
+ const updatePayload: Record = {
+ ...hackathonData,
+ updated_at: new Date().toISOString(),
+ }
+
+ // If hackathon was approved, reset to pending for re-approval
+ if (needsReapproval) {
+ updatePayload.approval_status = 'pending'
+ updatePayload.approved_by = null
+ updatePayload.approved_at = null
+ console.log(`🔄 Hackathon ${existingHackathon.id} was approved, resetting to pending for re-approval`)
+ }
+
const { data: hackathon, error } = await supabase
.from('hackathons')
- .update(hackathonData)
+ .update(updatePayload)
.eq('slug', slug)
- .select()
+ .select(`
+ *,
+ company:companies(*)
+ `)
.single()
if (error) {
@@ -212,6 +242,66 @@ class HackathonsService {
throw new Error('Failed to update hackathon')
}
+ // If hackathon needed re-approval, create notifications and log
+ if (needsReapproval && userId) {
+ const hackathonId = existingHackathon.id
+ if (!hackathonId) {
+ console.error('Hackathon ID is missing, cannot create notifications')
+ cache.clear()
+ return hackathon
+ }
+ // Import services dynamically to avoid circular dependencies
+ const { NotificationService } = await import('./notification-service')
+ const { moderationService } = await import('./moderation-service')
+
+ // Log the edit action
+ await moderationService.logModerationAction('edited', undefined, hackathonId, userId, 'Hackathon edited after approval - requires re-approval')
+
+ // Notify admins about the updated hackathon
+ const { data: adminUsers } = await supabase
+ .from('profiles')
+ .select('id')
+ .eq('role', 'admin')
+
+ if (adminUsers && adminUsers.length > 0) {
+ const notifications = adminUsers.map(admin => ({
+ user_id: admin.id,
+ company_id: existingHackathon.company_id,
+ type: 'hackathon_updated' as const,
+ title: 'Hackathon Updated - Requires Re-approval',
+ message: `"${existingHackathon.title}" has been edited and requires re-approval`,
+ action_url: `/admin/moderation/hackathons/${hackathonId}`,
+ action_label: 'Review Hackathon',
+ hackathon_id: hackathonId.toString(),
+ metadata: {
+ hackathon_title: existingHackathon.title,
+ hackathon_slug: existingHackathon.slug
+ }
+ }))
+
+ await supabase.from('notifications').insert(notifications)
+ console.log(`📧 Notified ${adminUsers.length} admin(s) about updated hackathon`)
+ }
+
+ // Notify company members about the status change
+ if (existingHackathon.company_id) {
+ await NotificationService.notifyCompanyMembers(existingHackathon.company_id, {
+ type: 'hackathon_status_changed',
+ title: 'Hackathon Requires Re-approval',
+ message: `Your hackathon "${existingHackathon.title}" has been edited and requires re-approval`,
+ company_id: existingHackathon.company_id,
+ action_url: `/dashboard/company/hackathons/${existingHackathon.slug}`,
+ action_label: 'View Hackathon',
+ hackathon_id: hackathonId.toString(),
+ metadata: {
+ hackathon_title: existingHackathon.title,
+ old_status: 'approved',
+ new_status: 'pending'
+ }
+ })
+ }
+ }
+
// Clear cache after updating hackathon
cache.clear()
return hackathon
diff --git a/types/notifications.ts b/types/notifications.ts
index 0d3980c5..c415cf51 100644
--- a/types/notifications.ts
+++ b/types/notifications.ts
@@ -4,9 +4,13 @@ export type NotificationType =
| 'event_approved'
| 'event_rejected'
| 'event_changes_requested'
+ | 'event_updated'
+ | 'event_status_changed'
| 'hackathon_approved'
| 'hackathon_rejected'
| 'hackathon_changes_requested'
+ | 'hackathon_updated'
+ | 'hackathon_status_changed'
| 'new_event_registration'
| 'new_hackathon_registration'
| 'team_member_invited'