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
5 changes: 1 addition & 4 deletions app/(landing)/blog/[slug]/not-found.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,7 @@ export default function BlogNotFound() {
</Link>
</Button>

<Button
asChild
className='bg-[#A7F950] text-black hover:bg-[#A7F950]/90'
>
<Button asChild className='bg-primary hover:bg-primary/90 text-black'>
<Link href='/'>
<Home className='mr-2 h-4 w-4' />
Go Home
Expand Down
10 changes: 5 additions & 5 deletions app/(landing)/code-of-conduct/CodeOfConductContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ const CodeOfConductContent = () => {
placeholder='Search keyword'
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
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'
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'
/>
</div>

Expand All @@ -209,7 +209,7 @@ const CodeOfConductContent = () => {
onClick={() => scrollToSection(item.id)}
className={`block w-full rounded px-3 py-2 text-left text-sm transition-colors hover:bg-[#1a1a1a] ${
activeSection === item.id
? 'bg-[#1a1a1a] text-[#A7F950]'
? 'text-primary bg-[#1a1a1a]'
: 'text-gray-300'
}`}
>
Expand Down Expand Up @@ -755,7 +755,7 @@ const CodeOfConductContent = () => {
href='https://www.contributor-covenant.org/faq'
target='_blank'
rel='noopener noreferrer'
className='text-[#A7F950] hover:underline'
className='text-primary hover:underline'
>
https://www.contributor-covenant.org/faq
</a>
Expand Down Expand Up @@ -785,14 +785,14 @@ const CodeOfConductContent = () => {
<div className='flex flex-col gap-3'>
<a
href='mailto:collins@boundlessfi.xyz?cc=benjamin@boundlessfi.xyz&subject=Code%20of%20Conduct%20Inquiry'
className='flex items-center gap-2 text-[#A7F950] hover:underline'
className='text-primary flex items-center gap-2 hover:underline'
>
<Mail className='h-4 w-4' />
<span>collins@boundlessfi.xyz</span>
</a>
<a
href='https://boundlessfi.xyz'
className='flex items-center gap-2 text-[#A7F950] hover:underline'
className='text-primary flex items-center gap-2 hover:underline'
>
<Globe className='h-4 w-4' />
<span>https://boundlessfi.xyz</span>
Expand Down
8 changes: 4 additions & 4 deletions app/(landing)/disclaimer/DisclaimerContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ const DisclaimerContent = () => {
placeholder='Search keyword'
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
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'
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'
/>
</div>

Expand All @@ -212,7 +212,7 @@ const DisclaimerContent = () => {
onClick={() => scrollToSection(item.id)}
className={`block w-full rounded px-3 py-2 text-left text-sm transition-colors hover:bg-[#1a1a1a] ${
activeSection === item.id
? 'bg-[#1a1a1a] text-[#A7F950]'
? 'text-primary bg-[#1a1a1a]'
: 'text-gray-300'
}`}
>
Expand Down Expand Up @@ -882,14 +882,14 @@ const DisclaimerContent = () => {
<div className='flex flex-col gap-3'>
<a
href='mailto:collins@boundlessfi.xyz?cc=benjamin@boundlessfi.xyz&subject=Disclaimer%20Inquiry'
className='flex items-center gap-2 text-[#A7F950] hover:underline'
className='text-primary flex items-center gap-2 hover:underline'
>
<Mail className='h-4 w-4' />
<span>collins@boundlessfi.xyz</span>
</a>
<a
href='https://boundlessfi.xyz'
className='flex items-center gap-2 text-[#A7F950] hover:underline'
className='text-primary flex items-center gap-2 hover:underline'
>
<Globe className='h-4 w-4' />
<span>https://boundlessfi.xyz</span>
Expand Down
114 changes: 46 additions & 68 deletions app/(landing)/hackathons/[slug]/HackathonPageClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { useState, useEffect, useMemo, useCallback } from 'react';
import { useRouter, useSearchParams, useParams } from 'next/navigation';
import { useHackathonData } from '@/lib/providers/hackathonProvider';
import { useHackathonAnnouncements } from '@/hooks/hackathon/use-hackathon-queries';
import { useRegisterHackathon } from '@/hooks/hackathon/use-register-hackathon';
import { useLeaveHackathon } from '@/hooks/hackathon/use-leave-hackathon';
import { useSubmission } from '@/hooks/hackathon/use-submission';
Expand All @@ -19,31 +20,34 @@ import { WinnersTab } from '@/components/hackathons/winners/WinnersTab';
import LoadingScreen from '@/features/projects/components/CreateProjectModal/LoadingScreen';
import { useTimelineEvents } from '@/hooks/hackathon/use-timeline-events';
import { toast } from 'sonner';
import type { Participant } from '@/lib/api/hackathons';
import type { Hackathon, Participant } from '@/lib/api/hackathons';
import { HackathonStickyCard } from '@/components/hackathons/hackathonStickyCard';
import { HackathonParticipants } from '@/components/hackathons/participants/hackathonParticipant';
import { useCommentSystem } from '@/hooks/use-comment-system';
import { CommentEntityType } from '@/types/comment';
import { useTeamPosts } from '@/hooks/hackathon/use-team-posts';
import {
listAnnouncements,
type HackathonAnnouncement,
} from '@/lib/api/hackathons/index';
import { Megaphone } from 'lucide-react';
import { AnnouncementsTab } from '@/components/hackathons/announcements/AnnouncementsTab';
import { reportError, reportMessage } from '@/lib/error-reporting';
import { reportMessage } from '@/lib/error-reporting';

export default function HackathonPageClient() {
interface HackathonPageClientProps {
/** Server-fetched hackathon — seeds React Query cache, eliminates loading state on first render. */
initialHackathon: Hackathon;
}

export default function HackathonPageClient({
initialHackathon,
}: HackathonPageClientProps) {
const router = useRouter();
const searchParams = useSearchParams();
const params = useParams();

// `currentHackathon` is immediately available — seeded from server via initialData
const {
currentHackathon,
submissions,
winners,
loading,
setCurrentHackathon,
refreshCurrentHackathon,
} = useHackathonData();

Expand Down Expand Up @@ -78,32 +82,9 @@ export default function HackathonPageClient() {
autoFetch: !!hackathonId,
});

// Fetch announcements for public view
const [announcements, setAnnouncements] = useState<HackathonAnnouncement[]>(
[]
);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [announcementsLoading, setAnnouncementsLoading] = useState(false);

useEffect(() => {
async function fetchAnnouncements() {
if (!hackathonId) return;
try {
setAnnouncementsLoading(true);
const data = await listAnnouncements(hackathonId);
// Only show published announcements for public view
setAnnouncements(data.filter(a => !a.isDraft));
} catch (error) {
reportError(error, {
context: 'hackathon-fetchAnnouncements',
hackathonId,
});
} finally {
setAnnouncementsLoading(false);
}
}
fetchAnnouncements();
}, [hackathonId]);
// React Query replaces the useEffect+useState announcements pattern
const { data: announcements = [], isLoading: announcementsLoading } =
useHackathonAnnouncements(hackathonId, !!hackathonId);

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

const hasWinners = winners && winners.length > 0;
const hasWinners = (winners && winners.length > 0) || loading;
const hasAnnouncements = announcements.length > 0 || announcementsLoading;

const tabs = [
{ id: 'overview', label: 'Overview' },
Expand All @@ -137,7 +119,7 @@ export default function HackathonPageClient() {
},
]
: []),
...(announcements.length > 0
...(hasAnnouncements
? [
{
id: 'announcements',
Expand Down Expand Up @@ -229,6 +211,8 @@ export default function HackathonPageClient() {
teamPosts.length,
winners,
announcements,
announcementsLoading,
loading,
]);

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

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

useEffect(() => {
let isMounted = true;

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

initHackathon();
// No longer needed — currentHackathon is seeded from server via React Query initialData.

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

// Handle tab changes from URL
// Now also defaults to 'overview' if the URL tab is not in the filtered hackathonTabs list.
// Handle tab changes from URL.
// Defaults to 'overview' if the URL tab is not in the filtered hackathonTabs list.
// This handles direct URL access to a disabled tab — user is silently redirected to overview.
useEffect(() => {
if (loading || !currentHackathon) return;
if (!currentHackathon) return;

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

Expand All @@ -360,12 +324,26 @@ export default function HackathonPageClient() {
return;
}

// Tab is disabled or unrecognised — fall back to overview
setActiveTab('overview');
const queryParams = new URLSearchParams(searchParams.toString());
queryParams.set('tab', 'overview');
router.replace(`?${queryParams.toString()}`, { scroll: false });
}, [searchParams, hackathonTabs, router, loading, currentHackathon]);
// If the tab is not in the list yet, check if it's because we're still loading data
const isKnownTabLoading =
(tabFromUrl === 'announcements' && announcementsLoading) ||
(tabFromUrl === 'winners' && loading);

if (!isKnownTabLoading) {
// Tab is disabled or unrecognised — fall back to overview
setActiveTab('overview');
const queryParams = new URLSearchParams(searchParams.toString());
queryParams.set('tab', 'overview');
router.replace(`?${queryParams.toString()}`, { scroll: false });
}
}, [
searchParams,
hackathonTabs,
router,
currentHackathon,
announcementsLoading,
loading,
]);

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

// Loading state
if (loading || isInitializing) {
// Only show a loading screen if data is still being fetched (e.g., window refocus refresh).
if (loading && !currentHackathon) {
return <LoadingScreen />;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,13 @@ export default function AnnouncementDetailPage() {
<div className='min-h-screen bg-black pb-24'>
{/* Top Header */}
<div className='sticky top-0 z-10 border-b border-zinc-900 bg-black/80 backdrop-blur-md'>
<div className='mx-auto max-w-4xl items-center justify-between px-6 py-4'>
<div className='mx-auto flex max-w-4xl items-center justify-between px-6 py-4'>
<button
onClick={() => window.close()}
onClick={() => router.push(`/hackathons/${slug}?tab=announcements`)}
className='flex items-center gap-2 text-sm text-zinc-400 transition-colors hover:text-white'
>
<ArrowLeft className='h-4 w-4' />
Close Tab
Back to Hackathon
</button>
<div className='flex items-center gap-2 text-zinc-500'>
<Megaphone className='text-primary h-4 w-4' />
Expand Down Expand Up @@ -170,7 +170,6 @@ export default function AnnouncementDetailPage() {
</div>
</div>
</div>

{/* Content */}
<div className='prose prose-invert prose-primary max-w-none'>
{markdownLoading ? (
Expand All @@ -191,7 +190,7 @@ export default function AnnouncementDetailPage() {
This announcement was published by the hackathon organizers.
</p>
<BoundlessButton
onClick={() => window.close()}
onClick={() => router.push(`/hackathons/${slug}?tab=announcements`)}
variant='outline'
size='sm'
>
Expand Down
18 changes: 18 additions & 0 deletions app/(landing)/hackathons/[slug]/components/Banner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Image from 'next/image';
import React from 'react';

const Banner = ({ banner, title }: { banner: string; title?: string }) => {
return (
<div className='relative h-[200px] w-full bg-gray-200 md:h-[360px]'>
<Image
src={banner}
alt={`${title || 'hackathon'} banner`}
fill
className='object-cover'
priority
/>
</div>
);
};

export default Banner;
Loading
Loading