From 97302a0f7cc76b667e79b5f9fa04f585e39db58d Mon Sep 17 00:00:00 2001 From: Akshay Date: Tue, 18 Nov 2025 09:59:38 +0530 Subject: [PATCH 1/3] feat(analytics): Add hackathon registration tracking and improve dashboard metrics - Add trackHackathonRegistration method to AnalyticsService to increment company registration counts - Integrate analytics tracking into hackathon registration endpoint with error handling - Fetch hackathons data in CompanyDashboard to calculate accurate approved hackathon count - Calculate total registrations from both events and hackathons instead of relying on stored metric - Update dashboard stats to use computed values for more accurate real-time metrics - Add error handling to prevent analytics failures from blocking registration flow --- app/api/hackathons/[id]/register/route.ts | 9 +++++++ components/dashboard/CompanyDashboard.tsx | 32 +++++++++++++++++++++-- lib/services/analytics-service.ts | 26 ++++++++++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/app/api/hackathons/[id]/register/route.ts b/app/api/hackathons/[id]/register/route.ts index 63de1e7b..58bb6e67 100644 --- a/app/api/hackathons/[id]/register/route.ts +++ b/app/api/hackathons/[id]/register/route.ts @@ -111,6 +111,15 @@ export async function POST( console.error('Error updating registered count:', updateError) } + // Track registration in analytics + try { + const { AnalyticsService } = await import('@/lib/services/analytics-service') + await AnalyticsService.trackHackathonRegistration(hackathon.id) + } catch (error) { + console.error('Error tracking registration in analytics:', error) + // Don't fail the registration if analytics tracking fails + } + return NextResponse.json({ success: true, message: 'Successfully registered for hackathon', diff --git a/components/dashboard/CompanyDashboard.tsx b/components/dashboard/CompanyDashboard.tsx index 262c2a37..f46407d6 100644 --- a/components/dashboard/CompanyDashboard.tsx +++ b/components/dashboard/CompanyDashboard.tsx @@ -105,6 +105,17 @@ export function CompanyDashboard({ company }: CompanyDashboardProps) { const eventsData = await eventsResponse.json() + // Fetch hackathons for total count + const hackathonsResponse = await fetch( + `/api/companies/${company.slug}/hackathons?limit=100` + ) + + if (!hackathonsResponse.ok) { + throw new Error('Failed to fetch hackathons') + } + + const hackathonsData = await hackathonsResponse.json() + // Calculate stats /* eslint-disable @typescript-eslint/no-explicit-any */ const pendingEvents = eventsData.events?.filter( @@ -115,6 +126,10 @@ export function CompanyDashboard({ company }: CompanyDashboardProps) { (e: any) => e.approval_status === 'approved' ) || [] + const approvedHackathons = hackathonsData.hackathons?.filter( + (h: any) => h.approval_status === 'approved' + ) || [] + const upcomingEventsData = eventsData.events ?.filter((e: any) => { const eventDate = new Date(e.date) @@ -122,12 +137,25 @@ export function CompanyDashboard({ company }: CompanyDashboardProps) { }) .sort((a: any, b: any) => new Date(a.date).getTime() - new Date(b.date).getTime()) .slice(0, 5) || [] + + // Calculate total registrations from both events and hackathons + const eventRegistrations = eventsData.events?.reduce( + (sum: number, e: any) => sum + (e.registered || 0), + 0 + ) || 0 + + const hackathonRegistrations = hackathonsData.hackathons?.reduce( + (sum: number, h: any) => sum + (h.registered || 0), + 0 + ) || 0 + + const totalRegistrations = eventRegistrations + hackathonRegistrations /* eslint-enable @typescript-eslint/no-explicit-any */ setStats({ totalEvents: approvedEvents.length, - totalHackathons: company.total_hackathons || 0, - totalRegistrations: analyticsData.summary?.total_registrations || 0, + totalHackathons: approvedHackathons.length, + totalRegistrations: totalRegistrations, totalViews: analyticsData.summary?.total_views || 0, totalClicks: analyticsData.summary?.total_clicks || 0, pendingApprovals: pendingEvents.length, diff --git a/lib/services/analytics-service.ts b/lib/services/analytics-service.ts index 04ea0e69..0b8d6433 100644 --- a/lib/services/analytics-service.ts +++ b/lib/services/analytics-service.ts @@ -116,6 +116,32 @@ export class AnalyticsService { } } + /** + * Track a registration for a hackathon + */ + static async trackHackathonRegistration(hackathonId: string): Promise { + const supabase = await createClient() + + const { data: hackathon, error: hackathonError } = await supabase + .from('hackathons') + .select('id, company_id') + .eq('id', hackathonId) + .single() + + if (hackathonError || !hackathon) { + throw new Error('Hackathon not found') + } + + // Update company analytics if hackathon has a company + if (hackathon.company_id) { + await this.incrementCompanyAnalytics( + hackathon.company_id, + 'total_registrations', + 1 + ) + } + } + /** * Track a view for a hackathon */ From 84e9288fac529d24ed39114edeeb3b77ec519395 Mon Sep 17 00:00:00 2001 From: Akshay Date: Tue, 18 Nov 2025 10:35:05 +0530 Subject: [PATCH 2/3] fix(hackathons): Improve registration RLS handling and add service role bypass - Use service role client to bypass RLS when updating hackathon registered count - Change `.single()` to `.maybeSingle()` for safer query handling in registration checks - Add comprehensive logging for registration/unregistration operations with count tracking - Implement fallback to direct delete if master registrations service fails - Add `updated_at` timestamp when incrementing/decrementing registered count - Fix registration status check to properly handle missing registrations - Improve error handling to not fail registration if count update fails - Add error code checking (PGRST116) to distinguish "not found" from actual errors - Update registration UI state immediately after successful registration - Ensure `setIsRegistered(false)` is called when user is not authenticated --- app/api/hackathons/[id]/register/route.ts | 117 ++++++++++++++++++---- app/hackathons/[id]/page.tsx | 43 +++++--- 2 files changed, 127 insertions(+), 33 deletions(-) diff --git a/app/api/hackathons/[id]/register/route.ts b/app/api/hackathons/[id]/register/route.ts index 58bb6e67..021a97b1 100644 --- a/app/api/hackathons/[id]/register/route.ts +++ b/app/api/hackathons/[id]/register/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' import { createClient } from '@/lib/supabase/server' +import { createClient as createSupabaseClient } from '@supabase/supabase-js' export async function POST( request: NextRequest, @@ -62,7 +63,7 @@ export async function POST( .eq('user_id', user.id) .eq('activity_type', 'hackathon') .eq('activity_id', hackathon.id.toString()) - .single() + .maybeSingle() if (existingRegistration) { return NextResponse.json( @@ -101,14 +102,36 @@ export async function POST( ) } - // Increment registered count - const { error: updateError } = await supabase + // Increment registered count using service role client to bypass RLS + console.log('Attempting to increment registered count:', { + hackathonId: hackathon.id, + currentCount: hackathon.registered, + newCount: (hackathon.registered || 0) + 1 + }) + + // Create admin client with service role key to bypass RLS + const supabaseAdmin = createSupabaseClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY! + ) + + const { data: updatedHackathon, error: updateError } = await supabaseAdmin .from('hackathons') - .update({ registered: (hackathon.registered || 0) + 1 }) + .update({ + registered: (hackathon.registered || 0) + 1, + updated_at: new Date().toISOString() + }) .eq('id', hackathon.id) + .select('registered') + .single() if (updateError) { console.error('Error updating registered count:', updateError) + // Don't fail the registration if count update fails + } else if (updatedHackathon) { + console.log('Successfully updated registered count to:', updatedHackathon.registered) + } else { + console.error('No hackathon returned from update') } // Track registration in analytics @@ -164,30 +187,86 @@ export async function DELETE( ) } - // 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()) + console.log('Attempting to unregister:', { + userId: user.id, + hackathonId: hackathon.id, + activityType: 'hackathon', + activityId: hackathon.id.toString() + }) - if (deleteError) { - console.error('Error deleting registration:', deleteError) - return NextResponse.json( - { error: 'Failed to unregister from hackathon' }, - { status: 500 } + // Use the master registrations service which should handle RLS properly + try { + const { masterRegistrationsService } = await import('@/lib/services/master-registrations') + await masterRegistrationsService.unregister( + user.id, + 'hackathon', + hackathon.id.toString() ) + console.log('Successfully unregistered using service') + } catch (serviceError) { + console.error('Error using service, trying direct delete:', serviceError) + + // Fallback to direct delete if service fails + const { data: deletedData, error: deleteError } = await supabase + .from('master_registrations') + .delete() + .eq('user_id', user.id) + .eq('activity_type', 'hackathon') + .eq('activity_id', hackathon.id.toString()) + .select() + + if (deleteError) { + console.error('Error deleting registration:', deleteError) + return NextResponse.json( + { error: 'Failed to unregister from hackathon' }, + { status: 500 } + ) + } + + console.log('Direct delete result:', { + deletedCount: deletedData?.length || 0, + deletedData: deletedData + }) + + if (!deletedData || deletedData.length === 0) { + console.log('No registration found to delete') + return NextResponse.json({ + success: true, + message: 'No active registration found', + }) + } } - // Decrement registered count - const { error: updateError } = await supabase + // Decrement registered count using service role client to bypass RLS + console.log('Attempting to decrement registered count:', { + hackathonId: hackathon.id, + currentCount: hackathon.registered, + newCount: Math.max(0, (hackathon.registered || 0) - 1) + }) + + // Create admin client with service role key to bypass RLS + const supabaseAdmin = createSupabaseClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY! + ) + + const { data: updatedHackathon, error: updateError } = await supabaseAdmin .from('hackathons') - .update({ registered: Math.max(0, (hackathon.registered || 0) - 1) }) + .update({ + registered: Math.max(0, (hackathon.registered || 0) - 1), + updated_at: new Date().toISOString() + }) .eq('id', hackathon.id) + .select('registered') + .single() if (updateError) { console.error('Error updating registered count:', updateError) + // Don't fail the unregistration if count update fails + } else if (updatedHackathon) { + console.log('Successfully updated registered count to:', updatedHackathon.registered) + } else { + console.log('No hackathon returned from update') } return NextResponse.json({ diff --git a/app/hackathons/[id]/page.tsx b/app/hackathons/[id]/page.tsx index 60c074f4..ed89ecb5 100644 --- a/app/hackathons/[id]/page.tsx +++ b/app/hackathons/[id]/page.tsx @@ -141,6 +141,7 @@ export default function HackathonDetailPage() { const checkRegistrationStatus = async () => { if (!isAuthenticated || !hackathon?.id) { setCheckingRegistration(false) + setIsRegistered(false) return } @@ -150,20 +151,30 @@ export default function HackathonDetailPage() { if (!user) { setCheckingRegistration(false) + setIsRegistered(false) return } - const { data } = await supabase + // Check for this specific hackathon registration + const { data, error } = await supabase .from('master_registrations') .select('id') .eq('user_id', user.id) .eq('activity_type', 'hackathon') .eq('activity_id', hackathon.id.toString()) - .single() + .maybeSingle() + // If there's an error (other than not found), log it + if (error && error.code !== 'PGRST116') { + console.error('Error checking registration:', error) + } + + // Set registration status based on whether data exists setIsRegistered(!!data) } catch (error) { console.error('Error checking registration:', error) + // On error, assume not registered + setIsRegistered(false) } finally { setCheckingRegistration(false) } @@ -187,19 +198,21 @@ export default function HackathonDetailPage() { }, }) + const result = await response.json() + if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.error || 'Failed to register') + throw new Error(result.error || 'Failed to register') } - toast.success('Successfully registered for the hackathon!') + // Update state immediately setIsRegistered(true) + setCheckingRegistration(false) + toast.success('Successfully registered for the hackathon!') - // Refresh hackathon data to update registered count - window.location.reload() + // Force a hard reload to clear any cached state + window.location.href = window.location.href } catch (err) { toast.error(err instanceof Error ? err.message : 'Failed to register') - } finally { setRegistering(false) } } @@ -213,19 +226,21 @@ export default function HackathonDetailPage() { method: 'DELETE', }) + const result = await response.json() + if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.error || 'Failed to unregister') + throw new Error(result.error || 'Failed to unregister') } - toast.success('Successfully unregistered from the hackathon') + // Update state immediately setIsRegistered(false) + setCheckingRegistration(false) + toast.success('Successfully unregistered from the hackathon') - // Refresh hackathon data to update registered count - window.location.reload() + // Force a hard reload to clear any cached state + window.location.href = window.location.href } catch (err) { toast.error(err instanceof Error ? err.message : 'Failed to unregister') - } finally { setRegistering(false) } } From 03dc6a84dec9fdeb97ca720df04d17561a127c1f Mon Sep 17 00:00:00 2001 From: Akshay Date: Tue, 18 Nov 2025 10:48:21 +0530 Subject: [PATCH 3/3] fix(hackathons): Use service role client for tracking clicks and views - Replace user-authenticated Supabase client with service role client to bypass RLS policies for click and view tracking - Add detailed logging for tracking operations including current and new counts - Update `updated_at` timestamp when incrementing click and view counts - Return updated click/view counts from database operations using `.select().single()` - Mark unused `request` parameter as `_request` to follow code conventions - Ensures analytics tracking works reliably regardless of user permissions --- app/api/hackathons/[id]/track-click/route.ts | 27 +++++++++++++++++--- app/api/hackathons/[id]/track-view/route.ts | 27 +++++++++++++++++--- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/app/api/hackathons/[id]/track-click/route.ts b/app/api/hackathons/[id]/track-click/route.ts index bfda8792..6090a98a 100644 --- a/app/api/hackathons/[id]/track-click/route.ts +++ b/app/api/hackathons/[id]/track-click/route.ts @@ -1,8 +1,9 @@ import { NextRequest, NextResponse } from 'next/server' import { createClient } from '@/lib/supabase/server' +import { createClient as createSupabaseClient } from '@supabase/supabase-js' export async function POST( - request: NextRequest, + _request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { try { @@ -23,11 +24,27 @@ export async function POST( ) } - // Increment click count - const { error: updateError } = await supabase + console.log('Tracking click for hackathon:', { + hackathonId: hackathon.id, + currentClicks: hackathon.clicks, + newClicks: (hackathon.clicks || 0) + 1 + }) + + // Increment click count using service role client to bypass RLS + const supabaseAdmin = createSupabaseClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY! + ) + + const { data: updatedHackathon, error: updateError } = await supabaseAdmin .from('hackathons') - .update({ clicks: (hackathon.clicks || 0) + 1 }) + .update({ + clicks: (hackathon.clicks || 0) + 1, + updated_at: new Date().toISOString() + }) .eq('id', hackathon.id) + .select('clicks') + .single() if (updateError) { console.error('Error updating click count:', updateError) @@ -37,6 +54,8 @@ export async function POST( ) } + console.log('Successfully updated click count to:', updatedHackathon?.clicks) + // Update company analytics if hackathon has a company if (hackathon.company_id) { const today = new Date().toISOString().split('T')[0] diff --git a/app/api/hackathons/[id]/track-view/route.ts b/app/api/hackathons/[id]/track-view/route.ts index 5ea6f8b6..34d42ab5 100644 --- a/app/api/hackathons/[id]/track-view/route.ts +++ b/app/api/hackathons/[id]/track-view/route.ts @@ -1,8 +1,9 @@ import { NextRequest, NextResponse } from 'next/server' import { createClient } from '@/lib/supabase/server' +import { createClient as createSupabaseClient } from '@supabase/supabase-js' export async function POST( - request: NextRequest, + _request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { try { @@ -23,11 +24,27 @@ export async function POST( ) } - // Increment view count - const { error: updateError } = await supabase + console.log('Tracking view for hackathon:', { + hackathonId: hackathon.id, + currentViews: hackathon.views, + newViews: (hackathon.views || 0) + 1 + }) + + // Increment view count using service role client to bypass RLS + const supabaseAdmin = createSupabaseClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY! + ) + + const { data: updatedHackathon, error: updateError } = await supabaseAdmin .from('hackathons') - .update({ views: (hackathon.views || 0) + 1 }) + .update({ + views: (hackathon.views || 0) + 1, + updated_at: new Date().toISOString() + }) .eq('id', hackathon.id) + .select('views') + .single() if (updateError) { console.error('Error updating view count:', updateError) @@ -37,6 +54,8 @@ export async function POST( ) } + console.log('Successfully updated view count to:', updatedHackathon?.views) + // Update company analytics if hackathon has a company if (hackathon.company_id) { const today = new Date().toISOString().split('T')[0]