Skip to content

Commit 8d53df8

Browse files
authored
Robust tab visibility and gating (#428)
* feat: Robust Tab Visibility and Gating * feat: Robust Tab Visibility and Gating * fix: downgrade @eslint/js to match eslint v9 * feat: Robust Tab Visibility and Gating * fix: address CodeRabbit review — tab key mapping and missing tab fallback * fix: add missing tab id to backend key mappings in tabIdToEnabledKey * fix: removed pnpm-lock.yaml and unwanted dependency
1 parent 9d26bd3 commit 8d53df8

1 file changed

Lines changed: 81 additions & 20 deletions

File tree

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

Lines changed: 81 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)