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