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
30 changes: 30 additions & 0 deletions app/(landing)/hackathons/[slug]/hackathon-detail-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
Hackathon Detail Page Redesign
Issue: #414

Figma Design
https://www.figma.com/design/EMNGAQl1SGObXcsoa24krt/Boundless_Project-Details?node-id=0-1&t=A1ywRcn60Xyw0X6h-1

This design proposes a cleaner and more professional UI/UX for the hackathon detail page.

Included in the Figma file:
- Desktop layout
- Mobile layout
- Banner / hero placement proposal
- Redesigned hero section
- Sticky sidebar card
- Tab navigation
- All tab layouts (overview, participants, resources, announcements, submissions, discussions, find team, winners)
- Loading state
- Hackathon not found state

Banner Placement Proposal
The hackathon banner is placed as a full-width hero image at the top of the page, allowing it to visually represent the hackathon and improve page identity.

The sidebar becomes a compact summary card with key information and actions.

Design Goals
- Simpler UI and improved visual hierarchy
- Clear primary actions (Join, Submit, View Submission)
- Consistent spacing and typography
- Better mobile usability
- Professional and product-quality look
140 changes: 140 additions & 0 deletions app/(landing)/hackathons/[slug]/submit/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
'use client';

import { use, useEffect, useState } 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`);
};

const [hasInitialLoaded, setHasInitialLoaded] = useState(false);

useEffect(() => {
if (!isLoading && !hackathonLoading && !isLoadingMySubmission) {
setHasInitialLoaded(true);
}
}, [isLoading, hackathonLoading, isLoadingMySubmission]);

if (!hasInitialLoaded) {
return <LoadingScreen />;
}

if (!currentHackathon) {
return (
<div className='flex min-h-screen flex-col items-center justify-center bg-black px-5 py-5 text-white'>
<h1 className='mb-4 text-2xl font-bold'>Hackathon Not Found</h1>
<Button
onClick={handleClose}
variant='ghost'
className='text-gray-400 hover:text-white'
>
<ArrowLeft className='mr-2 h-4 w-4' />
Go Back
</Button>
</div>
);
}
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,
teamName: (mySubmission as any).teamName,
teamMembers: (mySubmission as any).teamMembers,
}
: undefined
}
onSuccess={handleSuccess}
onClose={handleClose}
/>
</div>
</div>
</div>
);
}
32 changes: 32 additions & 0 deletions app/(landing)/newsletter/confirm/error/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
'use client';
import { useSearchParams } from 'next/navigation';
import Link from 'next/link';
import { Suspense } from 'react';

const msgs: Record<string, string> = {
expired: 'This confirmation link has expired.',
invalid: 'This confirmation link is invalid or already used.',
};

function Content() {
const p = useSearchParams();
return (
<main className='flex min-h-screen flex-col items-center justify-center gap-4 text-white'>
<h1 className='text-3xl font-semibold'>Confirmation failed</h1>
<p className='text-[#D9D9D9]'>
{msgs[p.get('reason') ?? ''] ?? 'An unexpected error occurred.'}
</p>
<Link href='/' className='text-[#A7F950] underline'>
Back to home
</Link>
</main>
);
}

export default function Page() {
return (
<Suspense>
<Content />
</Suspense>
);
}
14 changes: 14 additions & 0 deletions app/(landing)/newsletter/confirmed/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Link from 'next/link';
export default function NewsletterConfirmedPage() {
return (
<main className='flex min-h-screen flex-col items-center justify-center gap-4 text-white'>
<h1 className='text-3xl font-semibold'>You&apos;re subscribed! 🎉</h1>
<p className='text-[#D9D9D9]'>
Your subscription has been confirmed. Welcome aboard!
</p>
<Link href='/' className='text-[#A7F950] underline'>
Back to home
</Link>
</main>
);
}
31 changes: 31 additions & 0 deletions app/(landing)/newsletter/unsubscribe/error/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use client';
import { useSearchParams } from 'next/navigation';
import Link from 'next/link';
import { Suspense } from 'react';

const msgs: Record<string, string> = {
invalid: 'This unsubscribe link is invalid or has already been used.',
};

function Content() {
const p = useSearchParams();
return (
<main className='flex min-h-screen flex-col items-center justify-center gap-4 text-white'>
<h1 className='text-3xl font-semibold'>Unsubscribe failed</h1>
<p className='text-[#D9D9D9]'>
{msgs[p.get('reason') ?? ''] ?? 'An unexpected error occurred.'}
</p>
<Link href='/' className='text-[#A7F950] underline'>
Back to home
</Link>
</main>
);
}

export default function Page() {
return (
<Suspense>
<Content />
</Suspense>
);
}
14 changes: 14 additions & 0 deletions app/(landing)/newsletter/unsubscribed/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Link from 'next/link';
export default function NewsletterUnsubscribedPage() {
return (
<main className='flex min-h-screen flex-col items-center justify-center gap-4 text-white'>
<h1 className='text-3xl font-semibold'>You&apos;ve been unsubscribed</h1>
<p className='text-[#D9D9D9]'>
You won&apos;t receive any more emails from us.
</p>
<Link href='/' className='text-[#A7F950] underline'>
Back to home
</Link>
</main>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,23 @@ import {
import { Switch } from '@/components/ui/switch';
import { reportError } from '@/lib/error-reporting';

/** Strip Markdown to plain text for list preview (headings, bold, links, etc.). */
function stripMarkdown(md: string): string {
if (!md || typeof md !== 'string') return '';
return md
.replace(/!\[[^\]]*\]\([^)]*\)/g, '')
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
.replace(/#{1,6}\s*/g, '')
.replace(/\*\*([^*]+)\*\*/g, '$1')
.replace(/\*([^*]+)\*/g, '$1')
.replace(/__([^_]+)__/g, '$1')
.replace(/_([^_]+)_/g, '$1')
.replace(/`([^`]+)`/g, '$1')
.replace(/<[^>]*>/g, '')
.replace(/\n+/g, ' ')
.trim();
}

export default function AnnouncementPage() {
const params = useParams();
const organizationId = params.id as string;
Expand Down Expand Up @@ -298,7 +315,7 @@ export default function AnnouncementPage() {
)}
</div>
<p className='line-clamp-2 text-sm text-zinc-400'>
{item.content.replace(/<[^>]*>/g, '')}
{stripMarkdown(item.content)}
</p>
<div className='flex items-center gap-4 text-xs text-zinc-500'>
<span>
Expand Down
Loading
Loading