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
120 changes: 90 additions & 30 deletions app/admin/moderation/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,51 @@

import { useState, useEffect } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { ModerationQueue } from "@/components/moderation/ModerationQueue"
import { AlertCircle, CheckCircle, Clock, Filter } from "lucide-react"
import { HackathonModerationQueue } from "@/components/moderation/HackathonModerationQueue"
import { AlertCircle, CheckCircle, Clock, Filter, Calendar, Trophy } from "lucide-react"

export default function ModerationPage() {
const [stats, setStats] = useState({
const [activeTab, setActiveTab] = useState("events")
const [eventStats, setEventStats] = useState({
pending: 0,
approved: 0,
rejected: 0,
})
const [hackathonStats, setHackathonStats] = useState({
pending: 0,
approved: 0,
rejected: 0,
})
const [loading, setLoading] = useState(true)

useEffect(() => {
// Fetch moderation stats
// Fetch moderation stats for both events and hackathons
const fetchStats = async () => {
try {
const response = await fetch('/api/admin/moderation/events?limit=1000')
if (response.ok) {
const data = await response.json()
// Fetch event stats
const eventsResponse = await fetch('/api/admin/moderation/events?limit=1000')
if (eventsResponse.ok) {
const data = await eventsResponse.json()
if (data.success) {
setEventStats({
pending: data.data.total,
approved: 0,
rejected: 0,
})
}
}

// Fetch hackathon stats
const hackathonsResponse = await fetch('/api/admin/moderation/hackathons?limit=1000')
if (hackathonsResponse.ok) {
const data = await hackathonsResponse.json()
if (data.success) {
setStats({
setHackathonStats({
pending: data.data.total,
approved: 0, // Would need separate endpoint for this
rejected: 0, // Would need separate endpoint for this
approved: 0,
rejected: 0,
})
}
}
Expand All @@ -38,17 +60,19 @@ export default function ModerationPage() {
fetchStats()
}, [])

const stats = activeTab === "events" ? eventStats : hackathonStats

return (
<div className="bg-black min-h-screen px-4 py-8 md:px-8 lg:px-16 space-y-8">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between pb-6 border-b border-zinc-800/60 gap-4">
<div>
<h1 className="text-2xl sm:text-3xl md:text-4xl font-extrabold tracking-tight text-white drop-shadow-sm flex items-center gap-3">
<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" />
Event Moderation Queue
Content Moderation
</h1>
<p className="text-zinc-400 mt-1 font-medium text-sm sm:text-base">
Review and approve events submitted by companies
Review and approve events and hackathons submitted by companies
</p>
</div>
</div>
Expand Down Expand Up @@ -113,25 +137,61 @@ export default function ModerationPage() {
</Card>
</div>

{/* Moderation Queue */}
<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">
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-lg font-bold text-zinc-900 dark:text-white flex items-center gap-2">
<Filter className="h-5 w-5 text-purple-400" />
Pending Events
</CardTitle>
<CardDescription className="text-zinc-500 dark:text-zinc-300 font-medium text-sm">
Review events and take action
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent>
<ModerationQueue />
</CardContent>
</Card>
{/* Moderation Queue with Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full max-w-md grid-cols-2 mb-6">
<TabsTrigger value="events" className="flex items-center gap-2">
<Calendar className="h-4 w-4" />
Events
</TabsTrigger>
<TabsTrigger value="hackathons" className="flex items-center gap-2">
<Trophy className="h-4 w-4" />
Hackathons
</TabsTrigger>
</TabsList>

<TabsContent value="events">
<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">
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-lg font-bold text-zinc-900 dark:text-white flex items-center gap-2">
<Filter className="h-5 w-5 text-purple-400" />
Pending Events
</CardTitle>
<CardDescription className="text-zinc-500 dark:text-zinc-300 font-medium text-sm">
Review events and take action
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent>
<ModerationQueue />
</CardContent>
</Card>
</TabsContent>

<TabsContent value="hackathons">
<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">
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-lg font-bold text-zinc-900 dark:text-white flex items-center gap-2">
<Filter className="h-5 w-5 text-purple-400" />
Pending Hackathons
</CardTitle>
<CardDescription className="text-zinc-500 dark:text-zinc-300 font-medium text-sm">
Review hackathons and take action
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent>
<HackathonModerationQueue />
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
)
}
114 changes: 114 additions & 0 deletions app/api/admin/moderation/hackathons/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// API route for hackathon moderation actions
import { NextRequest, NextResponse } from 'next/server'
import { withPlatformAdmin } from '@/lib/services/authorization-service'
import { createClient } from '@/lib/supabase/server'
import { UnifiedCache } from '@/lib/unified-cache-system'

interface RouteContext {
params: Promise<{
id: string
}>
}

/**
* POST /api/admin/moderation/hackathons/[id]
* Approve or reject a hackathon
* Requires: Platform admin access
*/
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

if (!action || !['approve', 'reject'].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 }
)
}

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

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()

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(),
}

if (action === 'approve') {
updateData.approval_status = 'approved'
updateData.approved_by = user.id
updateData.approved_at = new Date().toISOString()
updateData.status = 'published'
updateData.rejection_reason = null
} else if (action === 'reject') {
updateData.approval_status = 'rejected'
updateData.rejection_reason = reason
updateData.status = 'draft'
}

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

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)
}
54 changes: 54 additions & 0 deletions app/api/admin/moderation/hackathons/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// API route for admin moderation queue - list pending hackathons
import { NextRequest, NextResponse } from 'next/server'
import { withPlatformAdmin } from '@/lib/services/authorization-service'
import { createClient } from '@/lib/supabase/server'

/**
* GET /api/admin/moderation/hackathons
* Get pending hackathons for moderation
* Requires: Platform admin access
*/
export const GET = withPlatformAdmin(async (request: NextRequest) => {
try {
const { searchParams } = new URL(request.url)
const limit = parseInt(searchParams.get('limit') || '20')
const offset = parseInt(searchParams.get('offset') || '0')

const supabase = await createClient()

// Get pending hackathons
const { data: hackathons, error, count } = await supabase
.from('hackathons')
.select(`
*,
company:companies(*)
`, { count: 'exact' })
.eq('approval_status', 'pending')
.order('created_at', { ascending: false })
.range(offset, offset + limit - 1)

if (error) {
throw error
}

return NextResponse.json({
success: true,
data: {
hackathons: hackathons || [],
total: count || 0,
limit,
offset,
hasMore: (count || 0) > offset + limit,
},
})
} catch (error) {
console.error('Error fetching hackathon moderation queue:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch moderation queue',
},
{ status: 500 }
)
}
})
Loading
Loading