diff --git a/app/api/admin/moderation/hackathons/[id]/route.ts b/app/api/admin/moderation/hackathons/[id]/route.ts index 48cf8433..61e7e2e2 100644 --- a/app/api/admin/moderation/hackathons/[id]/route.ts +++ b/app/api/admin/moderation/hackathons/[id]/route.ts @@ -76,7 +76,7 @@ export async function POST(request: NextRequest, context: RouteContext) { updateData.approval_status = 'approved' updateData.approved_by = user.id updateData.approved_at = new Date().toISOString() - updateData.status = 'published' + updateData.status = 'live' updateData.rejection_reason = null } else if (action === 'reject') { updateData.approval_status = 'rejected' diff --git a/app/api/hackathons/[id]/register/route.ts b/app/api/hackathons/[id]/register/route.ts new file mode 100644 index 00000000..63de1e7b --- /dev/null +++ b/app/api/hackathons/[id]/register/route.ts @@ -0,0 +1,195 @@ +import { NextRequest, NextResponse } from 'next/server' +import { createClient } from '@/lib/supabase/server' + +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const supabase = await createClient() + const { data: { user }, error: authError } = await supabase.auth.getUser() + + if (authError || !user) { + return NextResponse.json( + { error: 'Authentication required' }, + { status: 401 } + ) + } + + const { id: slug } = await params + + // Get hackathon by slug + const { data: hackathon, error: hackathonError } = await supabase + .from('hackathons') + .select('id, title, slug, status, approval_status, capacity, registered') + .eq('slug', slug) + .single() + + if (hackathonError || !hackathon) { + return NextResponse.json( + { error: 'Hackathon not found' }, + { status: 404 } + ) + } + + // Check if hackathon is approved and live/published + if (hackathon.approval_status !== 'approved') { + return NextResponse.json( + { error: 'This hackathon is not available for registration' }, + { status: 400 } + ) + } + + if (hackathon.status !== 'live' && hackathon.status !== 'published') { + return NextResponse.json( + { error: 'This hackathon is not currently accepting registrations' }, + { status: 400 } + ) + } + + // Check capacity + if (hackathon.capacity && hackathon.registered >= hackathon.capacity) { + return NextResponse.json( + { error: 'This hackathon has reached its capacity' }, + { status: 400 } + ) + } + + // Check if user is already registered using master_registrations + const { data: existingRegistration } = await supabase + .from('master_registrations') + .select('id') + .eq('user_id', user.id) + .eq('activity_type', 'hackathon') + .eq('activity_id', hackathon.id.toString()) + .single() + + if (existingRegistration) { + return NextResponse.json( + { error: 'You are already registered for this hackathon' }, + { status: 400 } + ) + } + + // Get user profile + const { data: profile } = await supabase + .from('profiles') + .select('first_name, last_name, email, phone') + .eq('id', user.id) + .single() + + const fullName = profile ? `${profile.first_name || ''} ${profile.last_name || ''}`.trim() : '' + + // Register user in master_registrations table + const { error: registrationError } = await supabase + .from('master_registrations') + .insert({ + user_id: user.id, + activity_type: 'hackathon', + activity_id: hackathon.id.toString(), + status: 'registered', + full_name: fullName || undefined, + email: profile?.email || user.email, + phone: profile?.phone || undefined, + }) + + if (registrationError) { + console.error('Error creating registration:', registrationError) + return NextResponse.json( + { error: 'Failed to register for hackathon' }, + { status: 500 } + ) + } + + // Increment registered count + const { error: updateError } = await supabase + .from('hackathons') + .update({ registered: (hackathon.registered || 0) + 1 }) + .eq('id', hackathon.id) + + if (updateError) { + console.error('Error updating registered count:', updateError) + } + + return NextResponse.json({ + success: true, + message: 'Successfully registered for hackathon', + }) + } catch (error) { + console.error('Error registering for hackathon:', error) + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ) + } +} + +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const supabase = await createClient() + const { data: { user }, error: authError } = await supabase.auth.getUser() + + if (authError || !user) { + return NextResponse.json( + { error: 'Authentication required' }, + { status: 401 } + ) + } + + const { id: slug } = await params + + // Get hackathon by slug + const { data: hackathon, error: hackathonError } = await supabase + .from('hackathons') + .select('id, registered') + .eq('slug', slug) + .single() + + if (hackathonError || !hackathon) { + return NextResponse.json( + { error: 'Hackathon not found' }, + { status: 404 } + ) + } + + // Delete registration from master_registrations + const { error: deleteError } = await supabase + .from('master_registrations') + .delete() + .eq('user_id', user.id) + .eq('activity_type', 'hackathon') + .eq('activity_id', hackathon.id.toString()) + + if (deleteError) { + console.error('Error deleting registration:', deleteError) + return NextResponse.json( + { error: 'Failed to unregister from hackathon' }, + { status: 500 } + ) + } + + // Decrement registered count + const { error: updateError } = await supabase + .from('hackathons') + .update({ registered: Math.max(0, (hackathon.registered || 0) - 1) }) + .eq('id', hackathon.id) + + if (updateError) { + console.error('Error updating registered count:', updateError) + } + + return NextResponse.json({ + success: true, + message: 'Successfully unregistered from hackathon', + }) + } catch (error) { + console.error('Error unregistering from hackathon:', error) + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/app/api/hackathons/[id]/track-click/route.ts b/app/api/hackathons/[id]/track-click/route.ts index 8875246c..bfda8792 100644 --- a/app/api/hackathons/[id]/track-click/route.ts +++ b/app/api/hackathons/[id]/track-click/route.ts @@ -9,11 +9,11 @@ export async function POST( const supabase = await createClient() const { id } = await params - // Get hackathon by id + // Get hackathon by slug (id param is actually the slug) const { data: hackathon, error: hackathonError } = await supabase .from('hackathons') .select('id, company_id, clicks') - .eq('id', id) + .eq('slug', id) .single() if (hackathonError || !hackathon) { diff --git a/app/api/hackathons/[id]/track-view/route.ts b/app/api/hackathons/[id]/track-view/route.ts index b407623a..5ea6f8b6 100644 --- a/app/api/hackathons/[id]/track-view/route.ts +++ b/app/api/hackathons/[id]/track-view/route.ts @@ -9,11 +9,11 @@ export async function POST( const supabase = await createClient() const { id } = await params - // Get hackathon by id + // Get hackathon by slug (id param is actually the slug) const { data: hackathon, error: hackathonError } = await supabase .from('hackathons') .select('id, company_id, views') - .eq('id', id) + .eq('slug', id) .single() if (hackathonError || !hackathon) { diff --git a/app/hackathons/[id]/page.tsx b/app/hackathons/[id]/page.tsx index 7ee9bb8c..60c074f4 100644 --- a/app/hackathons/[id]/page.tsx +++ b/app/hackathons/[id]/page.tsx @@ -14,6 +14,7 @@ import React from "react"; import { useHackathon } from "@/hooks/useHackathons" import { CompanyBadge } from "@/components/companies/CompanyBadge" import { useAnalyticsTracking } from "@/hooks/useAnalyticsTracking" +import { toast } from "sonner" // import Header from "@/components/header"; import Footer from "@/components/footer"; @@ -110,6 +111,9 @@ const RotatingSponsorsGrid = ({ sponsors }: { sponsors?: Sponsor[] }) => { export default function HackathonDetailPage() { const [isAuthenticated, setIsAuthenticated] = useState(false) + const [isRegistered, setIsRegistered] = useState(false) + const [registering, setRegistering] = useState(false) + const [checkingRegistration, setCheckingRegistration] = useState(true) const params = useParams() const slug = params?.id as string @@ -118,7 +122,7 @@ export default function HackathonDetailPage() { const { hackathon, loading: isLoading, error: fetchError } = useHackathon(slug) // Track analytics - useAnalyticsTracking({ + const { trackClick } = useAnalyticsTracking({ hackathonId: slug, trackView: true, }) @@ -132,6 +136,100 @@ export default function HackathonDetailPage() { checkAuth() }, []) + // Check registration status + useEffect(() => { + const checkRegistrationStatus = async () => { + if (!isAuthenticated || !hackathon?.id) { + setCheckingRegistration(false) + return + } + + try { + const supabase = createClient() + const { data: { user } } = await supabase.auth.getUser() + + if (!user) { + setCheckingRegistration(false) + return + } + + const { data } = await supabase + .from('master_registrations') + .select('id') + .eq('user_id', user.id) + .eq('activity_type', 'hackathon') + .eq('activity_id', hackathon.id.toString()) + .single() + + setIsRegistered(!!data) + } catch (error) { + console.error('Error checking registration:', error) + } finally { + setCheckingRegistration(false) + } + } + + checkRegistrationStatus() + }, [isAuthenticated, hackathon?.id]) + + const handleRegister = async () => { + if (!hackathon) return + + // Track click on registration button + trackClick() + + setRegistering(true) + try { + const response = await fetch(`/api/hackathons/${slug}/register`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }) + + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.error || 'Failed to register') + } + + toast.success('Successfully registered for the hackathon!') + setIsRegistered(true) + + // Refresh hackathon data to update registered count + window.location.reload() + } catch (err) { + toast.error(err instanceof Error ? err.message : 'Failed to register') + } finally { + setRegistering(false) + } + } + + const handleUnregister = async () => { + if (!hackathon) return + + setRegistering(true) + try { + const response = await fetch(`/api/hackathons/${slug}/register`, { + method: 'DELETE', + }) + + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.error || 'Failed to unregister') + } + + toast.success('Successfully unregistered from the hackathon') + setIsRegistered(false) + + // Refresh hackathon data to update registered count + window.location.reload() + } catch (err) { + toast.error(err instanceof Error ? err.message : 'Failed to unregister') + } finally { + setRegistering(false) + } + } + const getCategoryColor = (category: string) => { switch (category) { case "Web Development": @@ -624,7 +722,7 @@ export default function HackathonDetailPage() { {/* Registration Card (mobile only) */} - {hackathon?.registration_required && hackathon?.status === 'live' && ( + {(hackathon?.status === 'live' || hackathon?.status === 'published') && (
Registration
@@ -632,13 +730,32 @@ export default function HackathonDetailPage() { {hackathon?.registered ?? 0} participants registered
-
- - Registration deadline - {hackathon?.registration_deadline ? new Date(hackathon.registration_deadline).toLocaleDateString() : '-'} -
+ {hackathon?.registration_deadline && ( +
+ + Registration deadline + {new Date(hackathon.registration_deadline).toLocaleDateString()} +
+ )} {isAuthenticated ? ( - + isRegistered ? ( + + ) : ( + + ) ) : ( + isRegistered ? ( + + ) : ( + + ) ) : (