Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/api/admin/moderation/events/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const GET = withPlatformAdmin(async (request: NextRequest) => {
const limit = parseInt(searchParams.get('limit') || '20')
const offset = parseInt(searchParams.get('offset') || '0')

// Get pending events from moderation service
// Get pending and deleted events from moderation service
const { events, total } = await moderationService.getPendingEvents({
limit,
offset,
Expand Down
178 changes: 98 additions & 80 deletions app/api/admin/moderation/hackathons/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,96 +19,114 @@ export async function POST(request: NextRequest, context: RouteContext) {
return withPlatformAdmin(async () => {
try {
const { id } = await context.params
const body = await request.json()
const { action, reason } = body
const body = await request.json()
const { action, reason } = body

if (!action || !['approve', 'reject'].includes(action)) {
return NextResponse.json(
{ success: false, error: 'Invalid action' },
{ status: 400 }
)
}
if (!action || !['approve', 'reject', 'delete'].includes(action)) {
return NextResponse.json(
{ success: false, error: 'Invalid action' },
{ status: 400 }
)
}

if (action === 'reject' && !reason) {
return NextResponse.json(
{ success: false, error: 'Rejection reason is required' },
{ status: 400 }
)
}
if (action === 'reject' && !reason) {
return NextResponse.json(
{ success: false, error: 'Rejection reason is required' },
{ status: 400 }
)
}

const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()

if (!user) {
return NextResponse.json(
{ success: false, error: 'Unauthorized' },
{ status: 401 }
)
}
if (!user) {
return NextResponse.json(
{ success: false, error: 'Unauthorized' },
{ status: 401 }
)
}

// Get the hackathon
const { data: hackathon, error: fetchError } = await supabase
.from('hackathons')
.select('*')
.eq('id', id)
.single()
// Get the hackathon
const { data: hackathon, error: fetchError } = await supabase
.from('hackathons')
.select('*')
.eq('id', id)
.single()

if (fetchError || !hackathon) {
return NextResponse.json(
{ success: false, error: 'Hackathon not found' },
{ status: 404 }
)
}
if (fetchError || !hackathon) {
return NextResponse.json(
{ success: false, error: 'Hackathon not found' },
{ status: 404 }
)
}

// Update hackathon based on action
const updateData: {
updated_at: string
approval_status?: string
approved_by?: string
approved_at?: string
status?: string
rejection_reason?: string | null
} = {
updated_at: new Date().toISOString(),
}
// Update hackathon based on action
const updateData: {
updated_at: string
approval_status?: string
approved_by?: string
approved_at?: string
status?: string
rejection_reason?: string | null
} = {
updated_at: new Date().toISOString(),
}

if (action === 'approve') {
updateData.approval_status = 'approved'
updateData.approved_by = user.id
updateData.approved_at = new Date().toISOString()
updateData.status = 'live'
updateData.rejection_reason = null
} else if (action === 'reject') {
updateData.approval_status = 'rejected'
updateData.rejection_reason = reason
updateData.status = 'draft'
}
if (action === 'approve') {
updateData.approval_status = 'approved'
updateData.approved_by = user.id
updateData.approved_at = new Date().toISOString()
updateData.status = 'live'
updateData.rejection_reason = null
} else if (action === 'reject') {
updateData.approval_status = 'rejected'
updateData.rejection_reason = reason
updateData.status = 'draft'
} else if (action === 'delete') {
// Permanently delete the hackathon
const { error: deleteError } = await supabase
.from('hackathons')
.delete()
.eq('id', id)

const { error: updateError } = await supabase
.from('hackathons')
.update(updateData)
.eq('id', id)
if (deleteError) {
throw deleteError
}

if (updateError) {
throw updateError
}
// Invalidate caches
await UnifiedCache.purgeByTags(['content', 'api'])

return NextResponse.json({
success: true,
message: 'Hackathon permanently deleted',
})
}

const { error: updateError } = await supabase
.from('hackathons')
.update(updateData)
.eq('id', id)

// Invalidate caches
await UnifiedCache.purgeByTags(['content', 'api'])

return NextResponse.json({
success: true,
message: `Hackathon ${action}d successfully`,
})
} catch (error) {
console.error('Error in hackathon moderation action:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to execute action',
},
{ status: 500 }
)
}
if (updateError) {
throw updateError
}

// Invalidate caches
await UnifiedCache.purgeByTags(['content', 'api'])

return NextResponse.json({
success: true,
message: `Hackathon ${action}d successfully`,
})
} catch (error) {
console.error('Error in hackathon moderation action:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to execute action',
},
{ status: 500 }
)
}
})(request)
}
2 changes: 1 addition & 1 deletion app/api/admin/moderation/hackathons/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const GET = withPlatformAdmin(async (request: NextRequest) => {
*,
company:companies(*)
`, { count: 'exact' })
.eq('approval_status', 'pending')
.in('approval_status', ['pending', 'deleted'])
.order('created_at', { ascending: false })
.range(offset, offset + limit - 1)

Expand Down
23 changes: 15 additions & 8 deletions app/api/events/[slug]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export async function GET(
// Try to get from cache first
const cacheKey = `event:${slug}`;
const cached = await UnifiedCache.get(cacheKey);

if (cached) {
return UnifiedCache.createResponse(cached, 'API_STANDARD');
}
Expand All @@ -40,7 +40,7 @@ export async function GET(

} catch (error) {
console.error('Error in GET /api/events/[slug]:', error);

if (error instanceof EventError) {
return NextResponse.json(
{ error: error.message, code: error.code },
Expand Down Expand Up @@ -112,7 +112,7 @@ export async function PUT(

} catch (error) {
console.error('Error in PUT /api/events/[slug]:', error);

if (error instanceof EventError) {
return NextResponse.json(
{ error: error.message, code: error.code },
Expand Down Expand Up @@ -167,19 +167,26 @@ export async function DELETE(
}

// Delete event using service
await eventsService.deleteEvent(existingEvent.id, user.id);
const result = await eventsService.deleteEvent(existingEvent.id, user.id)

// Invalidate caches
await UnifiedCache.purgeByTags(['content', 'api']);
await UnifiedCache.purgeByTags(['content', 'api'])

return NextResponse.json(
{ message: 'Event deleted successfully' },
{
success: true,
message: result.soft_delete
? 'Event marked for deletion. Admin approval required.'
: 'Event deleted successfully',
soft_delete: result.soft_delete
},
{ status: 200 }
);
)
;

} catch (error) {
console.error('Error in DELETE /api/events/[slug]:', error);

if (error instanceof EventError) {
return NextResponse.json(
{ error: error.message, code: error.code },
Expand Down
62 changes: 60 additions & 2 deletions app/api/hackathons/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,13 +182,71 @@ export async function DELETE(_request: NextRequest, { params }: RouteContext) {
)
}

console.log('🗑️ Attempting to delete hackathon...')
// Check if hackathon is approved (live) - use soft delete
if (existingHackathon.approval_status === 'approved') {
console.log('🔄 Hackathon is approved - marking for deletion (soft delete)')

// Mark as deleted instead of hard deleting
const { error: updateError } = await supabase
.from('hackathons')
.update({
approval_status: 'deleted',
updated_at: new Date().toISOString()
})
.eq('id', existingHackathon.id)

if (updateError) {
console.error('❌ Error marking hackathon for deletion:', updateError)
throw new Error('Failed to mark hackathon for deletion')
}

console.log('✅ Hackathon marked for deletion - requires admin approval')

// Notify admins about the deletion request
const hackathonId = existingHackathon.id
if (hackathonId) {
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_deleted' as const,
title: 'Hackathon Deletion Request',
message: `"${existingHackathon.title}" has been marked for deletion and requires approval`,
action_url: `/admin/moderation/hackathons/${hackathonId}`,
action_label: 'Review Deletion',
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 deletion request`)
}
}

return NextResponse.json({
success: true,
message: 'Hackathon marked for deletion. Admin approval required.',
soft_delete: true
})
}

// For draft/pending hackathons, allow hard delete
console.log('🗑️ Hackathon is draft/pending - performing hard delete')
await hackathonsService.deleteHackathon(id)
console.log('✅ Hackathon deleted successfully')

return NextResponse.json({
success: true,
message: 'Hackathon deleted successfully'
message: 'Hackathon deleted successfully',
soft_delete: false
})
} catch (error) {
console.error('❌ Error in DELETE /api/hackathons/[id]:', error)
Expand Down
22 changes: 16 additions & 6 deletions app/dashboard/company/[slug]/events/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,16 @@ export default function CompanyEventsPage() {
}
}

// Filter out deleted items for stats (summary cards should only show active items)
const activeEvents = events.filter(e => e.approval_status !== 'deleted')

const stats = {
total: events.length,
approved: events.filter(e => e.approval_status === 'approved').length,
pending: events.filter(e => e.approval_status === 'pending').length,
draft: events.filter(e => e.status === 'draft').length,
totalViews: events.reduce((sum, e) => sum + (e.views || 0), 0),
totalRegistrations: events.reduce((sum, e) => sum + (e.registered || 0), 0),
total: activeEvents.length,
approved: activeEvents.filter(e => e.approval_status === 'approved').length,
pending: activeEvents.filter(e => e.approval_status === 'pending').length,
draft: activeEvents.filter(e => e.status === 'draft').length,
totalViews: activeEvents.reduce((sum, e) => sum + (e.views || 0), 0),
totalRegistrations: activeEvents.reduce((sum, e) => sum + (e.registered || 0), 0),
}

const getApprovalBadge = (status: string) => {
Expand Down Expand Up @@ -138,6 +141,13 @@ export default function CompanyEventsPage() {
Changes Requested
</Badge>
)
case 'deleted':
return (
<Badge className="bg-gray-500/10 text-gray-600 border-gray-500/20 pointer-events-none">
<Trash2 className="h-3 w-3 mr-1" />
Deleted
</Badge>
)
default:
return <Badge variant="outline" className="pointer-events-none">{status}</Badge>
}
Expand Down
Loading
Loading