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,
});

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,
});

// 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`);
};

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
63 changes: 46 additions & 17 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
}

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?.();
} catch {
// Error handled in hook
Expand Down Expand Up @@ -1081,15 +1097,17 @@ const SubmissionFormContent: React.FC<SubmissionFormContentProps> = ({
<div key='step-1' className='space-y-6'>
<div className='flex items-center justify-between'>
<div></div>
<Button
type='button'
variant='outline'
size='sm'
onClick={handleFillMockData}
className='border-gray-600 text-gray-300 hover:bg-gray-700 hover:text-white'
>
Fill with Mock Data
</Button>
{process.env.NODE_ENV === 'development' && (
<Button
type='button'
variant='outline'
size='sm'
onClick={handleFillMockData}
className='border-gray-600 text-gray-300 hover:bg-gray-700 hover:text-white'
>
Fill with Mock Data
</Button>
)}
</div>
<FormField
control={form.control}
Expand Down Expand Up @@ -1501,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 @@ -1566,6 +1593,8 @@ const SubmissionFormContent: React.FC<SubmissionFormContentProps> = ({
);
};

export { SubmissionFormContent };

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