diff --git a/app/(landing)/hackathons/[slug]/page.tsx b/app/(landing)/hackathons/[slug]/page.tsx index a13575eb3..07be7635c 100644 --- a/app/(landing)/hackathons/[slug]/page.tsx +++ b/app/(landing)/hackathons/[slug]/page.tsx @@ -1,9 +1,10 @@ 'use client'; -import { useState, useEffect, useMemo } from 'react'; +import { useState, useEffect, useMemo, useCallback } from 'react'; import { useRouter, useSearchParams, useParams } from 'next/navigation'; import { useHackathonData } from '@/lib/providers/hackathonProvider'; import { useRegisterHackathon } from '@/hooks/hackathon/use-register-hackathon'; +import { useLeaveHackathon } from '@/hooks/hackathon/use-leave-hackathon'; import { RegisterHackathonModal } from '@/components/hackathons/overview/RegisterHackathonModal'; import { HackathonBanner } from '@/components/hackathons/hackathonBanner'; @@ -16,6 +17,9 @@ import { HackathonDiscussions } from '@/components/hackathons/discussion/comment import { TeamFormationTab } from '@/components/hackathons/team-formation/TeamFormationTab'; import LoadingScreen from '@/components/landing-page/project/CreateProjectModal/LoadingScreen'; import { useTimelineEvents } from '@/hooks/hackathon/use-timeline-events'; +import { toast } from 'sonner'; +import { Participant } from '@/lib/api/hackathons'; +// import { HackathonResources } from '@/components/hackathons/resources/resources'; export default function HackathonPage() { const router = useRouter(); @@ -28,14 +32,16 @@ export default function HackathonPage() { submissions, loading, setCurrentHackathon, + refreshCurrentHackathon, } = useHackathonData(); + const timeline_Events = useTimelineEvents(currentHackathon, { includeEndDate: false, dateFormat: { month: 'short', day: 'numeric', year: 'numeric' }, }); + const hackathonTabs = useMemo(() => { const hasParticipants = participants.length > 0; - // const hasSubmissions = submissions.filter(p => p.status === 'Approved').length > 0; const tabs = [ { id: 'overview', label: 'Overview' }, @@ -81,19 +87,37 @@ export default function HackathonPage() { const [activeTab, setActiveTab] = useState('overview'); const [showRegisterModal, setShowRegisterModal] = useState(false); - // Registration logic - const { isRegistered, hasSubmitted, checkStatus } = useRegisterHackathon({ + // Function to refresh all hackathon data + const refreshHackathonData = useCallback(async () => { + if (hackathonId && refreshCurrentHackathon) { + await refreshCurrentHackathon(); + } + }, [hackathonId, refreshCurrentHackathon]); + + // Registration logic - use the updated hook with setters + const { + isRegistered, + hasSubmitted, + checkStatus, + // isChecking, + setIsRegistered, + setParticipant, + } = useRegisterHackathon({ hackathonSlugOrId: hackathonId, - organizationId: undefined, // organizationId not available in currentHackathon type + organizationId: undefined, autoCheck: !!hackathonId, }); + // Leave hackathon logic + const { isLeaving, leave: leaveHackathon } = useLeaveHackathon({ + hackathonSlugOrId: hackathonId, + organizationId: undefined, + }); + const isEnded = useMemo(() => { if (!currentHackathon?.deadline) return false; - const deadline = new Date(currentHackathon.deadline); const now = new Date(); - return now > deadline; }, [currentHackathon?.deadline]); @@ -111,8 +135,33 @@ export default function HackathonPage() { setShowRegisterModal(true); }; - const handleRegisterSuccess = () => { - checkStatus(); + const handleLeaveClick = async () => { + try { + setIsRegistered(false); + setParticipant(null); + + await leaveHackathon(); + + refreshHackathonData(); + + router.push('?tab=overview'); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'Failed to leave hackathon'; + toast.error(errorMessage); + setIsRegistered(true); + checkStatus(); + } + }; + + const handleRegisterSuccess = async (participantData: Participant) => { + // IMMEDIATELY update state with the returned participant data + setIsRegistered(true); + setParticipant(participantData); + + // Refresh hackathon data in background (participants count) + await refreshHackathonData(); + router.push('?tab=submission'); }; @@ -151,6 +200,7 @@ export default function HackathonPage() { if (loading) { return ; } + if (!currentHackathon) { return (
@@ -187,6 +237,8 @@ export default function HackathonPage() { registrationDeadline={currentHackathon.registrationDeadline} isTeamFormationEnabled={isTeamFormationEnabled} onJoinClick={handleJoinClick} + onLeaveClick={handleLeaveClick} + isLeaving={isLeaving} onSubmitClick={handleSubmitClick} onViewSubmissionClick={handleViewSubmissionClick} onFindTeamClick={handleFindTeamClick} @@ -199,7 +251,7 @@ export default function HackathonPage() { onOpenChange={setShowRegisterModal} hackathonSlugOrId={hackathonId} organizationId={undefined} - onSuccess={handleRegisterSuccess} + onSuccess={handleRegisterSuccess} // Now passes participant data participantType={ (currentHackathon?.participantType as | 'team' @@ -232,9 +284,6 @@ export default function HackathonPage() { {activeTab === 'participants' && participants.length > 0 && ( )} - {/* {activeTab === 'resources' && ( - - )} */} {activeTab === 'submission' && ( void; onViewSubmissionClick?: () => void; onFindTeamClick?: () => void; + onLeaveClick?: () => void; + isLeaving?: boolean; } interface TimeRemaining { @@ -125,10 +127,12 @@ export function HackathonBanner({ isTeamFormationEnabled = false, registrationDeadlinePolicy, registrationDeadline, + isLeaving, onJoinClick, onSubmitClick, onViewSubmissionClick, onFindTeamClick, + onLeaveClick, }: HackathonBannerProps) { const [timeRemaining, setTimeRemaining] = useState({ days: 0, @@ -381,11 +385,14 @@ export function HackathonBanner({ if (isRegistered) { return ( ); } diff --git a/components/hackathons/overview/RegisterHackathonModal.tsx b/components/hackathons/overview/RegisterHackathonModal.tsx index 99e57c597..8627a3ad6 100644 --- a/components/hackathons/overview/RegisterHackathonModal.tsx +++ b/components/hackathons/overview/RegisterHackathonModal.tsx @@ -55,7 +55,7 @@ interface RegisterHackathonModalProps { hackathonSlugOrId: string; organizationId?: string; participantType: 'individual' | 'team' | 'team_or_individual'; - onSuccess?: () => void; + onSuccess?: (participantData?: any) => void; } export function RegisterHackathonModal({ @@ -107,7 +107,7 @@ export function RegisterHackathonModal({ const onSubmit = async (data: RegisterFormData) => { try { - await registerForHackathon({ + const participantData = await registerForHackathon({ participationType: data.participationType, teamName: data.participationType === 'team' ? data.teamName : undefined, teamMembers: @@ -119,7 +119,9 @@ export function RegisterHackathonModal({ form.reset(); setTeamMembers([]); setTeamMemberEmail(''); - onSuccess?.(); + + // Pass the participant data to onSuccess + onSuccess?.(participantData); } catch { // Error handled in hook } diff --git a/components/organization/OrganizationContent.tsx b/components/organization/OrganizationContent.tsx index 078080752..b061c74e8 100644 --- a/components/organization/OrganizationContent.tsx +++ b/components/organization/OrganizationContent.tsx @@ -384,7 +384,7 @@ export default function OrganizationContent() { ) : (
-
+
{/* Decorative background */}
diff --git a/hooks/hackathon/use-leave-hackathon.ts b/hooks/hackathon/use-leave-hackathon.ts new file mode 100644 index 000000000..95ee68a42 --- /dev/null +++ b/hooks/hackathon/use-leave-hackathon.ts @@ -0,0 +1,60 @@ +'use client'; + +import { useState, useCallback } from 'react'; +import { leaveHackathon } from '@/lib/api/hackathons'; +import { useAuthStatus } from '@/hooks/use-auth'; +import { toast } from 'sonner'; + +interface UseLeaveHackathonOptions { + hackathonSlugOrId: string; + organizationId?: string; +} + +export function useLeaveHackathon({ + hackathonSlugOrId, + organizationId, +}: UseLeaveHackathonOptions) { + const { isAuthenticated } = useAuthStatus(); + const [isLeaving, setIsLeaving] = useState(false); + const [error, setError] = useState(null); + + const leave = useCallback(async () => { + if (!isAuthenticated) { + toast.error('Please sign in to leave hackathons'); + throw new Error('Authentication required'); + } + + if (!hackathonSlugOrId) { + toast.error('Hackathon ID is required'); + throw new Error('Hackathon ID is required'); + } + + setIsLeaving(true); + setError(null); + + try { + const response = await leaveHackathon(hackathonSlugOrId, organizationId); + + if (response.success) { + toast.success('Successfully left the hackathon'); + return response.data; + } else { + throw new Error(response.message || 'Failed to leave hackathon'); + } + } catch (err) { + const errorMessage = + err instanceof Error ? err.message : 'Failed to leave hackathon'; + setError(errorMessage); + toast.error(errorMessage); + throw err; + } finally { + setIsLeaving(false); + } + }, [hackathonSlugOrId, organizationId, isAuthenticated]); + + return { + isLeaving, + error, + leave, + }; +} diff --git a/hooks/hackathon/use-register-hackathon.ts b/hooks/hackathon/use-register-hackathon.ts index 4f92d8d8b..8d7f19431 100644 --- a/hooks/hackathon/use-register-hackathon.ts +++ b/hooks/hackathon/use-register-hackathon.ts @@ -27,11 +27,13 @@ export function useRegisterHackathon({ const [isRegistering, setIsRegistering] = useState(false); const [isChecking, setIsChecking] = useState(false); const [error, setError] = useState(null); + const [hasCheckedInitially, setHasCheckedInitially] = useState(false); const checkStatus = useCallback(async () => { if (!isAuthenticated || !hackathonSlugOrId) { setIsRegistered(false); setParticipant(null); + setHasCheckedInitially(true); return; } @@ -51,6 +53,7 @@ export function useRegisterHackathon({ setIsRegistered(false); setParticipant(null); } + setHasCheckedInitially(true); } catch (err) { const errorMessage = err instanceof Error @@ -59,6 +62,7 @@ export function useRegisterHackathon({ setError(errorMessage); setIsRegistered(false); setParticipant(null); + setHasCheckedInitially(true); } finally { setIsChecking(false); } @@ -87,6 +91,7 @@ export function useRegisterHackathon({ ); if (response.success && response.data) { + // IMMEDIATELY update state - don't wait for checkStatus setIsRegistered(true); setParticipant(response.data); toast.success('Successfully registered for hackathon!'); @@ -116,6 +121,7 @@ export function useRegisterHackathon({ } else if (!isAuthenticated) { setIsRegistered(false); setParticipant(null); + setHasCheckedInitially(true); } }, [autoCheck, isAuthenticated, hackathonSlugOrId, checkStatus]); @@ -127,6 +133,10 @@ export function useRegisterHackathon({ error, register, checkStatus, + hasCheckedInitially, + // Expose setters for immediate updates + setIsRegistered, + setParticipant, hasSubmitted: participant?.submission?.status === 'submitted', }; } diff --git a/lib/api/hackathons.ts b/lib/api/hackathons.ts index 709a6c503..5ee29ff20 100644 --- a/lib/api/hackathons.ts +++ b/lib/api/hackathons.ts @@ -673,6 +673,19 @@ export interface GetJudgingSubmissionsResponse message: string; } +export interface LeaveHackathonResponse + extends ApiResponse<{ + teamCleanedUp: boolean; + teamId?: string; + }> { + success: true; + data: { + teamCleanedUp: boolean; + teamId?: string; + }; + message: string; +} + export interface GetSubmissionScoresResponse extends ApiResponse { success: true; @@ -1478,6 +1491,28 @@ export const registerForHackathon = async ( return res.data; }; +/** + * Leave a hackathon + * Supports both slug-based (public) and organization/hackathon ID (authenticated) endpoints + */ +export const leaveHackathon = async ( + hackathonSlugOrId: string, + organizationId?: string +): Promise => { + let url: string; + + // If organizationId is provided, use authenticated endpoint + if (organizationId) { + url = `/organizations/${organizationId}/hackathons/${hackathonSlugOrId}/leave`; + } else { + // Otherwise, use public slug-based endpoint + url = `/hackathons/${hackathonSlugOrId}/leave`; + } + + const res = await api.delete(url); + return res.data; +}; + /** * Check registration status for a hackathon * Returns participant data if registered, null otherwise