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. +

+ + + +
+
+ ) + } + + return ( +
+
+ + + + +
+

Edit Hackathon

+

+ Update your hackathon details +

+
+ + + +
+
+

Danger Zone

+

Once you delete a hackathon, there is no going back.

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

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 +

+
+ + + +
+ + {/* 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} + )} +
+

{company.name}

+ + Verified + +
+
+
+
+
+
+ + {hackathon?.approval_status === 'rejected' && hackathon.rejection_reason && ( + + + + + Hackathon Rejected + + + +

{hackathon.rejection_reason}

+
+
+ )} + + + + Basic Information + + Provide the essential details about your hackathon + + + +
+
+ + handleChange('title', e.target.value)} + placeholder="Enter hackathon title" + /> +
+ +
+ + handleChange('slug', e.target.value)} + placeholder="auto-generated from title" + /> +

+ Leave empty to auto-generate from title +

+
+ +
+ +