33import { useState , useEffect , useMemo , useCallback } from 'react' ;
44import { useRouter , useSearchParams , useParams } from 'next/navigation' ;
55import { useHackathonData } from '@/lib/providers/hackathonProvider' ;
6+ import { useHackathonAnnouncements } from '@/hooks/hackathon/use-hackathon-queries' ;
67import { useRegisterHackathon } from '@/hooks/hackathon/use-register-hackathon' ;
78import { useLeaveHackathon } from '@/hooks/hackathon/use-leave-hackathon' ;
89import { useSubmission } from '@/hooks/hackathon/use-submission' ;
@@ -19,31 +20,34 @@ import { WinnersTab } from '@/components/hackathons/winners/WinnersTab';
1920import LoadingScreen from '@/features/projects/components/CreateProjectModal/LoadingScreen' ;
2021import { useTimelineEvents } from '@/hooks/hackathon/use-timeline-events' ;
2122import { toast } from 'sonner' ;
22- import type { Participant } from '@/lib/api/hackathons' ;
23+ import type { Hackathon , Participant } from '@/lib/api/hackathons' ;
2324import { HackathonStickyCard } from '@/components/hackathons/hackathonStickyCard' ;
2425import { HackathonParticipants } from '@/components/hackathons/participants/hackathonParticipant' ;
2526import { useCommentSystem } from '@/hooks/use-comment-system' ;
2627import { CommentEntityType } from '@/types/comment' ;
2728import { useTeamPosts } from '@/hooks/hackathon/use-team-posts' ;
28- import {
29- listAnnouncements ,
30- type HackathonAnnouncement ,
31- } from '@/lib/api/hackathons/index' ;
3229import { Megaphone } from 'lucide-react' ;
3330import { 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
0 commit comments