Skip to content

Commit 4fa41f9

Browse files
authored
Feat/new hackathon layout (#486)
* feat: Implement comprehensive hackathon detail page with new header, sidebar, and tabbed content sections including teams, announcements, winners, participants, and resources. * fix: update coderabit * feat: Enhance hackathon team invitation flow, recruitment post management, and dynamic tab visibility. * feat: Integrate AuthModalProvider for authentication handling and enhance messaging features across hackathon components * refactor: Update UI components to use primary color scheme for consistency across landing pages and hackathon sections
1 parent 746f040 commit 4fa41f9

146 files changed

Lines changed: 8182 additions & 1641 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/(landing)/blog/[slug]/not-found.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,7 @@ export default function BlogNotFound() {
2828
</Link>
2929
</Button>
3030

31-
<Button
32-
asChild
33-
className='bg-[#A7F950] text-black hover:bg-[#A7F950]/90'
34-
>
31+
<Button asChild className='bg-primary hover:bg-primary/90 text-black'>
3532
<Link href='/'>
3633
<Home className='mr-2 h-4 w-4' />
3734
Go Home

app/(landing)/code-of-conduct/CodeOfConductContent.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ const CodeOfConductContent = () => {
192192
placeholder='Search keyword'
193193
value={searchQuery}
194194
onChange={e => setSearchQuery(e.target.value)}
195-
className='bg-background-card w-full rounded-lg border border-[#2B2B2B] py-2.5 pr-4 pl-10 text-sm text-white placeholder:text-gray-500 focus:border-[#A7F950] focus:ring-1 focus:ring-[#A7F950] focus:outline-none'
195+
className='bg-background-card focus:border-primary focus:ring-primary w-full rounded-lg border border-[#2B2B2B] py-2.5 pr-4 pl-10 text-sm text-white placeholder:text-gray-500 focus:ring-1 focus:outline-none'
196196
/>
197197
</div>
198198

@@ -209,7 +209,7 @@ const CodeOfConductContent = () => {
209209
onClick={() => scrollToSection(item.id)}
210210
className={`block w-full rounded px-3 py-2 text-left text-sm transition-colors hover:bg-[#1a1a1a] ${
211211
activeSection === item.id
212-
? 'bg-[#1a1a1a] text-[#A7F950]'
212+
? 'text-primary bg-[#1a1a1a]'
213213
: 'text-gray-300'
214214
}`}
215215
>
@@ -755,7 +755,7 @@ const CodeOfConductContent = () => {
755755
href='https://www.contributor-covenant.org/faq'
756756
target='_blank'
757757
rel='noopener noreferrer'
758-
className='text-[#A7F950] hover:underline'
758+
className='text-primary hover:underline'
759759
>
760760
https://www.contributor-covenant.org/faq
761761
</a>
@@ -785,14 +785,14 @@ const CodeOfConductContent = () => {
785785
<div className='flex flex-col gap-3'>
786786
<a
787787
href='mailto:collins@boundlessfi.xyz?cc=benjamin@boundlessfi.xyz&subject=Code%20of%20Conduct%20Inquiry'
788-
className='flex items-center gap-2 text-[#A7F950] hover:underline'
788+
className='text-primary flex items-center gap-2 hover:underline'
789789
>
790790
<Mail className='h-4 w-4' />
791791
<span>collins@boundlessfi.xyz</span>
792792
</a>
793793
<a
794794
href='https://boundlessfi.xyz'
795-
className='flex items-center gap-2 text-[#A7F950] hover:underline'
795+
className='text-primary flex items-center gap-2 hover:underline'
796796
>
797797
<Globe className='h-4 w-4' />
798798
<span>https://boundlessfi.xyz</span>

app/(landing)/disclaimer/DisclaimerContent.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ const DisclaimerContent = () => {
195195
placeholder='Search keyword'
196196
value={searchQuery}
197197
onChange={e => setSearchQuery(e.target.value)}
198-
className='bg-background-card w-full rounded-lg border border-[#2B2B2B] py-2.5 pr-4 pl-10 text-sm text-white placeholder:text-gray-500 focus:border-[#A7F950] focus:ring-1 focus:ring-[#A7F950] focus:outline-none'
198+
className='bg-background-card focus:border-primary focus:ring-primary w-full rounded-lg border border-[#2B2B2B] py-2.5 pr-4 pl-10 text-sm text-white placeholder:text-gray-500 focus:ring-1 focus:outline-none'
199199
/>
200200
</div>
201201

@@ -212,7 +212,7 @@ const DisclaimerContent = () => {
212212
onClick={() => scrollToSection(item.id)}
213213
className={`block w-full rounded px-3 py-2 text-left text-sm transition-colors hover:bg-[#1a1a1a] ${
214214
activeSection === item.id
215-
? 'bg-[#1a1a1a] text-[#A7F950]'
215+
? 'text-primary bg-[#1a1a1a]'
216216
: 'text-gray-300'
217217
}`}
218218
>
@@ -882,14 +882,14 @@ const DisclaimerContent = () => {
882882
<div className='flex flex-col gap-3'>
883883
<a
884884
href='mailto:collins@boundlessfi.xyz?cc=benjamin@boundlessfi.xyz&subject=Disclaimer%20Inquiry'
885-
className='flex items-center gap-2 text-[#A7F950] hover:underline'
885+
className='text-primary flex items-center gap-2 hover:underline'
886886
>
887887
<Mail className='h-4 w-4' />
888888
<span>collins@boundlessfi.xyz</span>
889889
</a>
890890
<a
891891
href='https://boundlessfi.xyz'
892-
className='flex items-center gap-2 text-[#A7F950] hover:underline'
892+
className='text-primary flex items-center gap-2 hover:underline'
893893
>
894894
<Globe className='h-4 w-4' />
895895
<span>https://boundlessfi.xyz</span>

app/(landing)/hackathons/[slug]/HackathonPageClient.tsx

Lines changed: 46 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { useState, useEffect, useMemo, useCallback } from 'react';
44
import { useRouter, useSearchParams, useParams } from 'next/navigation';
55
import { useHackathonData } from '@/lib/providers/hackathonProvider';
6+
import { useHackathonAnnouncements } from '@/hooks/hackathon/use-hackathon-queries';
67
import { useRegisterHackathon } from '@/hooks/hackathon/use-register-hackathon';
78
import { useLeaveHackathon } from '@/hooks/hackathon/use-leave-hackathon';
89
import { useSubmission } from '@/hooks/hackathon/use-submission';
@@ -19,31 +20,34 @@ import { WinnersTab } from '@/components/hackathons/winners/WinnersTab';
1920
import LoadingScreen from '@/features/projects/components/CreateProjectModal/LoadingScreen';
2021
import { useTimelineEvents } from '@/hooks/hackathon/use-timeline-events';
2122
import { toast } from 'sonner';
22-
import type { Participant } from '@/lib/api/hackathons';
23+
import type { Hackathon, Participant } from '@/lib/api/hackathons';
2324
import { HackathonStickyCard } from '@/components/hackathons/hackathonStickyCard';
2425
import { HackathonParticipants } from '@/components/hackathons/participants/hackathonParticipant';
2526
import { useCommentSystem } from '@/hooks/use-comment-system';
2627
import { CommentEntityType } from '@/types/comment';
2728
import { useTeamPosts } from '@/hooks/hackathon/use-team-posts';
28-
import {
29-
listAnnouncements,
30-
type HackathonAnnouncement,
31-
} from '@/lib/api/hackathons/index';
3229
import { Megaphone } from 'lucide-react';
3330
import { AnnouncementsTab } from '@/components/hackathons/announcements/AnnouncementsTab';
34-
import { reportError, reportMessage } from '@/lib/error-reporting';
31+
import { reportMessage } from '@/lib/error-reporting';
3532

36-
export default function HackathonPageClient() {
33+
interface HackathonPageClientProps {
34+
/** Server-fetched hackathon — seeds React Query cache, eliminates loading state on first render. */
35+
initialHackathon: Hackathon;
36+
}
37+
38+
export default function HackathonPageClient({
39+
initialHackathon,
40+
}: HackathonPageClientProps) {
3741
const router = useRouter();
3842
const searchParams = useSearchParams();
3943
const params = useParams();
4044

45+
// `currentHackathon` is immediately available — seeded from server via initialData
4146
const {
4247
currentHackathon,
4348
submissions,
4449
winners,
4550
loading,
46-
setCurrentHackathon,
4751
refreshCurrentHackathon,
4852
} = useHackathonData();
4953

@@ -78,32 +82,9 @@ export default function HackathonPageClient() {
7882
autoFetch: !!hackathonId,
7983
});
8084

81-
// Fetch announcements for public view
82-
const [announcements, setAnnouncements] = useState<HackathonAnnouncement[]>(
83-
[]
84-
);
85-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
86-
const [announcementsLoading, setAnnouncementsLoading] = useState(false);
87-
88-
useEffect(() => {
89-
async function fetchAnnouncements() {
90-
if (!hackathonId) return;
91-
try {
92-
setAnnouncementsLoading(true);
93-
const data = await listAnnouncements(hackathonId);
94-
// Only show published announcements for public view
95-
setAnnouncements(data.filter(a => !a.isDraft));
96-
} catch (error) {
97-
reportError(error, {
98-
context: 'hackathon-fetchAnnouncements',
99-
hackathonId,
100-
});
101-
} finally {
102-
setAnnouncementsLoading(false);
103-
}
104-
}
105-
fetchAnnouncements();
106-
}, [hackathonId]);
85+
// React Query replaces the useEffect+useState announcements pattern
86+
const { data: announcements = [], isLoading: announcementsLoading } =
87+
useHackathonAnnouncements(hackathonId, !!hackathonId);
10788

10889
const hackathonTabs = useMemo(() => {
10990
const hasParticipants =
@@ -115,7 +96,8 @@ export default function HackathonPageClient() {
11596
const isTeamHackathon =
11697
participantType === 'TEAM' || participantType === 'TEAM_OR_INDIVIDUAL';
11798

118-
const hasWinners = winners && winners.length > 0;
99+
const hasWinners = (winners && winners.length > 0) || loading;
100+
const hasAnnouncements = announcements.length > 0 || announcementsLoading;
119101

120102
const tabs = [
121103
{ id: 'overview', label: 'Overview' },
@@ -137,7 +119,7 @@ export default function HackathonPageClient() {
137119
},
138120
]
139121
: []),
140-
...(announcements.length > 0
122+
...(hasAnnouncements
141123
? [
142124
{
143125
id: 'announcements',
@@ -229,6 +211,8 @@ export default function HackathonPageClient() {
229211
teamPosts.length,
230212
winners,
231213
announcements,
214+
announcementsLoading,
215+
loading,
232216
]);
233217

234218
// Refresh hackathon data
@@ -318,33 +302,13 @@ export default function HackathonPageClient() {
318302
router.push('?tab=team-formation');
319303
};
320304

321-
// Set current hackathon on mount
322-
const [isInitializing, setIsInitializing] = useState(true);
323-
324-
useEffect(() => {
325-
let isMounted = true;
326-
327-
const initHackathon = async () => {
328-
if (hackathonId) {
329-
await setCurrentHackathon(hackathonId);
330-
}
331-
if (isMounted) {
332-
setIsInitializing(false);
333-
}
334-
};
335-
336-
initHackathon();
305+
// No longer needed — currentHackathon is seeded from server via React Query initialData.
337306

338-
return () => {
339-
isMounted = false;
340-
};
341-
}, [hackathonId, setCurrentHackathon]);
342-
343-
// Handle tab changes from URL
344-
// Now also defaults to 'overview' if the URL tab is not in the filtered hackathonTabs list.
307+
// Handle tab changes from URL.
308+
// Defaults to 'overview' if the URL tab is not in the filtered hackathonTabs list.
345309
// This handles direct URL access to a disabled tab — user is silently redirected to overview.
346310
useEffect(() => {
347-
if (loading || !currentHackathon) return;
311+
if (!currentHackathon) return;
348312

349313
const tabFromUrl = searchParams.get('tab');
350314

@@ -360,12 +324,26 @@ export default function HackathonPageClient() {
360324
return;
361325
}
362326

363-
// Tab is disabled or unrecognised — fall back to overview
364-
setActiveTab('overview');
365-
const queryParams = new URLSearchParams(searchParams.toString());
366-
queryParams.set('tab', 'overview');
367-
router.replace(`?${queryParams.toString()}`, { scroll: false });
368-
}, [searchParams, hackathonTabs, router, loading, currentHackathon]);
327+
// If the tab is not in the list yet, check if it's because we're still loading data
328+
const isKnownTabLoading =
329+
(tabFromUrl === 'announcements' && announcementsLoading) ||
330+
(tabFromUrl === 'winners' && loading);
331+
332+
if (!isKnownTabLoading) {
333+
// Tab is disabled or unrecognised — fall back to overview
334+
setActiveTab('overview');
335+
const queryParams = new URLSearchParams(searchParams.toString());
336+
queryParams.set('tab', 'overview');
337+
router.replace(`?${queryParams.toString()}`, { scroll: false });
338+
}
339+
}, [
340+
searchParams,
341+
hackathonTabs,
342+
router,
343+
currentHackathon,
344+
announcementsLoading,
345+
loading,
346+
]);
369347

370348
const handleTabChange = (tabId: string) => {
371349
setActiveTab(tabId);
@@ -374,8 +352,8 @@ export default function HackathonPageClient() {
374352
router.push(`?${queryParams.toString()}`, { scroll: false });
375353
};
376354

377-
// Loading state
378-
if (loading || isInitializing) {
355+
// Only show a loading screen if data is still being fetched (e.g., window refocus refresh).
356+
if (loading && !currentHackathon) {
379357
return <LoadingScreen />;
380358
}
381359

app/(landing)/hackathons/[slug]/announcements/[announcementId]/page.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,13 @@ export default function AnnouncementDetailPage() {
9393
<div className='min-h-screen bg-black pb-24'>
9494
{/* Top Header */}
9595
<div className='sticky top-0 z-10 border-b border-zinc-900 bg-black/80 backdrop-blur-md'>
96-
<div className='mx-auto max-w-4xl items-center justify-between px-6 py-4'>
96+
<div className='mx-auto flex max-w-4xl items-center justify-between px-6 py-4'>
9797
<button
98-
onClick={() => window.close()}
98+
onClick={() => router.push(`/hackathons/${slug}?tab=announcements`)}
9999
className='flex items-center gap-2 text-sm text-zinc-400 transition-colors hover:text-white'
100100
>
101101
<ArrowLeft className='h-4 w-4' />
102-
Close Tab
102+
Back to Hackathon
103103
</button>
104104
<div className='flex items-center gap-2 text-zinc-500'>
105105
<Megaphone className='text-primary h-4 w-4' />
@@ -170,7 +170,6 @@ export default function AnnouncementDetailPage() {
170170
</div>
171171
</div>
172172
</div>
173-
174173
{/* Content */}
175174
<div className='prose prose-invert prose-primary max-w-none'>
176175
{markdownLoading ? (
@@ -191,7 +190,7 @@ export default function AnnouncementDetailPage() {
191190
This announcement was published by the hackathon organizers.
192191
</p>
193192
<BoundlessButton
194-
onClick={() => window.close()}
193+
onClick={() => router.push(`/hackathons/${slug}?tab=announcements`)}
195194
variant='outline'
196195
size='sm'
197196
>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import Image from 'next/image';
2+
import React from 'react';
3+
4+
const Banner = ({ banner, title }: { banner: string; title?: string }) => {
5+
return (
6+
<div className='relative h-[200px] w-full bg-gray-200 md:h-[360px]'>
7+
<Image
8+
src={banner}
9+
alt={`${title || 'hackathon'} banner`}
10+
fill
11+
className='object-cover'
12+
priority
13+
/>
14+
</div>
15+
);
16+
};
17+
18+
export default Banner;

0 commit comments

Comments
 (0)