Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 32 additions & 6 deletions app/(landing)/hackathons/[slug]/HackathonPageClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ 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 { useSubmission } from '@/hooks/hackathon/use-submission';
import { useAuthStatus } from '@/hooks/use-auth';
import { RegisterHackathonModal } from '@/components/hackathons/overview/RegisterHackathonModal';
import { HackathonBanner } from '@/components/hackathons/hackathonBanner';
import { HackathonNavTabs } from '@/components/hackathons/hackathonNavTabs';
Expand Down Expand Up @@ -45,6 +47,13 @@ export default function HackathonPageClient() {
refreshCurrentHackathon,
} = useHackathonData();

const { isAuthenticated } = useAuthStatus();

const { submission: mySubmission } = useSubmission({
hackathonSlugOrId: currentHackathon?.id || '',
autoFetch: !!currentHackathon && isAuthenticated,
});
Comment on lines +52 to +55

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

isInitializing is cleared before the requested hackathon is actually aligned.

await setCurrentHackathon(hackathonId) does not synchronize the provider state here, so the new useSubmission hook can still auto-fetch against whatever currentHackathon was already in context. On route-to-route transitions, that makes hasSubmitted vulnerable to the previous hackathon's submission until the provider catches up.

Also applies to: 322-341

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(landing)/hackathons/[slug]/HackathonPageClient.tsx around lines 52 -
55, The hook auto-fetches before the provider state aligns because useSubmission
is passed currentHackathon?.id and autoFetch true based on isAuthenticated; fix
by binding the hook to the intended hackathon id and gate autoFetch until the
provider is aligned: call useSubmission with hackathonSlugOrId set to the
route's hackathonId (the variable used in setCurrentHackathon) or add the
condition autoFetch: !!currentHackathon && currentHackathon.id === hackathonId
&& isAuthenticated, and apply the same change to the other occurrence (the block
referenced at lines 322-341) so the hook only auto-fetches when the provider's
currentHackathon actually matches the requested hackathon.


const timeline_Events = useTimelineEvents(currentHackathon, {
includeEndDate: false,
dateFormat: { month: 'short', day: 'numeric', year: 'numeric' },
Expand Down Expand Up @@ -232,7 +241,7 @@ export default function HackathonPageClient() {
// Registration status
const {
isRegistered,
hasSubmitted,
hasSubmitted: participantHasSubmitted,
setParticipant,
register: registerForHackathon,
} = useRegisterHackathon({
Expand All @@ -246,6 +255,8 @@ export default function HackathonPageClient() {
organizationId: undefined,
});

const hasSubmitted = !!mySubmission || participantHasSubmitted;

// Leave hackathon functionality
const { isLeaving, leave: leaveHackathon } = useLeaveHackathon({
hackathonSlugOrId: currentHackathon?.id || '',
Expand Down Expand Up @@ -296,7 +307,7 @@ export default function HackathonPageClient() {
};

const handleSubmitClick = () => {
router.push('?tab=submission');
router.push(`/hackathons/${currentHackathon?.slug}/submit`);
};

const handleViewSubmissionClick = () => {
Expand All @@ -308,10 +319,25 @@ export default function HackathonPageClient() {
};

// Set current hackathon on mount
const [isInitializing, setIsInitializing] = useState(true);

useEffect(() => {
if (hackathonId) {
setCurrentHackathon(hackathonId);
}
let isMounted = true;

const initHackathon = async () => {
if (hackathonId) {
await setCurrentHackathon(hackathonId);
}
if (isMounted) {
setIsInitializing(false);
}
};

initHackathon();

return () => {
isMounted = false;
};
}, [hackathonId, setCurrentHackathon]);

// Handle tab changes from URL
Expand Down Expand Up @@ -349,7 +375,7 @@ export default function HackathonPageClient() {
};

// Loading state
if (loading) {
if (loading || isInitializing) {
return <LoadingScreen />;
}

Expand Down
120 changes: 120 additions & 0 deletions app/(landing)/hackathons/[slug]/submit/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
'use client';

import { use, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useHackathonData } from '@/lib/providers/hackathonProvider';
import { useAuthStatus } from '@/hooks/use-auth';
import { useSubmission } from '@/hooks/hackathon/use-submission';
import { SubmissionFormContent } from '@/components/hackathons/submissions/SubmissionForm';
import LoadingScreen from '@/features/projects/components/CreateProjectModal/LoadingScreen';
import { Button } from '@/components/ui/button';
import { ArrowLeft } from 'lucide-react';
import { toast } from 'sonner';

export default function SubmitProjectPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const router = useRouter();
const { isAuthenticated, isLoading } = useAuthStatus();

const resolvedParams = use(params);
const hackathonSlug = resolvedParams.slug;

const {
currentHackathon,
loading: hackathonLoading,
setCurrentHackathon,
} = useHackathonData();

useEffect(() => {
if (hackathonSlug) {
setCurrentHackathon(hackathonSlug);
}
}, [hackathonSlug, setCurrentHackathon]);

const hackathonId = currentHackathon?.id || '';
const orgId = currentHackathon?.organizationId || undefined;

const {
submission: mySubmission,
isFetching: isLoadingMySubmission,
fetchMySubmission,
} = useSubmission({
hackathonSlugOrId: hackathonId || '',
autoFetch: isAuthenticated && !!hackathonId,
});
Comment on lines +31 to +47

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Gate this page on the requested hackathon, not just any hackathon in context.

setCurrentHackathon(hackathonSlug) runs after the first render, but useSubmission and the render gate both read currentHackathon immediately. On client-side transitions, that can still be the previous hackathon, so this page can fetch and prefill the wrong submission until the provider catches up.

Suggested fix
   const {
     currentHackathon,
     loading: hackathonLoading,
     setCurrentHackathon,
   } = useHackathonData();
+
+  const isCurrentHackathonReady =
+    currentHackathon?.slug === hackathonSlug;

   const hackathonId = currentHackathon?.id || '';
   const orgId = currentHackathon?.organizationId || undefined;

   const {
     submission: mySubmission,
     isFetching: isLoadingMySubmission,
     fetchMySubmission,
   } = useSubmission({
     hackathonSlugOrId: hackathonId || '',
-    autoFetch: isAuthenticated && !!hackathonId,
+    autoFetch: isAuthenticated && isCurrentHackathonReady && !!hackathonId,
   });
@@
-  if (
-    isLoading ||
-    hackathonLoading ||
-    isLoadingMySubmission ||
-    !currentHackathon
-  ) {
+  if (
+    isLoading ||
+    hackathonLoading ||
+    isLoadingMySubmission ||
+    !isCurrentHackathonReady
+  ) {
     return <LoadingScreen />;
   }

Also applies to: 73-80

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(landing)/hackathons/[slug]/submit/page.tsx around lines 31 - 47, The
page currently uses currentHackathon immediately, causing stale data on client
transitions; instead gate on the requested hackathon slug and avoid using stale
currentHackathon for fetching. Change logic so you only run useSubmission and
render when the provider has been synchronized to the requested hackathon: check
that currentHackathon?.slug === hackathonSlug (or, alternatively, pass
hackathonSlug directly into useSubmission as hackathonSlugOrId) and only enable
autoFetch when that match is true; keep setCurrentHackathon(hackathonSlug) in
useEffect but prevent useSubmission from using currentHackathon
(hackathonId/orgId) until the slug matches. This uses the existing symbols
setCurrentHackathon, currentHackathon, hackathonSlug, useSubmission, and
hackathonId to locate and fix the code paths.


// Authentication check
useEffect(() => {
if (!isLoading && !isAuthenticated) {
toast.error('You must be logged in to submit a project');
router.push(
`/auth?mode=signin&callbackUrl=/hackathons/${hackathonSlug}/submit`
);
}
}, [isAuthenticated, isLoading, router, hackathonSlug]);

const handleClose = () => {
router.push(`/hackathons/${hackathonSlug}`);
};

const handleSuccess = () => {
fetchMySubmission();
toast.success(
mySubmission
? 'Submission updated successfully!'
: 'Project submitted successfully!'
);
router.push(`/hackathons/${hackathonSlug}?tab=submission`);
Comment on lines +63 to +70

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove the extra success toast.

useSubmission.create() and update() already call toast.success(...), so handleSuccess shows a second notification on every successful submit/edit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(landing)/hackathons/[slug]/submit/page.tsx around lines 63 - 70,
handleSuccess currently triggers a second success toast even though
useSubmission.create() and update() already call toast.success; remove the
toast.success(...) call from handleSuccess and keep it to just call
fetchMySubmission() and navigate via
router.push(`/hackathons/${hackathonSlug}?tab=submission`), referencing the
existing handleSuccess, fetchMySubmission, mySubmission and router.push symbols
when making the change.

};

if (
isLoading ||
hackathonLoading ||
isLoadingMySubmission ||
!currentHackathon
) {
return <LoadingScreen />;
}

return (
<div className='min-h-screen bg-black px-5 py-5 text-white md:px-[50px] lg:px-[100px]'>
<div className='mx-auto max-w-[1200px] pb-10'>
<Button
variant='ghost'
className='mb-6 pl-0 text-gray-400 hover:text-white'
onClick={handleClose}
>
<ArrowLeft className='mr-2 h-4 w-4' />
Back to Hackathon
</Button>

<div className='min-h-[700px] overflow-hidden rounded-xl border border-gray-800 bg-gray-900/50 shadow-2xl'>
<SubmissionFormContent
hackathonSlugOrId={hackathonId}
organizationId={orgId}
submissionId={mySubmission?.id}
initialData={
mySubmission
? {
projectName: mySubmission.projectName,
category: mySubmission.category,
description: mySubmission.description,
logo: mySubmission.logo,
videoUrl: mySubmission.videoUrl,
introduction: mySubmission.introduction,
links: mySubmission.links,
participationType: (mySubmission as any).participationType,
}
: undefined
}
onSuccess={handleSuccess}
onClose={handleClose}
/>
</div>
</div>
</div>
);
}
6 changes: 3 additions & 3 deletions components/hackathons/hackathonBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -315,14 +315,14 @@ export function HackathonBanner({
{status === 'ongoing' &&
isRegistered &&
hasSubmitted &&
onViewSubmissionClick && (
onSubmitClick && (
<Button
onClick={onViewSubmissionClick}
onClick={onSubmitClick}
variant='outline'
className='w-full border-gray-700 bg-transparent py-5 text-gray-300 hover:bg-gray-900'
>
<FileText className='mr-2 h-4 w-4' />
View Submission
Edit Submission
</Button>
)}

Expand Down
25 changes: 11 additions & 14 deletions components/hackathons/hackathonStickyCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -238,20 +238,17 @@ export function HackathonStickyCard(props: HackathonStickyCardProps) {
</Button>
)}

{/* View Submission Button */}
{status === 'ongoing' &&
isRegistered &&
hasSubmitted &&
onViewSubmissionClick && (
<Button
onClick={onViewSubmissionClick}
variant='outline'
className='w-full border-gray-700 py-4 text-sm text-gray-300 hover:bg-gray-900'
>
<FileText className='mr-1.5 h-3.5 w-3.5' />
View Submission
</Button>
)}
{/* Edit / View Submission Button */}
{status === 'ongoing' && isRegistered && hasSubmitted && (
<Button
onClick={onSubmitClick}
variant='outline'
className='w-full border-gray-700 py-4 text-sm text-gray-300 hover:bg-gray-900'
>
<FileText className='mr-1.5 h-3.5 w-3.5' />
Edit Submission
</Button>
)}

{/* Find Team Button */}
{status === 'ongoing' &&
Expand Down
43 changes: 35 additions & 8 deletions components/hackathons/submissions/SubmissionForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ interface SubmissionFormContentProps {
initialData?: Partial<SubmissionFormDataLocal>;
submissionId?: string;
onSuccess?: () => void;
onClose?: () => void;
}

const INITIAL_STEPS: Step[] = [
Expand Down Expand Up @@ -198,8 +199,19 @@ const SubmissionFormContent: React.FC<SubmissionFormContentProps> = ({
initialData,
submissionId,
onSuccess,
onClose,
}) => {
const { collapse, isExpanded: open } = useExpandableScreen();
// Use context carefully since it might not be available when used standalone
let collapse = () => {};
let open = true;
try {
const expandableCtx = useExpandableScreen();
collapse = expandableCtx.collapse;
open = expandableCtx.isExpanded;
} catch (e) {
// Standalone mode, not in ExpandableScreen
}
Comment on lines +204 to +213

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

fd -t f "SubmissionForm.tsx" -o "submission" --maxdepth 5

Repository: boundlessfi/boundless

Length of output: 204


🏁 Script executed:

find . -name "SubmissionForm.tsx" -o -name "*page.tsx" | grep -E "(submit|submission)" | head -20

Repository: boundlessfi/boundless

Length of output: 291


🏁 Script executed:

git ls-files | grep -i submission | head -20

Repository: boundlessfi/boundless

Length of output: 1340


🏁 Script executed:

wc -l components/hackathons/submissions/SubmissionForm.tsx

Repository: boundlessfi/boundless

Length of output: 122


🏁 Script executed:

sed -n '200,220p' components/hackathons/submissions/SubmissionForm.tsx

Repository: boundlessfi/boundless

Length of output: 740


🏁 Script executed:

sed -n '780,800p' components/hackathons/submissions/SubmissionForm.tsx

Repository: boundlessfi/boundless

Length of output: 505


🏁 Script executed:

cat -n components/hackathons/submissions/SubmissionForm.tsx | head -50

Repository: boundlessfi/boundless

Length of output: 1571


🏁 Script executed:

cat -n app/'(landing)'/hackathons/'[slug]'/submit/page.tsx

Repository: boundlessfi/boundless

Length of output: 4564


🏁 Script executed:

grep -n "SubmissionForm\|SubmissionFormContent" app/'(landing)'/hackathons/'[slug]'/submit/page.tsx -B 5 -A 10

Repository: boundlessfi/boundless

Length of output: 1500


🏁 Script executed:

grep -n "export\|SubmissionFormContent" components/hackathons/submissions/SubmissionForm.tsx | head -30

Repository: boundlessfi/boundless

Length of output: 405


🏁 Script executed:

sed -n '1598,1617p' components/hackathons/submissions/SubmissionForm.tsx

Repository: boundlessfi/boundless

Length of output: 648


🏁 Script executed:

sed -n '190,250p' components/hackathons/submissions/SubmissionForm.tsx

Repository: boundlessfi/boundless

Length of output: 1819


🏁 Script executed:

grep -r "SubmissionScreenWrapper\|SubmissionFormContent" --include="*.tsx" --include="*.ts" | grep -v node_modules | head -20

Repository: boundlessfi/boundless

Length of output: 1112


🏁 Script executed:

grep -n "useExpandableScreen" components/hackathons/submissions/SubmissionForm.tsx

Repository: boundlessfi/boundless

Length of output: 143


🏁 Script executed:

cat -n biome.json 2>/dev/null || cat -n biomejs.json 2>/dev/null || echo "No Biome config found"

Repository: boundlessfi/boundless

Length of output: 2350


Move useExpandableScreen() call to SubmissionScreenWrapper and pass context values as props.

The try/catch around useExpandableScreen() at lines 208–213 violates React Rules of Hooks and will fail Biome's lint/correctness/useHookAtTopLevel check. Hooks must be called unconditionally at the top level of components, not within control-flow branches.

The safe solution: Since SubmissionScreenWrapper (lines 1598–1617) is already inside the ExpandableScreen context, have it call useExpandableScreen() safely and pass collapse and isExpanded to SubmissionFormContent via props. For standalone usage (e.g., the submit page), the component already receives onClose and can ignore the context values.

Additionally, the post-submit callback sequence at lines 788–793 calls both onClose and onSuccess in the standalone page, firing two navigation actions in sequence. Pass the context values via props to avoid needing fallback logic.

🧰 Tools
🪛 Biome (2.4.4)

[error] 208-208: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

(lint/correctness/useHookAtTopLevel)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/hackathons/submissions/SubmissionForm.tsx` around lines 204 - 213,
The call to useExpandableScreen() must be removed from SubmissionForm and moved
into SubmissionScreenWrapper: call useExpandableScreen() unconditionally inside
SubmissionScreenWrapper, extract collapse and isExpanded, and pass them as props
(e.g., collapse, isExpanded) down to SubmissionFormContent/SubmissionForm;
delete the try/catch hook usage in SubmissionForm and read collapse/isExpanded
from props instead. Also update the post-submit sequence in SubmissionForm (the
code that currently calls onClose and onSuccess) to use the passed
collapse/isExpanded values to decide navigation (prefer calling collapse() when
isExpanded is true, otherwise call onClose), and then call onSuccess once—this
avoids firing two navigation actions on standalone pages. Ensure prop names
match between SubmissionScreenWrapper and SubmissionFormContent/SubmissionForm.


const { currentHackathon } = useHackathonData();
const { user } = useAuthStatus();

Expand Down Expand Up @@ -773,7 +785,11 @@ const SubmissionFormContent: React.FC<SubmissionFormContentProps> = ({
} else {
await create(submissionData);
}
collapse();
if (onClose) {
onClose();
} else {
collapse();
}
onSuccess?.();
Comment on lines +788 to 793

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't call onClose and onSuccess on the same success path.

app/(landing)/hackathons/[slug]/submit/page.tsx passes both callbacks, so this branch now fires two route changes after a successful submit: one from onClose() and another from onSuccess(). That can add an extra history entry or flash the intermediate hackathon page.

Suggested fix
-      if (onClose) {
-        onClose();
-      } else {
-        collapse();
-      }
-      onSuccess?.();
+      if (onSuccess) {
+        onSuccess();
+      } else if (onClose) {
+        onClose();
+      } else {
+        collapse();
+      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/hackathons/submissions/SubmissionForm.tsx` around lines 788 - 793,
The current success path calls both onClose() and onSuccess(), causing duplicate
route changes; change the logic in SubmissionForm so that onSuccess is only
invoked when onClose is not provided (i.e., keep onClose() as the sole action
when present, and call collapse() followed by onSuccess?.() in the else branch).
Update the block that currently references onClose, collapse, and onSuccess so
only one navigation-related callback runs per successful submit.

} catch {
// Error handled in hook
Expand Down Expand Up @@ -1503,22 +1519,31 @@ const SubmissionFormContent: React.FC<SubmissionFormContentProps> = ({
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className='flex flex-1 gap-8 overflow-y-auto px-10 py-6'
className='flex flex-1 flex-col gap-6 overflow-y-auto px-4 py-6 sm:px-10 md:flex-row md:gap-8'
>
<div className='sticky top-0 h-fit'>
<div className='mt-4 h-fit w-full md:sticky md:top-0 md:mt-0 md:w-auto'>
<Stepper steps={steps} />
</div>
<div className='flex flex-1 flex-col space-y-6'>
<div className='flex w-full flex-1 flex-col space-y-6'>
{renderStepContent()}
<div className='mt-auto flex justify-between pt-6 pb-6'>
<Button
type='button'
variant='outline'
onClick={handleBack}
disabled={currentStep === 0}
onClick={() => {
if (currentStep > 0) {
handleBack();
} else {
if (onClose) {
onClose();
} else {
collapse();
}
}
}}
className='border-gray-700 text-white hover:bg-gray-800'
>
Back
{currentStep === 0 ? 'Cancel' : 'Back'}
</Button>
{currentStep < steps.length - 1 ? (
<Button
Expand Down Expand Up @@ -1568,6 +1593,8 @@ const SubmissionFormContent: React.FC<SubmissionFormContentProps> = ({
);
};

export { SubmissionFormContent };

interface SubmissionScreenWrapperProps extends SubmissionFormContentProps {
children: React.ReactNode;
}
Expand Down
Loading
Loading