Skip to content

Commit 771abc3

Browse files
committed
feat(hackathons): Add comprehensive hackathon management and moderation system
- Add hackathon creation and editing pages in company dashboard - Implement hackathon moderation queue in admin panel with approval/rejection workflow - Create HackathonForm component for creating and editing hackathons - Add API endpoints for hackathon CRUD operations and moderation actions - Implement hackathon submission workflow with status tracking - Add tabbed interface to moderation page for managing both events and hackathons - Enhance hackathon service with submission and moderation logic - Update company hackathons listing page with management capabilities - Add hackathon statistics tracking in moderation dashboard
1 parent 2f871d1 commit 771abc3

File tree

13 files changed

+1922
-57
lines changed

13 files changed

+1922
-57
lines changed

app/admin/moderation/page.tsx

Lines changed: 90 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,51 @@
22

33
import { useState, useEffect } from "react"
44
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
5+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
56
import { ModerationQueue } from "@/components/moderation/ModerationQueue"
6-
import { AlertCircle, CheckCircle, Clock, Filter } from "lucide-react"
7+
import { HackathonModerationQueue } from "@/components/moderation/HackathonModerationQueue"
8+
import { AlertCircle, CheckCircle, Clock, Filter, Calendar, Trophy } from "lucide-react"
79

810
export default function ModerationPage() {
9-
const [stats, setStats] = useState({
11+
const [activeTab, setActiveTab] = useState("events")
12+
const [eventStats, setEventStats] = useState({
13+
pending: 0,
14+
approved: 0,
15+
rejected: 0,
16+
})
17+
const [hackathonStats, setHackathonStats] = useState({
1018
pending: 0,
1119
approved: 0,
1220
rejected: 0,
1321
})
1422
const [loading, setLoading] = useState(true)
1523

1624
useEffect(() => {
17-
// Fetch moderation stats
25+
// Fetch moderation stats for both events and hackathons
1826
const fetchStats = async () => {
1927
try {
20-
const response = await fetch('/api/admin/moderation/events?limit=1000')
21-
if (response.ok) {
22-
const data = await response.json()
28+
// Fetch event stats
29+
const eventsResponse = await fetch('/api/admin/moderation/events?limit=1000')
30+
if (eventsResponse.ok) {
31+
const data = await eventsResponse.json()
32+
if (data.success) {
33+
setEventStats({
34+
pending: data.data.total,
35+
approved: 0,
36+
rejected: 0,
37+
})
38+
}
39+
}
40+
41+
// Fetch hackathon stats
42+
const hackathonsResponse = await fetch('/api/admin/moderation/hackathons?limit=1000')
43+
if (hackathonsResponse.ok) {
44+
const data = await hackathonsResponse.json()
2345
if (data.success) {
24-
setStats({
46+
setHackathonStats({
2547
pending: data.data.total,
26-
approved: 0, // Would need separate endpoint for this
27-
rejected: 0, // Would need separate endpoint for this
48+
approved: 0,
49+
rejected: 0,
2850
})
2951
}
3052
}
@@ -38,17 +60,19 @@ export default function ModerationPage() {
3860
fetchStats()
3961
}, [])
4062

63+
const stats = activeTab === "events" ? eventStats : hackathonStats
64+
4165
return (
4266
<div className="bg-black min-h-screen px-4 py-8 md:px-8 lg:px-16 space-y-8">
4367
{/* Header */}
4468
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between pb-6 border-b border-zinc-800/60 gap-4">
4569
<div>
4670
<h1 className="text-2xl sm:text-3xl md:text-4xl font-extrabold tracking-tight text-white drop-shadow-sm flex items-center gap-3">
4771
<span className="inline-block w-2 h-6 sm:h-8 bg-gradient-to-b from-purple-400 to-blue-400 rounded-full mr-2" />
48-
Event Moderation Queue
72+
Content Moderation
4973
</h1>
5074
<p className="text-zinc-400 mt-1 font-medium text-sm sm:text-base">
51-
Review and approve events submitted by companies
75+
Review and approve events and hackathons submitted by companies
5276
</p>
5377
</div>
5478
</div>
@@ -113,25 +137,61 @@ export default function ModerationPage() {
113137
</Card>
114138
</div>
115139

116-
{/* Moderation Queue */}
117-
<Card className="border-0 shadow-2xl rounded-2xl bg-gradient-to-br from-zinc-100/80 to-zinc-200/60 dark:from-zinc-900/60 dark:to-zinc-800/40">
118-
<CardHeader>
119-
<div className="flex items-center justify-between">
120-
<div>
121-
<CardTitle className="text-lg font-bold text-zinc-900 dark:text-white flex items-center gap-2">
122-
<Filter className="h-5 w-5 text-purple-400" />
123-
Pending Events
124-
</CardTitle>
125-
<CardDescription className="text-zinc-500 dark:text-zinc-300 font-medium text-sm">
126-
Review events and take action
127-
</CardDescription>
128-
</div>
129-
</div>
130-
</CardHeader>
131-
<CardContent>
132-
<ModerationQueue />
133-
</CardContent>
134-
</Card>
140+
{/* Moderation Queue with Tabs */}
141+
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
142+
<TabsList className="grid w-full max-w-md grid-cols-2 mb-6">
143+
<TabsTrigger value="events" className="flex items-center gap-2">
144+
<Calendar className="h-4 w-4" />
145+
Events
146+
</TabsTrigger>
147+
<TabsTrigger value="hackathons" className="flex items-center gap-2">
148+
<Trophy className="h-4 w-4" />
149+
Hackathons
150+
</TabsTrigger>
151+
</TabsList>
152+
153+
<TabsContent value="events">
154+
<Card className="border-0 shadow-2xl rounded-2xl bg-gradient-to-br from-zinc-100/80 to-zinc-200/60 dark:from-zinc-900/60 dark:to-zinc-800/40">
155+
<CardHeader>
156+
<div className="flex items-center justify-between">
157+
<div>
158+
<CardTitle className="text-lg font-bold text-zinc-900 dark:text-white flex items-center gap-2">
159+
<Filter className="h-5 w-5 text-purple-400" />
160+
Pending Events
161+
</CardTitle>
162+
<CardDescription className="text-zinc-500 dark:text-zinc-300 font-medium text-sm">
163+
Review events and take action
164+
</CardDescription>
165+
</div>
166+
</div>
167+
</CardHeader>
168+
<CardContent>
169+
<ModerationQueue />
170+
</CardContent>
171+
</Card>
172+
</TabsContent>
173+
174+
<TabsContent value="hackathons">
175+
<Card className="border-0 shadow-2xl rounded-2xl bg-gradient-to-br from-zinc-100/80 to-zinc-200/60 dark:from-zinc-900/60 dark:to-zinc-800/40">
176+
<CardHeader>
177+
<div className="flex items-center justify-between">
178+
<div>
179+
<CardTitle className="text-lg font-bold text-zinc-900 dark:text-white flex items-center gap-2">
180+
<Filter className="h-5 w-5 text-purple-400" />
181+
Pending Hackathons
182+
</CardTitle>
183+
<CardDescription className="text-zinc-500 dark:text-zinc-300 font-medium text-sm">
184+
Review hackathons and take action
185+
</CardDescription>
186+
</div>
187+
</div>
188+
</CardHeader>
189+
<CardContent>
190+
<HackathonModerationQueue />
191+
</CardContent>
192+
</Card>
193+
</TabsContent>
194+
</Tabs>
135195
</div>
136196
)
137197
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// API route for hackathon moderation actions
2+
import { NextRequest, NextResponse } from 'next/server'
3+
import { withPlatformAdmin } from '@/lib/services/authorization-service'
4+
import { createClient } from '@/lib/supabase/server'
5+
import { UnifiedCache } from '@/lib/unified-cache-system'
6+
7+
interface RouteContext {
8+
params: Promise<{
9+
id: string
10+
}>
11+
}
12+
13+
/**
14+
* POST /api/admin/moderation/hackathons/[id]
15+
* Approve or reject a hackathon
16+
* Requires: Platform admin access
17+
*/
18+
export async function POST(request: NextRequest, context: RouteContext) {
19+
return withPlatformAdmin(async () => {
20+
try {
21+
const { id } = await context.params
22+
const body = await request.json()
23+
const { action, reason } = body
24+
25+
if (!action || !['approve', 'reject'].includes(action)) {
26+
return NextResponse.json(
27+
{ success: false, error: 'Invalid action' },
28+
{ status: 400 }
29+
)
30+
}
31+
32+
if (action === 'reject' && !reason) {
33+
return NextResponse.json(
34+
{ success: false, error: 'Rejection reason is required' },
35+
{ status: 400 }
36+
)
37+
}
38+
39+
const supabase = await createClient()
40+
const { data: { user } } = await supabase.auth.getUser()
41+
42+
if (!user) {
43+
return NextResponse.json(
44+
{ success: false, error: 'Unauthorized' },
45+
{ status: 401 }
46+
)
47+
}
48+
49+
// Get the hackathon
50+
const { data: hackathon, error: fetchError } = await supabase
51+
.from('hackathons')
52+
.select('*')
53+
.eq('id', id)
54+
.single()
55+
56+
if (fetchError || !hackathon) {
57+
return NextResponse.json(
58+
{ success: false, error: 'Hackathon not found' },
59+
{ status: 404 }
60+
)
61+
}
62+
63+
// Update hackathon based on action
64+
const updateData: {
65+
updated_at: string
66+
approval_status?: string
67+
approved_by?: string
68+
approved_at?: string
69+
status?: string
70+
rejection_reason?: string | null
71+
} = {
72+
updated_at: new Date().toISOString(),
73+
}
74+
75+
if (action === 'approve') {
76+
updateData.approval_status = 'approved'
77+
updateData.approved_by = user.id
78+
updateData.approved_at = new Date().toISOString()
79+
updateData.status = 'published'
80+
updateData.rejection_reason = null
81+
} else if (action === 'reject') {
82+
updateData.approval_status = 'rejected'
83+
updateData.rejection_reason = reason
84+
updateData.status = 'draft'
85+
}
86+
87+
const { error: updateError } = await supabase
88+
.from('hackathons')
89+
.update(updateData)
90+
.eq('id', id)
91+
92+
if (updateError) {
93+
throw updateError
94+
}
95+
96+
// Invalidate caches
97+
await UnifiedCache.purgeByTags(['content', 'api'])
98+
99+
return NextResponse.json({
100+
success: true,
101+
message: `Hackathon ${action}d successfully`,
102+
})
103+
} catch (error) {
104+
console.error('Error in hackathon moderation action:', error)
105+
return NextResponse.json(
106+
{
107+
success: false,
108+
error: error instanceof Error ? error.message : 'Failed to execute action',
109+
},
110+
{ status: 500 }
111+
)
112+
}
113+
})(request)
114+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// API route for admin moderation queue - list pending hackathons
2+
import { NextRequest, NextResponse } from 'next/server'
3+
import { withPlatformAdmin } from '@/lib/services/authorization-service'
4+
import { createClient } from '@/lib/supabase/server'
5+
6+
/**
7+
* GET /api/admin/moderation/hackathons
8+
* Get pending hackathons for moderation
9+
* Requires: Platform admin access
10+
*/
11+
export const GET = withPlatformAdmin(async (request: NextRequest) => {
12+
try {
13+
const { searchParams } = new URL(request.url)
14+
const limit = parseInt(searchParams.get('limit') || '20')
15+
const offset = parseInt(searchParams.get('offset') || '0')
16+
17+
const supabase = await createClient()
18+
19+
// Get pending hackathons
20+
const { data: hackathons, error, count } = await supabase
21+
.from('hackathons')
22+
.select(`
23+
*,
24+
company:companies(*)
25+
`, { count: 'exact' })
26+
.eq('approval_status', 'pending')
27+
.order('created_at', { ascending: false })
28+
.range(offset, offset + limit - 1)
29+
30+
if (error) {
31+
throw error
32+
}
33+
34+
return NextResponse.json({
35+
success: true,
36+
data: {
37+
hackathons: hackathons || [],
38+
total: count || 0,
39+
limit,
40+
offset,
41+
hasMore: (count || 0) > offset + limit,
42+
},
43+
})
44+
} catch (error) {
45+
console.error('Error fetching hackathon moderation queue:', error)
46+
return NextResponse.json(
47+
{
48+
success: false,
49+
error: error instanceof Error ? error.message : 'Failed to fetch moderation queue',
50+
},
51+
{ status: 500 }
52+
)
53+
}
54+
})

0 commit comments

Comments
 (0)