diff --git a/app/api/admin/moderation/events/route.ts b/app/api/admin/moderation/events/route.ts
index 08e7268c..bdc9e1fe 100644
--- a/app/api/admin/moderation/events/route.ts
+++ b/app/api/admin/moderation/events/route.ts
@@ -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,
diff --git a/app/api/admin/moderation/hackathons/[id]/route.ts b/app/api/admin/moderation/hackathons/[id]/route.ts
index 61e7e2e2..db68cb88 100644
--- a/app/api/admin/moderation/hackathons/[id]/route.ts
+++ b/app/api/admin/moderation/hackathons/[id]/route.ts
@@ -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)
}
diff --git a/app/api/admin/moderation/hackathons/route.ts b/app/api/admin/moderation/hackathons/route.ts
index 895ba1ba..373d402e 100644
--- a/app/api/admin/moderation/hackathons/route.ts
+++ b/app/api/admin/moderation/hackathons/route.ts
@@ -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)
diff --git a/app/api/events/[slug]/route.ts b/app/api/events/[slug]/route.ts
index 6311202e..231f0a80 100644
--- a/app/api/events/[slug]/route.ts
+++ b/app/api/events/[slug]/route.ts
@@ -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');
}
@@ -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 },
@@ -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 },
@@ -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 },
diff --git a/app/api/hackathons/[id]/route.ts b/app/api/hackathons/[id]/route.ts
index 2081a2c5..30b18325 100644
--- a/app/api/hackathons/[id]/route.ts
+++ b/app/api/hackathons/[id]/route.ts
@@ -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)
diff --git a/app/dashboard/company/[slug]/events/page.tsx b/app/dashboard/company/[slug]/events/page.tsx
index 8240e031..61d788eb 100644
--- a/app/dashboard/company/[slug]/events/page.tsx
+++ b/app/dashboard/company/[slug]/events/page.tsx
@@ -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) => {
@@ -138,6 +141,13 @@ export default function CompanyEventsPage() {
Changes Requested
)
+ case 'deleted':
+ return (
+
+
+ Deleted
+
+ )
default:
return {status}
}
diff --git a/app/dashboard/company/[slug]/hackathons/page.tsx b/app/dashboard/company/[slug]/hackathons/page.tsx
index 1b985cc0..b021cb39 100644
--- a/app/dashboard/company/[slug]/hackathons/page.tsx
+++ b/app/dashboard/company/[slug]/hackathons/page.tsx
@@ -29,7 +29,7 @@ interface Hackathon {
excerpt: string
category: string
status: string
- approval_status: 'draft' | 'pending' | 'approved' | 'rejected' | 'changes_requested'
+ approval_status: 'draft' | 'pending' | 'approved' | 'rejected' | 'changes_requested' | 'deleted'
date: string
time: string
duration: string
@@ -57,7 +57,7 @@ export default function CompanyHackathonsPage() {
setLoading(true)
// Fetch all hackathons (not just approved) for company members
const response = await fetch(`/api/companies/${currentCompany.slug}/hackathons?status=all&limit=100`)
-
+
if (!response.ok) {
throw new Error('Failed to fetch hackathons')
}
@@ -101,7 +101,7 @@ export default function CompanyHackathonsPage() {
toast.success('Hackathon deleted successfully')
setDeleteDialogOpen(false)
setHackathonToDelete(null)
-
+
// Refresh the list
fetchHackathons()
} catch (error) {
@@ -125,13 +125,16 @@ export default function CompanyHackathonsPage() {
hackathon.category?.toLowerCase().includes(searchTerm.toLowerCase())
)
+ // Filter out deleted items for stats (summary cards should only show active items)
+ const activeHackathons = hackathons.filter(h => h.approval_status !== 'deleted')
+
const stats = {
- total: hackathons.length,
- approved: hackathons.filter(h => h.approval_status === 'approved').length,
- pending: hackathons.filter(h => h.approval_status === 'pending').length,
- draft: hackathons.filter(h => h.status === 'draft').length,
- totalViews: hackathons.reduce((sum, h) => sum + (h.views || 0), 0),
- totalRegistrations: hackathons.reduce((sum, h) => sum + (h.registered || 0), 0),
+ total: activeHackathons.length,
+ approved: activeHackathons.filter(h => h.approval_status === 'approved').length,
+ pending: activeHackathons.filter(h => h.approval_status === 'pending').length,
+ draft: activeHackathons.filter(h => h.status === 'draft').length,
+ totalViews: activeHackathons.reduce((sum, h) => sum + (h.views || 0), 0),
+ totalRegistrations: activeHackathons.reduce((sum, h) => sum + (h.registered || 0), 0),
}
const getApprovalBadge = (status: string) => {
@@ -164,6 +167,13 @@ export default function CompanyHackathonsPage() {
Changes Requested
)
+ case 'deleted':
+ return (
+
+
+ Deleted
+
+ )
default:
return {status}
}
@@ -185,7 +195,7 @@ export default function CompanyHackathonsPage() {
return {hackathon.approval_status}
}
}
-
+
// If approved, show event status
switch (hackathon.status) {
case 'live':
@@ -393,9 +403,9 @@ export default function CompanyHackathonsPage() {
)}
-
-
-
+ {hackathon.approval_status === 'deleted' ? (
+ // Show delete button for soft-deleted items
+
+ ) : (
+ // Show approve/reject buttons for pending items
+ <>
+
+
+ >
+ )}
@@ -299,7 +322,9 @@ export function HackathonModerationQueue() {
{pendingAction?.type === "approve"
? "This hackathon will be published and visible to all users."
- : "This hackathon will be rejected and the company will be notified."}
+ : pendingAction?.type === "delete"
+ ? "This will permanently delete the hackathon. This action cannot be undone."
+ : "This hackathon will be rejected and the company will be notified."}
{pendingAction?.type === "reject" && (
@@ -325,7 +350,7 @@ export function HackathonModerationQueue() {
: "bg-red-600 hover:bg-red-700"
}
>
- {pendingAction?.type === "approve" ? "Approve" : "Reject"}
+ {pendingAction?.type === "approve" ? "Approve" : pendingAction?.type === "delete" ? "Permanently Delete" : "Reject"}
diff --git a/lib/services/events.ts b/lib/services/events.ts
index be166f0f..359f9f4b 100644
--- a/lib/services/events.ts
+++ b/lib/services/events.ts
@@ -550,18 +550,73 @@ class EventsService {
/**
* Delete an event
* @param id Event ID
- * @param _userId ID of the user deleting the event
+ * @param _userId ID of the user deleting the event (unused but kept for API consistency)
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
- async deleteEvent(id: number, _userId: string): Promise {
+ async deleteEvent(id: number, _userId: string): Promise<{ soft_delete: boolean }> {
const supabase = await createClient()
- // Get existing event to verify ownership
+ // Get existing event to verify ownership and check approval status
const existingEvent = await this.getEventById(id)
if (!existingEvent) {
throw new EventError('Event not found', EventErrorCodes.NOT_FOUND, 404)
}
+ // Check if event is approved (live) - use soft delete
+ if (existingEvent.approval_status === 'approved') {
+ console.log('🔄 Event is approved - marking for deletion (soft delete)')
+
+ // Mark as deleted instead of hard deleting
+ const { error: updateError } = await supabase
+ .from('events')
+ .update({
+ approval_status: 'deleted',
+ updated_at: new Date().toISOString()
+ })
+ .eq('id', id)
+
+ if (updateError) {
+ console.error('❌ Error marking event for deletion:', updateError)
+ throw new EventError('Failed to mark event for deletion', EventErrorCodes.NOT_FOUND, 500)
+ }
+
+ console.log('✅ Event marked for deletion - requires admin approval')
+
+ // Notify admins about the deletion request
+ const eventId = existingEvent.id
+ if (eventId) {
+ 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: existingEvent.company_id,
+ type: 'event_deleted' as const,
+ title: 'Event Deletion Request',
+ message: `"${existingEvent.title}" has been marked for deletion and requires approval`,
+ action_url: `/admin/moderation/events/${eventId}`,
+ action_label: 'Review Deletion',
+ event_id: eventId.toString(),
+ metadata: {
+ event_title: existingEvent.title,
+ event_slug: existingEvent.slug
+ }
+ }))
+
+ await supabase.from('notifications').insert(notifications)
+ console.log(`📧 Notified ${adminUsers.length} admin(s) about deletion request`)
+ }
+ }
+
+ clearCache()
+ return { soft_delete: true }
+ }
+
+ // For draft/pending events, allow hard delete
+ console.log('🗑️ Event is draft/pending - performing hard delete')
const { error } = await supabase
.from('events')
.delete()
@@ -573,6 +628,7 @@ class EventsService {
}
clearCache()
+ return { soft_delete: false }
}
/**
diff --git a/lib/services/moderation-service.ts b/lib/services/moderation-service.ts
index a1bcb120..447fc360 100644
--- a/lib/services/moderation-service.ts
+++ b/lib/services/moderation-service.ts
@@ -81,7 +81,7 @@ class ModerationService {
`,
{ count: 'exact' }
)
- .eq('approval_status', 'pending')
+ .in('approval_status', ['pending', 'deleted'])
.order('created_at', { ascending: true })
.range(offset, offset + limit - 1)
diff --git a/types/events.ts b/types/events.ts
index 9f4d7197..80e568cc 100644
--- a/types/events.ts
+++ b/types/events.ts
@@ -55,7 +55,7 @@ export interface Event {
company_id?: string
company?: Company
created_by?: string
- approval_status: 'pending' | 'approved' | 'rejected' | 'changes_requested'
+ approval_status: 'draft' | 'pending' | 'approved' | 'rejected' | 'changes_requested' | 'deleted'
approved_by?: string
approved_at?: string
rejection_reason?: string
diff --git a/types/hackathons.ts b/types/hackathons.ts
index fe9f2814..5b22a253 100644
--- a/types/hackathons.ts
+++ b/types/hackathons.ts
@@ -54,7 +54,7 @@ export interface Hackathon {
company_id?: string
company?: Company
created_by?: string
- approval_status: 'pending' | 'approved' | 'rejected' | 'changes_requested'
+ approval_status: 'draft' | 'pending' | 'approved' | 'rejected' | 'changes_requested' | 'deleted'
approved_by?: string
approved_at?: string
rejection_reason?: string