@@ -167,6 +167,33 @@ export default function HackathonPageClient() {
167167 } ) ;
168168 }
169169
170+ // Filter tabs against enabledTabs so only explicitly enabled tabs are shown.
171+ // 'overview' is always kept as it is the default fallback tab.
172+ // If enabledTabs is undefined/null (not configured), all tabs are shown as before.
173+ // Map UI tab ids to backend enabledTabs keys where they differ.
174+ const tabIdToEnabledKey : Record < string , string > = {
175+ 'team-formation' : 'joinATeamTab' ,
176+ winners : 'winnersTab' ,
177+ resources : 'resourcesTab' ,
178+ participants : 'participantsTab' ,
179+ announcements : 'announcementsTab' ,
180+ submission : 'submissionTab' ,
181+ discussions : 'discussionTab' ,
182+ } ;
183+
184+ type EnabledTab = NonNullable <
185+ typeof currentHackathon
186+ > [ 'enabledTabs' ] [ number ] ;
187+ const enabledTabs = currentHackathon ?. enabledTabs ;
188+
189+ if ( Array . isArray ( enabledTabs ) ) {
190+ const enabledSet = new Set ( enabledTabs ) ;
191+ return tabs . filter ( tab => {
192+ if ( tab . id === 'overview' ) return true ;
193+ const key = ( tabIdToEnabledKey [ tab . id ] ?? tab . id ) as EnabledTab ;
194+ return enabledSet . has ( key ) ;
195+ } ) ;
196+ }
170197 return tabs ;
171198 } , [
172199 currentHackathon ?. participants ,
@@ -273,12 +300,29 @@ export default function HackathonPageClient() {
273300 } , [ hackathonId , setCurrentHackathon ] ) ;
274301
275302 // Handle tab changes from URL
303+ // Now also defaults to 'overview' if the URL tab is not in the filtered hackathonTabs list.
304+ // This handles direct URL access to a disabled tab — user is silently redirected to overview.
276305 useEffect ( ( ) => {
277306 const tabFromUrl = searchParams . get ( 'tab' ) ;
278- if ( tabFromUrl && hackathonTabs . some ( tab => tab . id === tabFromUrl ) ) {
307+
308+ // No tab in URL — default to overview
309+ if ( ! tabFromUrl ) {
310+ setActiveTab ( 'overview' ) ;
311+ return ;
312+ }
313+
314+ if ( hackathonTabs . some ( tab => tab . id === tabFromUrl ) ) {
315+ // Tab exists in filtered list — activate it normally
279316 setActiveTab ( tabFromUrl ) ;
317+ return ;
280318 }
281- } , [ searchParams , hackathonTabs ] ) ;
319+
320+ // Tab is disabled or unrecognised — fall back to overview
321+ setActiveTab ( 'overview' ) ;
322+ const queryParams = new URLSearchParams ( searchParams . toString ( ) ) ;
323+ queryParams . set ( 'tab' , 'overview' ) ;
324+ router . replace ( `?${ queryParams . toString ( ) } ` , { scroll : false } ) ;
325+ } , [ searchParams , hackathonTabs , router ] ) ;
282326
283327 const handleTabChange = ( tabId : string ) => {
284328 setActiveTab ( tabId ) ;
@@ -308,6 +352,11 @@ export default function HackathonPageClient() {
308352 ) ;
309353 }
310354
355+ // Helper: checks if a tab id is present in the filtered hackathonTabs array.
356+ // Used below to guard each tab's content from rendering if the tab is disabled.
357+ const isTabVisible = ( tabId : string ) =>
358+ hackathonTabs . some ( tab => tab . id === tabId ) ;
359+
311360 // Shared props for banner and sticky card
312361 const sharedActionProps = {
313362 deadline : currentHackathon . submissionDeadline ,
@@ -378,48 +427,60 @@ export default function HackathonPageClient() {
378427 />
379428 ) }
380429
430+ { /* isTabVisible('resources') guard — HackathonResources will not
431+ render at all if 'resources' is not in enabledTabs, even via direct URL */ }
381432 { activeTab === 'resources' &&
433+ isTabVisible ( 'resources' ) &&
382434 currentHackathon . resources ?. length > 0 && < HackathonResources /> }
435+
436+ { /* isTabVisible('participants') guard */ }
383437 { activeTab === 'participants' &&
438+ isTabVisible ( 'participants' ) &&
384439 currentHackathon . participants ?. length > 0 && (
385440 < HackathonParticipants />
386441 ) }
387442
388- { activeTab === 'announcements' && announcements . length > 0 && (
389- < AnnouncementsTab
390- announcements = { announcements }
391- hackathonSlug = { hackathonId }
392- />
393- ) }
443+ { /* isTabVisible('announcements') guard */ }
444+ { activeTab === 'announcements' &&
445+ isTabVisible ( 'announcements' ) &&
446+ announcements . length > 0 && (
447+ < AnnouncementsTab
448+ announcements = { announcements }
449+ hackathonSlug = { hackathonId }
450+ />
451+ ) }
394452
395- { activeTab === 'submission' && (
453+ { /* isTabVisible('submission') guard */ }
454+ { activeTab === 'submission' && isTabVisible ( 'submission' ) && (
396455 < SubmissionTab
397456 organizationId = { currentHackathon . organizationId }
398457 isRegistered = { isRegistered }
399458 />
400459 ) }
401460
402- { activeTab === 'discussions' && (
461+ { /* isTabVisible('discussions') guard */ }
462+ { activeTab === 'discussions' && isTabVisible ( 'discussions' ) && (
403463 < HackathonDiscussions
404464 hackathonId = { hackathonId }
405465 isRegistered = { isRegistered }
406466 />
407467 ) }
408468
409- { activeTab === 'team-formation' && (
410- < TeamFormationTab
411- hackathonSlugOrId = { hackathonId }
412- isRegistered = { isRegistered }
413- />
414- ) }
469+ { /* isTabVisible('team-formation') guard */ }
470+ { activeTab === 'team-formation' &&
471+ isTabVisible ( 'team-formation' ) && (
472+ < TeamFormationTab
473+ hackathonSlugOrId = { hackathonId }
474+ isRegistered = { isRegistered }
475+ />
476+ ) }
415477
416- { activeTab === 'winners' && (
478+ { /* isTabVisible('winners') guard */ }
479+ { activeTab === 'winners' && isTabVisible ( 'winners' ) && (
417480 < WinnersTab winners = { winners } hackathonSlug = { hackathonId } />
418481 ) }
419482
420- { activeTab === 'resources' && currentHackathon ?. resources ?. [ 0 ] && (
421- < HackathonResources />
422- ) }
483+ { /* Note: duplicate resources render removed — was already covered above */ }
423484 </ div >
424485 </ div >
425486
0 commit comments