diff --git a/app/admin/moderation/page.tsx b/app/admin/moderation/page.tsx
index a5218b42..f9eb6ec8 100644
--- a/app/admin/moderation/page.tsx
+++ b/app/admin/moderation/page.tsx
@@ -2,11 +2,19 @@
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,
@@ -14,17 +22,31 @@ export default function ModerationPage() {
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,
})
}
}
@@ -38,6 +60,8 @@ export default function ModerationPage() {
fetchStats()
}, [])
+ const stats = activeTab === "events" ? eventStats : hackathonStats
+
return (
{/* Header */}
@@ -45,10 +69,10 @@ export default function ModerationPage() {
- Event Moderation Queue
+ Content Moderation
- Review and approve events submitted by companies
+ Review and approve events and hackathons submitted by companies
@@ -113,25 +137,61 @@ export default function ModerationPage() {
- {/* Moderation Queue */}
-
-
-
-
-
-
- Pending Events
-
-
- Review events and take action
-
-
-
-
-
-
-
-
+ {/* Moderation Queue with Tabs */}
+
+
+
+
+ Events
+
+
+
+ Hackathons
+
+
+
+
+
+
+
+
+
+
+ Pending Events
+
+
+ Review events and take action
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Pending Hackathons
+
+
+ Review hackathons and take action
+
+
+
+
+
+
+
+
+
+
)
}
diff --git a/app/api/admin/moderation/hackathons/[id]/route.ts b/app/api/admin/moderation/hackathons/[id]/route.ts
new file mode 100644
index 00000000..48cf8433
--- /dev/null
+++ b/app/api/admin/moderation/hackathons/[id]/route.ts
@@ -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)
+}
diff --git a/app/api/admin/moderation/hackathons/route.ts b/app/api/admin/moderation/hackathons/route.ts
new file mode 100644
index 00000000..895ba1ba
--- /dev/null
+++ b/app/api/admin/moderation/hackathons/route.ts
@@ -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 }
+ )
+ }
+})
diff --git a/app/api/companies/[slug]/hackathons/route.ts b/app/api/companies/[slug]/hackathons/route.ts
index 1e5ada98..f9291c0b 100644
--- a/app/api/companies/[slug]/hackathons/route.ts
+++ b/app/api/companies/[slug]/hackathons/route.ts
@@ -1,6 +1,5 @@
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
-import { UnifiedCache } from '@/lib/unified-cache-system'
// Force Node.js runtime for API routes
export const runtime = 'nodejs'
@@ -20,19 +19,15 @@ export async function GET(
// Parse query parameters
const search = searchParams.get('search') || undefined
+ const status = searchParams.get('status') || undefined
const limit = parseInt(searchParams.get('limit') || '12')
const offset = parseInt(searchParams.get('offset') || '0')
- // Try to get from cache first
- const cacheKey = `company:${slug}:hackathons:${JSON.stringify({ search, limit, offset })}`
- const cached = await UnifiedCache.get(cacheKey)
-
- if (cached) {
- return UnifiedCache.createResponse(cached, 'API_STANDARD')
- }
-
const supabase = await createClient()
+ // Check if user is authenticated
+ const { data: { user } } = await supabase.auth.getUser()
+
// First, get the company by slug
const { data: company, error: companyError } = await supabase
.from('companies')
@@ -55,13 +50,40 @@ export async function GET(
)
}
+ // Check if user is a member of the company
+ let isCompanyMember = false
+ if (user) {
+ const { data: membership } = await supabase
+ .from('company_members')
+ .select('role')
+ .eq('company_id', company.id)
+ .eq('user_id', user.id)
+ .eq('status', 'active')
+ .single()
+
+ if (membership) {
+ isCompanyMember = true
+ }
+ }
+
// Build the query for hackathons
let query = supabase
.from('hackathons')
.select('*', { count: 'exact' })
.eq('company_id', company.id)
- .eq('approval_status', 'approved')
- .order('start_date', { ascending: false })
+
+ // If user is not a company member, only show approved hackathons
+ if (!isCompanyMember) {
+ query = query.eq('approval_status', 'approved')
+ } else if (status === 'all') {
+ // Company members can see all hackathons
+ // No additional filter needed
+ } else {
+ // Default: show all for company members
+ // No additional filter needed
+ }
+
+ query = query.order('date', { ascending: false })
// Apply search filter if provided
if (search) {
@@ -94,10 +116,7 @@ export async function GET(
hasMore: (count || 0) > offset + limit,
}
- // Cache the result
- await UnifiedCache.set(cacheKey, result, 'API_STANDARD')
-
- return UnifiedCache.createResponse(result, 'API_STANDARD')
+ return NextResponse.json(result)
} catch (error) {
console.error('Error in GET /api/companies/[slug]/hackathons:', error)
diff --git a/app/api/hackathons/[id]/route.ts b/app/api/hackathons/[id]/route.ts
index fe1fd7a8..859ee2d6 100644
--- a/app/api/hackathons/[id]/route.ts
+++ b/app/api/hackathons/[id]/route.ts
@@ -1,5 +1,6 @@
import { NextRequest, NextResponse } from 'next/server'
import { hackathonsService } from '@/lib/services/hackathons'
+import { createClient } from '@/lib/supabase/server'
// Force Node.js runtime for API routes
export const runtime = 'nodejs';
@@ -39,9 +40,65 @@ export async function PUT(request: NextRequest, { params }: RouteContext) {
const { id } = await params
const hackathonData = await request.json()
+ // Check authentication
+ const supabase = await createClient()
+ const { data: { user } } = await supabase.auth.getUser()
+
+ if (!user) {
+ return NextResponse.json(
+ { error: 'Unauthorized: Authentication required' },
+ { status: 401 }
+ )
+ }
+
+ // Get the existing hackathon to check company_id
+ const existingHackathon = await hackathonsService.getHackathonBySlug(id)
+
+ if (!existingHackathon) {
+ return NextResponse.json(
+ { error: 'Hackathon not found' },
+ { status: 404 }
+ )
+ }
+
+ let isAuthorized = false
+
+ // Check if user is admin
+ const { data: profile } = await supabase
+ .from('profiles')
+ .select('is_admin')
+ .eq('id', user.id)
+ .single()
+
+ if (profile?.is_admin) {
+ isAuthorized = true
+ }
+
+ // If not admin, check if user is a member of the company
+ if (!isAuthorized && existingHackathon.company_id) {
+ const { data: membership } = await supabase
+ .from('company_members')
+ .select('role')
+ .eq('company_id', existingHackathon.company_id)
+ .eq('user_id', user.id)
+ .eq('status', 'active')
+ .single()
+
+ if (membership) {
+ isAuthorized = true
+ }
+ }
+
+ if (!isAuthorized) {
+ return NextResponse.json(
+ { error: 'Unauthorized: You must be a company member or admin to update this hackathon' },
+ { status: 401 }
+ )
+ }
+
const hackathon = await hackathonsService.updateHackathon(id, hackathonData)
- return NextResponse.json(hackathon)
+ return NextResponse.json({ hackathon })
} catch (error) {
console.error('Error in PUT /api/hackathons/[id]:', error)
return NextResponse.json(
@@ -55,6 +112,63 @@ export async function PUT(request: NextRequest, { params }: RouteContext) {
export async function DELETE(_request: NextRequest, { params }: RouteContext) {
try {
const { id } = await params
+
+ // Check authentication
+ const supabase = await createClient()
+ const { data: { user } } = await supabase.auth.getUser()
+
+ if (!user) {
+ return NextResponse.json(
+ { error: 'Unauthorized: Authentication required' },
+ { status: 401 }
+ )
+ }
+
+ // Get the existing hackathon to check company_id
+ const existingHackathon = await hackathonsService.getHackathonBySlug(id)
+
+ if (!existingHackathon) {
+ return NextResponse.json(
+ { error: 'Hackathon not found' },
+ { status: 404 }
+ )
+ }
+
+ let isAuthorized = false
+
+ // Check if user is admin
+ const { data: profile } = await supabase
+ .from('profiles')
+ .select('is_admin')
+ .eq('id', user.id)
+ .single()
+
+ if (profile?.is_admin) {
+ isAuthorized = true
+ }
+
+ // If not admin, check if user is a member of the company
+ if (!isAuthorized && existingHackathon.company_id) {
+ const { data: membership } = await supabase
+ .from('company_members')
+ .select('role')
+ .eq('company_id', existingHackathon.company_id)
+ .eq('user_id', user.id)
+ .eq('status', 'active')
+ .single()
+
+ if (membership) {
+ isAuthorized = true
+ }
+ }
+
+ if (!isAuthorized) {
+ return NextResponse.json(
+ { error: 'Unauthorized: You must be a company member or admin to delete this hackathon' },
+ { status: 401 }
+ )
+ }
+
await hackathonsService.deleteHackathon(id)
return NextResponse.json({ message: 'Hackathon deleted successfully' })
diff --git a/app/api/hackathons/[id]/submit/route.ts b/app/api/hackathons/[id]/submit/route.ts
new file mode 100644
index 00000000..460e6941
--- /dev/null
+++ b/app/api/hackathons/[id]/submit/route.ts
@@ -0,0 +1,129 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { hackathonsService } from '@/lib/services/hackathons'
+import { createClient } from '@/lib/supabase/server'
+import { UnifiedCache } from '@/lib/unified-cache-system'
+
+export const runtime = 'nodejs'
+
+interface RouteContext {
+ params: Promise<{
+ id: string
+ }>
+}
+
+// POST: Submit hackathon for approval
+export async function POST(request: NextRequest, { params }: RouteContext) {
+ try {
+ const { id } = await params
+
+ // Check authentication
+ const supabase = await createClient()
+ const { data: { user }, error: authError } = await supabase.auth.getUser()
+
+ if (authError || !user) {
+ return NextResponse.json(
+ { error: 'Unauthorized: Authentication required' },
+ { status: 401 }
+ )
+ }
+
+ // Get existing hackathon
+ const existingHackathon = await hackathonsService.getHackathonBySlug(id)
+
+ if (!existingHackathon) {
+ return NextResponse.json(
+ { error: 'Hackathon not found' },
+ { status: 404 }
+ )
+ }
+
+ // Check authorization - user must be a member of the company
+ let isAuthorized = false
+
+ // Check if user is admin
+ const { data: profile } = await supabase
+ .from('profiles')
+ .select('is_admin')
+ .eq('id', user.id)
+ .single()
+
+ if (profile?.is_admin) {
+ isAuthorized = true
+ }
+
+ // If not admin, check if user is a member of the company
+ if (!isAuthorized && existingHackathon.company_id) {
+ const { data: membership } = await supabase
+ .from('company_members')
+ .select('role')
+ .eq('company_id', existingHackathon.company_id)
+ .eq('user_id', user.id)
+ .eq('status', 'active')
+ .single()
+
+ if (membership) {
+ isAuthorized = true
+ }
+ }
+
+ if (!isAuthorized) {
+ return NextResponse.json(
+ { error: 'You do not have permission to submit this hackathon' },
+ { status: 403 }
+ )
+ }
+
+ // Check if hackathon is already submitted or approved
+ if (existingHackathon.approval_status === 'pending') {
+ return NextResponse.json(
+ { error: 'Hackathon is already pending approval' },
+ { status: 400 }
+ )
+ }
+
+ if (existingHackathon.approval_status === 'approved') {
+ return NextResponse.json(
+ { error: 'Hackathon is already approved' },
+ { status: 400 }
+ )
+ }
+
+ // Update approval status to pending and status to published
+ const { data: hackathon, error } = await supabase
+ .from('hackathons')
+ .update({
+ approval_status: 'pending',
+ status: 'published',
+ updated_at: new Date().toISOString(),
+ })
+ .eq('slug', id)
+ .select()
+ .single()
+
+ if (error) {
+ console.error('Error submitting hackathon:', error)
+ return NextResponse.json(
+ { error: 'Failed to submit hackathon for approval' },
+ { status: 500 }
+ )
+ }
+
+ // Invalidate caches
+ await UnifiedCache.purgeByTags(['content', 'api'])
+
+ return NextResponse.json(
+ {
+ message: 'Hackathon submitted for approval successfully',
+ hackathon
+ },
+ { status: 200 }
+ )
+
+ } catch (error) {
+ console.error('Error in POST /api/hackathons/[id]/submit:', error)
+ return NextResponse.json(
+ { error: 'Internal server error' },
+ { status: 500 }
+ )
+ }
+}
diff --git a/app/api/hackathons/route.ts b/app/api/hackathons/route.ts
index 3bad0983..363c41d3 100644
--- a/app/api/hackathons/route.ts
+++ b/app/api/hackathons/route.ts
@@ -44,6 +44,7 @@ interface HackathonData {
socials?: Record;
sponsors?: unknown[];
marking_scheme?: unknown;
+ company_id?: string;
}
// GET: Fetch hackathons with optional filters
@@ -102,30 +103,48 @@ export async function POST(request: NextRequest) {
try {
const hackathonData: HackathonData = await request.json();
- // Check for admin authentication header or session
+ // Check for authentication
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
+ if (!user) {
+ return NextResponse.json(
+ { error: 'Unauthorized: Authentication required' },
+ { status: 401 }
+ );
+ }
+
let isAuthorized = false;
- // Check if user is authenticated and is admin
- if (user) {
- // Check admin status from profiles table
- const { data: profile } = await supabase
- .from('profiles')
- .select('is_admin')
- .eq('id', user.id)
+ // Check if user is admin
+ const { data: profile } = await supabase
+ .from('profiles')
+ .select('is_admin')
+ .eq('id', user.id)
+ .single();
+
+ if (profile?.is_admin) {
+ isAuthorized = true;
+ }
+
+ // If not admin, check if user is a member of the company
+ if (!isAuthorized && hackathonData.company_id) {
+ const { data: membership } = await supabase
+ .from('company_members')
+ .select('role')
+ .eq('company_id', hackathonData.company_id)
+ .eq('user_id', user.id)
+ .eq('status', 'active')
.single();
- if (profile?.is_admin) {
+ if (membership) {
isAuthorized = true;
}
}
- // If not authorized through session, check if it's a direct admin request
if (!isAuthorized) {
return NextResponse.json(
- { error: 'Unauthorized: Admin access required' },
+ { error: 'Unauthorized: You must be a company member or admin to create hackathons' },
{ status: 401 }
);
}
diff --git a/app/dashboard/company/[slug]/hackathons/[hackathonSlug]/edit/page.tsx b/app/dashboard/company/[slug]/hackathons/[hackathonSlug]/edit/page.tsx
new file mode 100644
index 00000000..672cd13a
--- /dev/null
+++ b/app/dashboard/company/[slug]/hackathons/[hackathonSlug]/edit/page.tsx
@@ -0,0 +1,170 @@
+'use client'
+
+import { useState, useEffect, useCallback } from 'react'
+import { useRouter, useParams } from 'next/navigation'
+import { useCompanyContext } from '@/contexts/CompanyContext'
+import { HackathonForm } from '@/components/dashboard/HackathonForm'
+import { ArrowLeft, Trash2 } from 'lucide-react'
+import { Button } from '@/components/ui/button'
+import Link from 'next/link'
+import { Hackathon } from '@/types/hackathons'
+import { toast } from 'sonner'
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+} from '@/components/ui/alert-dialog'
+
+export default function EditHackathonPage() {
+ const router = useRouter()
+ const params = useParams()
+ const companySlug = params.slug as string
+ const hackathonSlug = params.hackathonSlug as string
+ const { currentCompany, loading: companyLoading } = useCompanyContext()
+ const [hackathon, setHackathon] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const [deleting, setDeleting] = useState(false)
+
+ const fetchHackathon = useCallback(async () => {
+ try {
+ setLoading(true)
+ const response = await fetch(`/api/hackathons/${hackathonSlug}`)
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch hackathon')
+ }
+
+ const data = await response.json()
+ setHackathon(data)
+ } catch (error) {
+ console.error('Error fetching hackathon:', error)
+ toast.error('Failed to load hackathon')
+ router.push(`/dashboard/company/${companySlug}/hackathons`)
+ } finally {
+ setLoading(false)
+ }
+ }, [hackathonSlug, companySlug, router])
+
+ useEffect(() => {
+ if (hackathonSlug) {
+ fetchHackathon()
+ }
+ }, [hackathonSlug, fetchHackathon])
+
+ const handleSuccess = (updatedHackathon: Hackathon) => {
+ setHackathon(updatedHackathon)
+ toast.success('Hackathon updated successfully!')
+ }
+
+ const handleDelete = async () => {
+ try {
+ setDeleting(true)
+ const response = await fetch(`/api/hackathons/${hackathon?.id}`, {
+ method: 'DELETE',
+ })
+
+ if (!response.ok) {
+ throw new Error('Failed to delete hackathon')
+ }
+
+ toast.success('Hackathon deleted successfully!')
+ router.push(`/dashboard/company/${companySlug}/hackathons`)
+ } catch (error) {
+ console.error('Error deleting hackathon:', error)
+ toast.error('Failed to delete hackathon')
+ } finally {
+ setDeleting(false)
+ }
+ }
+
+ if (loading || companyLoading || !currentCompany) {
+ return (
+
+ )
+ }
+
+ if (!hackathon) {
+ return (
+
+
+
Hackathon not found
+
+ The hackathon you're looking for doesn't exist or you don't have access to it.
+
+
+
Back to Hackathons
+
+
+
+ )
+ }
+
+ return (
+
+
+
+
+
+ Back to Hackathons
+
+
+
+
+
Edit Hackathon
+
+ Update your hackathon details
+
+
+
+
+
+
+
+
Danger Zone
+
Once you delete a hackathon, there is no going back.
+
+
+
+
+
+ Delete Hackathon
+
+
+
+
+ Are you sure?
+
+ This action cannot be undone. This will permanently delete the hackathon
+ "{hackathon.title}" and all associated data.
+
+
+
+ Cancel
+
+ {deleting ? 'Deleting...' : 'Delete Hackathon'}
+
+
+
+
+
+
+
+ )
+}
diff --git a/app/dashboard/company/[slug]/hackathons/create/page.tsx b/app/dashboard/company/[slug]/hackathons/create/page.tsx
new file mode 100644
index 00000000..46b19d7a
--- /dev/null
+++ b/app/dashboard/company/[slug]/hackathons/create/page.tsx
@@ -0,0 +1,53 @@
+'use client'
+
+import { useRouter, useParams } from 'next/navigation'
+import { useCompanyContext } from '@/contexts/CompanyContext'
+import { HackathonForm } from '@/components/dashboard/HackathonForm'
+import { ArrowLeft } from 'lucide-react'
+import { Button } from '@/components/ui/button'
+import Link from 'next/link'
+
+export default function CreateHackathonPage() {
+ const router = useRouter()
+ const params = useParams()
+ const companySlug = params.slug as string
+ const { currentCompany, loading: companyLoading } = useCompanyContext()
+
+ const handleSuccess = () => {
+ router.push(`/dashboard/company/${companySlug}/hackathons`)
+ }
+
+ if (companyLoading || !currentCompany) {
+ return (
+
+ )
+ }
+
+ return (
+
+
+
+
+
+ Back to Hackathons
+
+
+
+
+
Create Hackathon
+
+ Create a new hackathon for your company
+
+
+
+
+
+
+ )
+}
diff --git a/app/dashboard/company/[slug]/hackathons/page.tsx b/app/dashboard/company/[slug]/hackathons/page.tsx
new file mode 100644
index 00000000..80f862e7
--- /dev/null
+++ b/app/dashboard/company/[slug]/hackathons/page.tsx
@@ -0,0 +1,297 @@
+'use client'
+
+import { useState, useEffect, useCallback } from 'react'
+import { useCompanyContext } from '@/contexts/CompanyContext'
+import { Button } from '@/components/ui/button'
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
+import { Badge } from '@/components/ui/badge'
+import { Input } from '@/components/ui/input'
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
+import { Trophy, Search, Plus, Edit, Eye, Clock, CheckCircle, XCircle, AlertCircle } from 'lucide-react'
+import { toast } from 'sonner'
+import Link from 'next/link'
+
+interface Hackathon {
+ id: string
+ slug: string
+ title: string
+ excerpt: string
+ category: string
+ status: string
+ approval_status: string
+ date: string
+ time: string
+ duration: string
+ views: number
+ registered: number
+ prize?: string
+}
+
+export default function CompanyHackathonsPage() {
+ const { currentCompany, loading: companyLoading } = useCompanyContext()
+ const [hackathons, setHackathons] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [searchTerm, setSearchTerm] = useState('')
+
+ const fetchHackathons = useCallback(async () => {
+ if (!currentCompany) return
+
+ try {
+ 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')
+ }
+
+ const data = await response.json()
+ setHackathons(data.hackathons || [])
+ } catch (error) {
+ console.error('Error fetching hackathons:', error)
+ toast.error('Failed to load hackathons')
+ } finally {
+ setLoading(false)
+ }
+ }, [currentCompany])
+
+ useEffect(() => {
+ if (currentCompany) {
+ fetchHackathons()
+ }
+ }, [currentCompany, fetchHackathons])
+
+ const filteredHackathons = hackathons.filter(hackathon =>
+ hackathon.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ hackathon.category?.toLowerCase().includes(searchTerm.toLowerCase())
+ )
+
+ 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,
+ }
+
+ const getApprovalBadge = (status: string) => {
+ switch (status) {
+ case 'approved':
+ return (
+
+
+ Approved
+
+ )
+ case 'pending':
+ return (
+
+
+ Pending
+
+ )
+ case 'rejected':
+ return (
+
+
+ Rejected
+
+ )
+ case 'changes_requested':
+ return (
+
+
+ Changes Requested
+
+ )
+ default:
+ return {status}
+ }
+ }
+
+ const getStatusBadge = (status: string) => {
+ switch (status) {
+ case 'live':
+ case 'published':
+ return Live
+ case 'draft':
+ return Draft
+ case 'cancelled':
+ return Cancelled
+ case 'completed':
+ return Completed
+ default:
+ return {status}
+ }
+ }
+
+ if (companyLoading || !currentCompany) {
+ return (
+
+ )
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
Hackathons
+
+ Manage your company's hackathons and coding challenges
+
+
+
+
+
+ Create Hackathon
+
+
+
+
+ {/* Stats Cards */}
+
+
+
+ Total Hackathons
+
+
+
+ {stats.total}
+
+
+
+
+ Approved
+
+
+
+ {stats.approved}
+
+
+
+
+ Pending Review
+
+
+
+ {stats.pending}
+
+
+
+
+ Drafts
+
+
+
+ {stats.draft}
+
+
+
+
+ {/* Search */}
+
+
+
+ setSearchTerm(e.target.value)}
+ className="pl-10"
+ />
+
+
+
+ {/* Hackathons Table */}
+
+
+ All Hackathons ({filteredHackathons.length})
+
+ View and manage all your hackathons
+
+
+
+ {loading ? (
+
+ ) : filteredHackathons.length === 0 ? (
+
+
+
+ No hackathons found
+
+
+ {searchTerm ? 'Try adjusting your search' : 'Hackathon creation coming soon'}
+
+
+ ) : (
+
+
+
+ Hackathon
+ Duration
+ Category
+ Status
+ Approval
+ Views
+ Participants
+ Actions
+
+
+
+ {filteredHackathons.map((hackathon) => (
+
+
+
+ {hackathon.title}
+
+ {hackathon.excerpt}
+
+
+
+
+
+ {new Date(hackathon.date).toLocaleDateString()}
+ {hackathon.duration && ` (${hackathon.duration})`}
+
+
+
+ {hackathon.category || 'General'}
+
+ {getStatusBadge(hackathon.status)}
+ {getApprovalBadge(hackathon.approval_status)}
+
+
+
+ {hackathon.views || 0}
+
+
+ {hackathon.registered || 0}
+
+
+
+
+
+
+
+ {hackathon.approval_status === 'approved' && (
+
+
+
+
+
+ )}
+
+
+
+ ))}
+
+
+ )}
+
+
+
+ )
+}
diff --git a/components/dashboard/HackathonForm.tsx b/components/dashboard/HackathonForm.tsx
new file mode 100644
index 00000000..84d29028
--- /dev/null
+++ b/components/dashboard/HackathonForm.tsx
@@ -0,0 +1,493 @@
+'use client'
+
+import { useState } from 'react'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import { Textarea } from '@/components/ui/textarea'
+import { Label } from '@/components/ui/label'
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
+import { Badge } from '@/components/ui/badge'
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
+import { Hackathon } from '@/types/hackathons'
+import { Company } from '@/types/company'
+import { Save, Send, AlertCircle, ChevronDown } from 'lucide-react'
+import { toast } from 'sonner'
+
+interface HackathonFormProps {
+ company: Company
+ hackathon?: Hackathon
+ mode: 'create' | 'edit'
+ onSuccess?: (hackathon: Hackathon) => void
+}
+
+export function HackathonForm({ company, hackathon, mode, onSuccess }: HackathonFormProps) {
+ const [loading, setLoading] = useState(false)
+ const [companyInfoOpen, setCompanyInfoOpen] = useState(false)
+ const [formData, setFormData] = useState({
+ title: hackathon?.title || '',
+ slug: hackathon?.slug || '',
+ excerpt: hackathon?.excerpt || '',
+ description: hackathon?.description || '',
+ organizer: hackathon?.organizer || company.name,
+ date: hackathon?.date || '',
+ time: hackathon?.time || '',
+ duration: hackathon?.duration || '',
+ category: hackathon?.category || '',
+ location: hackathon?.location || '',
+ capacity: hackathon?.capacity || 0,
+ price: hackathon?.price || 'Free',
+ payment: hackathon?.payment || 'Not Required',
+ registration_required: hackathon?.registration_required ?? true,
+ registration_deadline: hackathon?.registration_deadline || '',
+ prize: hackathon?.prize || '',
+ prize_details: hackathon?.prize_details || '',
+ team_size_min: (hackathon?.team_size as { min?: number; max?: number } | undefined)?.min || 1,
+ team_size_max: (hackathon?.team_size as { min?: number; max?: number } | undefined)?.max || 5,
+ })
+
+ const handleChange = (field: string, value: string | number | boolean) => {
+ setFormData(prev => ({ ...prev, [field]: value }))
+ }
+
+ const handleSubmit = async (submitForApproval: boolean = false) => {
+ try {
+ setLoading(true)
+
+ if (!formData.title || !formData.excerpt || !formData.description) {
+ toast.error('Please fill in all required fields')
+ return
+ }
+
+ if (!formData.date || !formData.time || !formData.duration) {
+ toast.error('Please provide hackathon date, time, and duration')
+ return
+ }
+
+ if (!formData.category || !formData.location) {
+ toast.error('Please select category and location')
+ return
+ }
+
+ const slug = formData.slug || formData.title
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, '-')
+ .replace(/^-|-$/g, '')
+
+ // Remove team_size_min and team_size_max from formData before spreading
+ const { team_size_min, team_size_max, ...restFormData } = formData
+
+ const hackathonData = {
+ ...restFormData,
+ slug,
+ categories: [formData.category],
+ tags: [],
+ locations: [formData.location],
+ registered: hackathon?.registered || 0,
+ event_type: ['Offline'],
+ user_types: ['Professionals', 'College Students'],
+ featured: false,
+ status: 'draft',
+ rules: [],
+ schedule: [],
+ faq: [],
+ socials: {},
+ sponsors: [],
+ company_id: company.id,
+ team_size: {
+ min: team_size_min,
+ max: team_size_max,
+ },
+ views: hackathon?.views || 0,
+ clicks: hackathon?.clicks || 0,
+ approval_status: 'draft',
+ is_codeunia_event: false,
+ }
+
+ let response
+ if (mode === 'edit' && hackathon) {
+ response = await fetch(`/api/hackathons/${hackathon.id}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(hackathonData),
+ })
+ } else {
+ response = await fetch('/api/hackathons', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(hackathonData),
+ })
+ }
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}))
+ throw new Error(errorData.error || 'Failed to save hackathon')
+ }
+
+ const data = await response.json()
+
+ // If submitting for approval, call the submit endpoint
+ if (submitForApproval && data.hackathon) {
+ const submitResponse = await fetch(`/api/hackathons/${data.hackathon.slug}/submit`, {
+ method: 'POST',
+ })
+
+ if (!submitResponse.ok) {
+ toast.warning('Hackathon saved but failed to submit for approval')
+ } else {
+ toast.success('Hackathon submitted for approval!')
+ }
+ } else {
+ toast.success(mode === 'edit' ? 'Hackathon updated successfully!' : 'Hackathon created as draft!')
+ }
+
+ if (onSuccess && data.hackathon) {
+ onSuccess(data.hackathon)
+ }
+ } catch (error) {
+ console.error('Error saving hackathon:', error)
+ toast.error(error instanceof Error ? error.message : 'Failed to save hackathon')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const canSubmitForApproval = () => {
+ return formData.title && formData.excerpt && formData.description &&
+ formData.date && formData.time && formData.duration &&
+ formData.category && formData.location
+ }
+
+ return (
+
+
+
+
+
+
+
+ Company Information
+
+ This hackathon will be associated with {company.name}
+
+
+
+
+
+
+
+
+
+ {company.logo_url && (
+ // eslint-disable-next-line @next/next/no-img-element
+
+ )}
+
+
{company.name}
+
+ Verified
+
+
+
+
+
+
+
+
+ {hackathon?.approval_status === 'rejected' && hackathon.rejection_reason && (
+
+
+
+
+ Hackathon Rejected
+
+
+
+ {hackathon.rejection_reason}
+
+
+ )}
+
+
+
+ Basic Information
+
+ Provide the essential details about your hackathon
+
+
+
+
+
+ Hackathon Title *
+ handleChange('title', e.target.value)}
+ placeholder="Enter hackathon title"
+ />
+
+
+
+
URL Slug
+
handleChange('slug', e.target.value)}
+ placeholder="auto-generated from title"
+ />
+
+ Leave empty to auto-generate from title
+
+
+
+
+ Short Description *
+
+
+
+ Full Description *
+
+
+
+
+
+
+
+ Hackathon Details
+
+ When and where will the hackathon take place?
+
+
+
+
+
+
+
+ Category *
+ handleChange('category', value)}
+ >
+
+
+
+
+ Web Development
+ Mobile Development
+ AI/ML
+ Blockchain
+ IoT
+ Game Development
+ Cybersecurity
+ Open Innovation
+
+
+
+
+ Location *
+ handleChange('location', e.target.value)}
+ placeholder="Hackathon location or Online"
+ />
+
+
+
+
+
+
+
+
+
+ Team & Prizes
+
+ Configure team size and prize details
+
+
+
+
+
+
+
+
+ Prize Details
+
+
+
+
+
+
+ Registration & Pricing
+
+ Configure registration and pricing details
+
+
+
+
+
+ Entry Fee
+ handleChange('price', e.target.value)}
+ placeholder="e.g., Free, ₹500"
+ />
+
+
+ Payment Type
+ handleChange('payment', value)}
+ >
+
+
+
+
+ Not Required
+ Required
+
+
+
+
+
+
+
+
+
+ * Required fields
+
+
+ handleSubmit(false)}
+ disabled={loading}
+ >
+
+ {loading ? 'Saving...' : 'Save as Draft'}
+
+ handleSubmit(true)}
+ disabled={loading || !canSubmitForApproval()}
+ >
+
+ {loading ? 'Submitting...' : 'Submit for Approval'}
+
+
+
+
+ )
+}
diff --git a/components/moderation/HackathonModerationQueue.tsx b/components/moderation/HackathonModerationQueue.tsx
new file mode 100644
index 00000000..1c532cda
--- /dev/null
+++ b/components/moderation/HackathonModerationQueue.tsx
@@ -0,0 +1,335 @@
+"use client"
+
+import { useState, useEffect, useCallback } from "react"
+import { Hackathon } from "@/types/hackathons"
+import { Company } from "@/types/company"
+import { Button } from "@/components/ui/button"
+import { Badge } from "@/components/ui/badge"
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table"
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+} from "@/components/ui/alert-dialog"
+import { Textarea } from "@/components/ui/textarea"
+import { Label } from "@/components/ui/label"
+import {
+ CheckCircle,
+ XCircle,
+ Calendar,
+ MapPin,
+ Eye,
+ Loader2,
+ ChevronLeft,
+ ChevronRight,
+} from "lucide-react"
+import { toast } from "sonner"
+import { format } from "date-fns"
+import Link from "next/link"
+
+interface HackathonWithCompany extends Hackathon {
+ company?: Company
+}
+
+interface ModerationAction {
+ type: "approve" | "reject"
+ hackathonId: number
+ reason?: string
+}
+
+export function HackathonModerationQueue() {
+ const [hackathons, setHackathons] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [actionLoading, setActionLoading] = useState(null)
+ const [currentPage, setCurrentPage] = useState(1)
+ const [totalPages, setTotalPages] = useState(1)
+ const [showActionDialog, setShowActionDialog] = useState(false)
+ const [pendingAction, setPendingAction] = useState(null)
+ const [actionReason, setActionReason] = useState("")
+
+ const itemsPerPage = 10
+
+ const fetchHackathons = useCallback(async () => {
+ try {
+ setLoading(true)
+ const offset = (currentPage - 1) * itemsPerPage
+ const response = await fetch(
+ `/api/admin/moderation/hackathons?limit=${itemsPerPage}&offset=${offset}`
+ )
+
+ if (!response.ok) {
+ throw new Error("Failed to fetch hackathons")
+ }
+
+ const data = await response.json()
+ if (data.success) {
+ setHackathons(data.data.hackathons)
+ setTotalPages(Math.ceil(data.data.total / itemsPerPage))
+ }
+ } catch (error) {
+ console.error("Error fetching hackathons:", error)
+ toast.error("Failed to load hackathons")
+ } finally {
+ setLoading(false)
+ }
+ }, [currentPage])
+
+ useEffect(() => {
+ fetchHackathons()
+ }, [fetchHackathons])
+
+ const handleAction = (type: "approve" | "reject", hackathonId: number) => {
+ setPendingAction({ type, hackathonId })
+ setActionReason("")
+ setShowActionDialog(true)
+ }
+
+ const executeAction = async () => {
+ if (!pendingAction) return
+
+ try {
+ setActionLoading(pendingAction.hackathonId)
+ setShowActionDialog(false)
+
+ const response = await fetch(`/api/admin/moderation/hackathons/${pendingAction.hackathonId}`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ action: pendingAction.type,
+ reason: actionReason || undefined,
+ }),
+ })
+
+ if (!response.ok) {
+ throw new Error("Failed to execute action")
+ }
+
+ toast.success(
+ pendingAction.type === "approve"
+ ? "Hackathon approved successfully"
+ : "Hackathon rejected"
+ )
+
+ // Refresh the list
+ await fetchHackathons()
+ } catch (error) {
+ console.error("Error executing action:", error)
+ toast.error("Failed to execute action")
+ } finally {
+ setActionLoading(null)
+ setPendingAction(null)
+ setActionReason("")
+ }
+ }
+
+ if (loading) {
+ return (
+
+
+
+ )
+ }
+
+ if (hackathons.length === 0) {
+ return (
+
+
+
+ All caught up!
+
+
+ No hackathons pending review at the moment.
+
+
+ )
+ }
+
+ return (
+
+
+
+
+
+ Hackathon
+ Company
+ Date
+ Location
+ Submitted
+ Actions
+
+
+
+ {hackathons.map((hackathon) => (
+
+
+
+
+ {hackathon.title}
+
+
+ {hackathon.excerpt}
+
+
+
+ {hackathon.category}
+
+ {hackathon.prize && (
+
+ {hackathon.prize}
+
+ )}
+
+
+
+
+
+ {hackathon.company?.logo_url && (
+ // eslint-disable-next-line @next/next/no-img-element
+
+ )}
+
+ {hackathon.company?.name || hackathon.organizer}
+
+
+
+
+
+
+ {format(new Date(hackathon.date), "MMM dd, yyyy")}
+
+
+
+
+
+ {hackathon.location}
+
+
+
+ {hackathon.created_at
+ ? format(new Date(hackathon.created_at), "MMM dd, yyyy")
+ : "N/A"}
+
+
+
+
+
+
+
+
+ handleAction("approve", hackathon.id!)}
+ disabled={actionLoading === hackathon.id}
+ className="text-green-600 hover:text-green-700 hover:bg-green-50"
+ >
+ {actionLoading === hackathon.id ? (
+
+ ) : (
+
+ )}
+
+ handleAction("reject", hackathon.id!)}
+ disabled={actionLoading === hackathon.id}
+ className="text-red-600 hover:text-red-700 hover:bg-red-50"
+ >
+
+
+
+
+
+ ))}
+
+
+
+
+ {/* Pagination */}
+ {totalPages > 1 && (
+
+
+ Page {currentPage} of {totalPages}
+
+
+ setCurrentPage((p) => Math.max(1, p - 1))}
+ disabled={currentPage === 1}
+ >
+
+
+ setCurrentPage((p) => Math.min(totalPages, p + 1))}
+ disabled={currentPage === totalPages}
+ >
+
+
+
+
+ )}
+
+ {/* Action Dialog */}
+
+
+
+
+ {pendingAction?.type === "approve" ? "Approve Hackathon" : "Reject Hackathon"}
+
+
+ {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 === "reject" && (
+
+ Rejection Reason *
+
+ )}
+
+ Cancel
+
+ {pendingAction?.type === "approve" ? "Approve" : "Reject"}
+
+
+
+
+
+ )
+}
diff --git a/lib/services/hackathons.ts b/lib/services/hackathons.ts
index 81d56a9a..ab6d5291 100644
--- a/lib/services/hackathons.ts
+++ b/lib/services/hackathons.ts
@@ -38,6 +38,13 @@ class HackathonsService {
company:companies(*)
`, { count: 'exact' })
+ // Only show approved hackathons by default (unless approval_status filter is explicitly provided)
+ if (filters.approval_status) {
+ query = query.eq('approval_status', filters.approval_status)
+ } else {
+ query = query.eq('approval_status', 'approved')
+ }
+
// Apply filters
if (filters.search) {
query = query.or(`title.ilike.%${filters.search}%,description.ilike.%${filters.search}%`)
@@ -150,6 +157,7 @@ class HackathonsService {
company:companies(*)
`)
.eq('featured', true)
+ .eq('approval_status', 'approved')
.gte('date', new Date().toISOString().split('T')[0])
.order('date', { ascending: true })
.limit(limit)