Skip to content

Commit 24ff47c

Browse files
authored
Merge pull request #334 from codeunia-dev/feat/companydashboard
Feat: Company Dashboard Notifications & Layout Enhancements
2 parents cbe17be + 85cd491 commit 24ff47c

File tree

15 files changed

+401
-44
lines changed

15 files changed

+401
-44
lines changed

app/api/admin/hackathons/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export async function PUT(request: NextRequest) {
117117
return NextResponse.json({ error: 'Hackathon data is required' }, { status: 400 })
118118
}
119119

120-
const hackathon = await hackathonsService.updateHackathon(slug, data)
120+
const hackathon = await hackathonsService.updateHackathon(slug, data, user.id)
121121

122122
return NextResponse.json(hackathon)
123123
} catch (error) {

app/api/admin/moderation/events/[id]/approve/route.ts

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,20 +40,58 @@ export async function POST(
4040
notes
4141
)
4242

43-
// Send notification email to company
44-
if (approvedEvent.company && approvedEvent.company.email) {
45-
const emailContent = getEventApprovedEmail({
46-
eventTitle: approvedEvent.title,
47-
companyName: approvedEvent.company.name,
48-
eventUrl: `${process.env.NEXT_PUBLIC_SITE_URL || 'https://codeunia.com'}/events/${approvedEvent.slug}`,
49-
publishDate: new Date().toLocaleDateString(),
50-
notes: notes || '',
43+
// Get creator's email from profiles table
44+
let creatorEmail: string | null = null
45+
let creatorName: string | null = null
46+
47+
if (approvedEvent.created_by) {
48+
const { createClient } = await import('@/lib/supabase/server')
49+
const supabase = await createClient()
50+
const { data: creatorProfile } = await supabase
51+
.from('profiles')
52+
.select('email, first_name, last_name')
53+
.eq('id', approvedEvent.created_by)
54+
.single()
55+
56+
if (creatorProfile) {
57+
creatorEmail = creatorProfile.email
58+
creatorName = creatorProfile.first_name
59+
? `${creatorProfile.first_name} ${creatorProfile.last_name || ''}`.trim()
60+
: null
61+
}
62+
}
63+
64+
// Prepare email content
65+
const emailContent = getEventApprovedEmail({
66+
eventTitle: approvedEvent.title,
67+
companyName: approvedEvent.company?.name || 'Your Company',
68+
eventUrl: `${process.env.NEXT_PUBLIC_SITE_URL || 'https://codeunia.com'}/events/${approvedEvent.slug}`,
69+
publishDate: new Date().toLocaleDateString(),
70+
notes: notes || '',
71+
creatorName: creatorName || undefined,
72+
})
73+
74+
// Send notification email to event creator (primary)
75+
if (creatorEmail) {
76+
console.log(`📧 Sending event approval email to creator: ${creatorEmail}`)
77+
await sendEmail({
78+
to: creatorEmail,
79+
subject: emailContent.subject,
80+
html: emailContent.html,
81+
}).catch(error => {
82+
console.error('❌ Failed to send approval email to creator:', error)
5183
})
84+
}
5285

86+
// Also send to company email if different from creator
87+
if (approvedEvent.company?.email && approvedEvent.company.email !== creatorEmail) {
88+
console.log(`📧 Sending event approval email to company: ${approvedEvent.company.email}`)
5389
await sendEmail({
5490
to: approvedEvent.company.email,
5591
subject: emailContent.subject,
5692
html: emailContent.html,
93+
}).catch(error => {
94+
console.error('❌ Failed to send approval email to company:', error)
5795
})
5896
}
5997

@@ -90,12 +128,19 @@ function getEventApprovedEmail(params: {
90128
eventUrl: string
91129
publishDate: string
92130
notes: string
131+
creatorName?: string
93132
}) {
133+
const greeting = params.creatorName ? `Hi ${params.creatorName},` : 'Hello,'
134+
94135
const content = `
95136
<h2 style="margin: 0 0 20px 0; color: #111827; font-size: 20px;">
96137
🎉 Your Event is Live!
97138
</h2>
98139
140+
<p style="margin: 0 0 15px 0; color: #374151; font-size: 16px; line-height: 1.5;">
141+
${greeting}
142+
</p>
143+
99144
<p style="margin: 0 0 15px 0; color: #374151; font-size: 16px; line-height: 1.5;">
100145
Great news! Your event has been approved and is now live on CodeUnia.
101146
</p>

app/api/admin/moderation/events/[id]/reject/route.ts

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,20 +50,58 @@ export async function POST(
5050
reason
5151
)
5252

53-
// Send notification email to company
54-
if (rejectedEvent.company && rejectedEvent.company.email) {
55-
const emailContent = getEventRejectedEmail({
56-
eventTitle: rejectedEvent.title,
57-
companyName: rejectedEvent.company.name,
58-
rejectionReason: reason,
59-
editUrl: `${process.env.NEXT_PUBLIC_SITE_URL || 'https://codeunia.com'}/dashboard/company/events/${rejectedEvent.slug}/edit`,
60-
guidelines: `${process.env.NEXT_PUBLIC_SITE_URL || 'https://codeunia.com'}/guidelines`,
53+
// Get creator's email from profiles table
54+
let creatorEmail: string | null = null
55+
let creatorName: string | null = null
56+
57+
if (rejectedEvent.created_by) {
58+
const { createClient } = await import('@/lib/supabase/server')
59+
const supabase = await createClient()
60+
const { data: creatorProfile } = await supabase
61+
.from('profiles')
62+
.select('email, first_name, last_name')
63+
.eq('id', rejectedEvent.created_by)
64+
.single()
65+
66+
if (creatorProfile) {
67+
creatorEmail = creatorProfile.email
68+
creatorName = creatorProfile.first_name
69+
? `${creatorProfile.first_name} ${creatorProfile.last_name || ''}`.trim()
70+
: null
71+
}
72+
}
73+
74+
// Prepare email content
75+
const emailContent = getEventRejectedEmail({
76+
eventTitle: rejectedEvent.title,
77+
companyName: rejectedEvent.company?.name || 'Your Company',
78+
rejectionReason: reason,
79+
editUrl: `${process.env.NEXT_PUBLIC_SITE_URL || 'https://codeunia.com'}/dashboard/company/${rejectedEvent.company?.slug}/events`,
80+
guidelines: `${process.env.NEXT_PUBLIC_SITE_URL || 'https://codeunia.com'}/guidelines`,
81+
creatorName: creatorName || undefined,
82+
})
83+
84+
// Send notification email to event creator (primary)
85+
if (creatorEmail) {
86+
console.log(`📧 Sending event rejection email to creator: ${creatorEmail}`)
87+
await sendEmail({
88+
to: creatorEmail,
89+
subject: emailContent.subject,
90+
html: emailContent.html,
91+
}).catch(error => {
92+
console.error('❌ Failed to send rejection email to creator:', error)
6193
})
94+
}
6295

96+
// Also send to company email if different from creator
97+
if (rejectedEvent.company?.email && rejectedEvent.company.email !== creatorEmail) {
98+
console.log(`📧 Sending event rejection email to company: ${rejectedEvent.company.email}`)
6399
await sendEmail({
64100
to: rejectedEvent.company.email,
65101
subject: emailContent.subject,
66102
html: emailContent.html,
103+
}).catch(error => {
104+
console.error('❌ Failed to send rejection email to company:', error)
67105
})
68106
}
69107

@@ -100,14 +138,21 @@ function getEventRejectedEmail(params: {
100138
rejectionReason: string
101139
editUrl: string
102140
guidelines: string
141+
creatorName?: string
103142
}) {
143+
const greeting = params.creatorName ? `Hi ${params.creatorName},` : 'Hello,'
144+
104145
const content = `
105146
<h2 style="margin: 0 0 20px 0; color: #111827; font-size: 20px;">
106147
Event Review Update
107148
</h2>
108149
109150
<p style="margin: 0 0 15px 0; color: #374151; font-size: 16px; line-height: 1.5;">
110-
Thank you for submitting your event to CodeUnia. After review, we're unable to approve your event at this time.
151+
${greeting}
152+
</p>
153+
154+
<p style="margin: 0 0 15px 0; color: #374151; font-size: 16px; line-height: 1.5;">
155+
Thank you for submitting your event to Codeunia. After review, we're unable to approve your event at this time.
111156
</p>
112157
113158
<div style="background-color: #fef2f2; border-left: 4px solid #ef4444; padding: 15px; margin: 20px 0; border-radius: 4px;">
@@ -158,7 +203,7 @@ function getEventRejectedEmail(params: {
158203
<head>
159204
<meta charset="utf-8">
160205
<meta name="viewport" content="width=device-width, initial-scale=1.0">
161-
<title>Event Review Update - CodeUnia</title>
206+
<title>Event Review Update - Codeunia</title>
162207
</head>
163208
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
164209
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f5f5f5; padding: 20px;">
@@ -167,7 +212,7 @@ function getEventRejectedEmail(params: {
167212
<table width="600" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
168213
<tr>
169214
<td style="background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); padding: 30px; text-align: center;">
170-
<h1 style="margin: 0; color: #ffffff; font-size: 24px; font-weight: bold;">CodeUnia</h1>
215+
<h1 style="margin: 0; color: #ffffff; font-size: 24px; font-weight: bold;">Codeunia</h1>
171216
</td>
172217
</tr>
173218
<tr>
@@ -181,7 +226,7 @@ function getEventRejectedEmail(params: {
181226
Need help? Visit our <a href="${process.env.NEXT_PUBLIC_SITE_URL || 'https://codeunia.com'}/protected/help" style="color: #3b82f6; text-decoration: none;">Help Center</a>
182227
</p>
183228
<p style="margin: 0; color: #9ca3af; font-size: 12px;">
184-
© ${new Date().getFullYear()} CodeUnia. All rights reserved.
229+
© ${new Date().getFullYear()} Codeunia. All rights reserved.
185230
</p>
186231
</td>
187232
</tr>

app/api/companies/[slug]/members/[userId]/route.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { companyMemberService } from '@/lib/services/company-member-service'
55
import { CompanyError } from '@/types/company'
66
import { UnifiedCache } from '@/lib/unified-cache-system'
77
import { z } from 'zod'
8-
import { getRoleChangeEmail, sendCompanyEmail } from '@/lib/email/company-emails'
8+
import { getRoleChangeEmail, getMemberRemovedEmail, sendCompanyEmail } from '@/lib/email/company-emails'
99

1010
// Force Node.js runtime for API routes
1111
export const runtime = 'nodejs'
@@ -263,9 +263,50 @@ export async function DELETE(
263263
)
264264
}
265265

266+
// Get member's profile information for email before removing
267+
const { data: memberProfile } = await supabase
268+
.from('profiles')
269+
.select('email, first_name, last_name')
270+
.eq('id', userId)
271+
.single()
272+
273+
// Get requesting user's name for email
274+
const { data: requestingUserProfile } = await supabase
275+
.from('profiles')
276+
.select('first_name, last_name')
277+
.eq('id', user.id)
278+
.single()
279+
280+
const removedByName = requestingUserProfile?.first_name
281+
? `${requestingUserProfile.first_name} ${requestingUserProfile.last_name || ''}`.trim()
282+
: 'a team administrator'
283+
266284
// Remove member
267285
await companyMemberService.removeMember(targetMember.id)
268286

287+
// Send removal notification email
288+
if (memberProfile?.email) {
289+
const memberName = memberProfile.first_name || memberProfile.email.split('@')[0]
290+
291+
const emailContent = getMemberRemovedEmail({
292+
memberName,
293+
companyName: company.name,
294+
removedBy: removedByName,
295+
role: targetMember.role,
296+
})
297+
298+
// Send email asynchronously (don't wait for it)
299+
console.log(`📧 Sending member removal email to ${memberProfile.email}`)
300+
sendCompanyEmail({
301+
to: memberProfile.email,
302+
subject: emailContent.subject,
303+
html: emailContent.html,
304+
}).catch(error => {
305+
console.error('❌ Failed to send member removal email:', error)
306+
// Don't fail the request if email fails
307+
})
308+
}
309+
269310
// Invalidate cache
270311
await UnifiedCache.purgeByTags(['content', 'api'])
271312

app/api/hackathons/[id]/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export async function PUT(request: NextRequest, { params }: RouteContext) {
9696
)
9797
}
9898

99-
const hackathon = await hackathonsService.updateHackathon(id, hackathonData)
99+
const hackathon = await hackathonsService.updateHackathon(id, hackathonData, user.id)
100100

101101
return NextResponse.json({ hackathon })
102102
} catch (error) {

app/companies/[slug]/events/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ export default function CompanyEventsPage() {
162162

163163
{/* Company Header */}
164164
{company && (
165-
<section className="py-12 bg-gradient-to-b from-muted/30 to-background border-b border-primary/10">
165+
<section className="pt-24 pb-12 bg-gradient-to-b from-muted/30 to-background border-b border-primary/10">
166166
<div className="container px-4 mx-auto">
167167
<motion.div
168168
initial={{ opacity: 0, y: 20 }}

app/companies/[slug]/hackathons/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ export default function CompanyHackathonsPage() {
144144

145145
{/* Company Header */}
146146
{company && (
147-
<section className="py-12 bg-gradient-to-b from-muted/30 to-background border-b border-primary/10">
147+
<section className="pt-24 pb-12 bg-gradient-to-b from-muted/30 to-background border-b border-primary/10">
148148
<div className="container px-4 mx-auto">
149149
<motion.div
150150
initial={{ opacity: 0, y: 20 }}

app/companies/[slug]/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export default function CompanyProfilePage() {
8888
<div className="flex flex-col min-h-screen bg-gradient-to-br from-background via-background to-muted/10">
8989
<Header />
9090

91-
<main className="flex-1 py-12">
91+
<main className="flex-1 pt-24 pb-12">
9292
<div className="container px-4 mx-auto">
9393
<motion.div
9494
initial={{ opacity: 0, y: 20 }}

components/companies/CompanyProfile.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,15 @@ export function CompanyProfile({ company, isOwner = false, className }: CompanyP
2929
<Card className="overflow-hidden">
3030
{/* Banner */}
3131
<div
32-
className="h-48 bg-gradient-to-r from-primary/20 via-primary/10 to-primary/20"
32+
className="h-56 sm:h-64 bg-gradient-to-r from-primary/20 via-primary/10 to-primary/20"
3333
style={company.banner_url ? {
3434
backgroundImage: `url(${company.banner_url})`,
3535
backgroundSize: 'cover',
3636
backgroundPosition: 'center',
3737
} : undefined}
3838
/>
3939

40-
<CardHeader className="relative -mt-16 pb-4">
40+
<CardHeader className="relative -mt-20 pb-4">
4141
<div className="flex flex-col sm:flex-row items-start sm:items-end gap-4">
4242
{/* Logo */}
4343
<Avatar className="h-32 w-32 border-4 border-background shadow-xl">

components/notifications/notification-utils.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,13 @@ export function getNotificationIcon(type: NotificationType): LucideIcon {
1818
event_approved: CheckCircle2,
1919
event_rejected: XCircle,
2020
event_changes_requested: AlertCircle,
21+
event_updated: AlertCircle,
22+
event_status_changed: AlertCircle,
2123
hackathon_approved: CheckCircle2,
2224
hackathon_rejected: XCircle,
2325
hackathon_changes_requested: AlertCircle,
26+
hackathon_updated: AlertCircle,
27+
hackathon_status_changed: AlertCircle,
2428
new_event_registration: Calendar,
2529
new_hackathon_registration: Calendar,
2630
team_member_invited: UserPlus,
@@ -40,9 +44,13 @@ export function getNotificationColor(type: NotificationType): string {
4044
event_approved: 'text-green-500',
4145
event_rejected: 'text-red-500',
4246
event_changes_requested: 'text-yellow-500',
47+
event_updated: 'text-orange-500',
48+
event_status_changed: 'text-yellow-500',
4349
hackathon_approved: 'text-green-500',
4450
hackathon_rejected: 'text-red-500',
4551
hackathon_changes_requested: 'text-yellow-500',
52+
hackathon_updated: 'text-orange-500',
53+
hackathon_status_changed: 'text-yellow-500',
4654
new_event_registration: 'text-blue-500',
4755
new_hackathon_registration: 'text-blue-500',
4856
team_member_invited: 'text-blue-500',

0 commit comments

Comments
 (0)