Skip to content

Commit 0060643

Browse files
committed
Merge branch 'main' of github.com:boundlessfi/boundless
2 parents 6e78420 + 8d53df8 commit 0060643

27 files changed

Lines changed: 2156 additions & 477 deletions

File tree

Redesign Project Detail Page

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Completed: Redesign Project Detail Page — Simpler, clearer UI/UX
2+
3+
https://www.figma.com/design/EMNGAQl1SGObXcsoa24krt/Boundless_Project-Details?node-id=0-1&t=CY7XUp5OWWoJ8qpK-1
4+

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,
@@ -380,48 +429,60 @@ export default function HackathonPageClient() {
380429
/>
381430
)}
382431

432+
{/* isTabVisible('resources') guard — HackathonResources will not
433+
render at all if 'resources' is not in enabledTabs, even via direct URL */}
383434
{activeTab === 'resources' &&
435+
isTabVisible('resources') &&
384436
currentHackathon.resources?.length > 0 && <HackathonResources />}
437+
438+
{/* isTabVisible('participants') guard */}
385439
{activeTab === 'participants' &&
440+
isTabVisible('participants') &&
386441
currentHackathon.participants?.length > 0 && (
387442
<HackathonParticipants />
388443
)}
389444

390-
{activeTab === 'announcements' && announcements.length > 0 && (
391-
<AnnouncementsTab
392-
announcements={announcements}
393-
hackathonSlug={hackathonId}
394-
/>
395-
)}
445+
{/* isTabVisible('announcements') guard */}
446+
{activeTab === 'announcements' &&
447+
isTabVisible('announcements') &&
448+
announcements.length > 0 && (
449+
<AnnouncementsTab
450+
announcements={announcements}
451+
hackathonSlug={hackathonId}
452+
/>
453+
)}
396454

397-
{activeTab === 'submission' && (
455+
{/* isTabVisible('submission') guard */}
456+
{activeTab === 'submission' && isTabVisible('submission') && (
398457
<SubmissionTab
399458
organizationId={currentHackathon.organizationId}
400459
isRegistered={isRegistered}
401460
/>
402461
)}
403462

404-
{activeTab === 'discussions' && (
463+
{/* isTabVisible('discussions') guard */}
464+
{activeTab === 'discussions' && isTabVisible('discussions') && (
405465
<HackathonDiscussions
406466
hackathonId={hackathonId}
407467
isRegistered={isRegistered}
408468
/>
409469
)}
410470

411-
{activeTab === 'team-formation' && (
412-
<TeamFormationTab
413-
hackathonSlugOrId={hackathonId}
414-
isRegistered={isRegistered}
415-
/>
416-
)}
471+
{/* isTabVisible('team-formation') guard */}
472+
{activeTab === 'team-formation' &&
473+
isTabVisible('team-formation') && (
474+
<TeamFormationTab
475+
hackathonSlugOrId={hackathonId}
476+
isRegistered={isRegistered}
477+
/>
478+
)}
417479

418-
{activeTab === 'winners' && (
480+
{/* isTabVisible('winners') guard */}
481+
{activeTab === 'winners' && isTabVisible('winners') && (
419482
<WinnersTab winners={winners} hackathonSlug={hackathonId} />
420483
)}
421484

422-
{activeTab === 'resources' && currentHackathon?.resources?.[0] && (
423-
<HackathonResources />
424-
)}
485+
{/* Note: duplicate resources render removed — was already covered above */}
425486
</div>
426487
</div>
427488

app/(landing)/profile/[username]/profile-data.tsx

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
11
'use client';
22

33
import { useEffect, useState } from 'react';
4+
import { Filter } from 'lucide-react';
45
import { getUserProfileByUsername } from '@/lib/api/auth';
6+
import { authClient } from '@/lib/auth-client';
57
import { PublicUserProfile } from '@/features/projects/types';
68
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
7-
import ActivityTab from '@/components/profile/ActivityTab';
89
import {
910
DropdownMenu,
1011
DropdownMenuContent,
1112
DropdownMenuItem,
1213
DropdownMenuTrigger,
1314
} from '@/components/ui/dropdown-menu';
1415
import { Button } from '@/components/ui/button';
16+
import ActivityTab from '@/components/profile/ActivityTab';
1517
import OrganizationsTab from '@/components/profile/OrganizationsTab';
16-
import { Filter } from 'lucide-react';
17-
import { authClient } from '@/lib/auth-client';
1818
import ProfileOverviewPublic from '@/components/profile/ProfileOverviewPublic';
1919
import ProjectsTabPublic from '@/components/profile/ProjectsTabPublic';
20+
import PublicEarningsTab from '@/components/profile/PublicEarningsTab';
2021

2122
interface PublicProfileDataProps {
2223
username: string;
@@ -32,20 +33,26 @@ const FILTER_OPTIONS = [
3233
'All Time',
3334
];
3435

35-
export function ProfileData({ username }: PublicProfileDataProps) {
36+
const TAB_CLASS =
37+
'data-[state=active]:border-b-primary/45 rounded-none border-b-2 border-transparent bg-transparent px-0 py-3 text-sm font-medium text-zinc-500 data-[state=active]:text-white';
38+
39+
export function ProfileData({
40+
username,
41+
}: PublicProfileDataProps): React.ReactElement {
3642
const [userData, setUserData] = useState<PublicUserProfile | null>(null);
3743
const [loading, setLoading] = useState(true);
3844
const [error, setError] = useState<string | null>(null);
3945
const [selectedFilter, setSelectedFilter] = useState('All');
40-
const [authUser, setAuthUser] = useState<string | null>(null);
46+
const [authUsername, setAuthUsername] = useState<string | null>(null);
4147
const [isAuthenticated, setIsAuthenticated] = useState(false);
48+
4249
useEffect(() => {
43-
async function loadProfile() {
50+
async function loadProfile(): Promise<void> {
4451
try {
52+
setLoading(true);
4553
const { data: session } = await authClient.getSession();
46-
setAuthUser(session?.user?.profile?.username || null);
54+
setAuthUsername(session?.user?.profile?.username || null);
4755
setIsAuthenticated(!!session?.user);
48-
setLoading(true);
4956
const data = await getUserProfileByUsername(username);
5057
setUserData(data);
5158
} catch (err) {
@@ -87,8 +94,7 @@ export function ProfileData({ username }: PublicProfileDataProps) {
8794
name: org.name,
8895
avatarUrl: org.logo || '/blog1.jpg',
8996
})) || [];
90-
// Determine if it's the user's own profile
91-
const isOwnProfile = isAuthenticated && authUser === userData.username;
97+
const isOwnProfile = isAuthenticated && authUsername === userData.username;
9298

9399
return (
94100
<section className='mt-14 flex flex-col gap-8 lg:flex-row lg:gap-16'>
@@ -103,21 +109,18 @@ export function ProfileData({ username }: PublicProfileDataProps) {
103109
<Tabs defaultValue='activity' className='w-full'>
104110
<div className='border-b border-zinc-800'>
105111
<TabsList className='h-auto w-full justify-start gap-6 bg-transparent p-0'>
106-
<TabsTrigger
107-
value='activity'
108-
className='data-[state=active]:border-b-primary/45 rounded-none border-b-2 border-transparent bg-transparent px-0 py-3 text-sm font-medium text-zinc-500 data-[state=active]:text-white'
109-
>
112+
<TabsTrigger value='activity' className={TAB_CLASS}>
110113
Activity
111114
</TabsTrigger>
112-
<TabsTrigger
113-
value='projects'
114-
className='data-[state=active]:border-b-primary/45 rounded-none border-b-2 border-transparent bg-transparent px-0 py-3 text-sm font-medium text-zinc-500 data-[state=active]:text-white'
115-
>
115+
<TabsTrigger value='projects' className={TAB_CLASS}>
116116
Projects
117117
</TabsTrigger>
118+
<TabsTrigger value='earnings' className={TAB_CLASS}>
119+
Earnings
120+
</TabsTrigger>
118121
<TabsTrigger
119122
value='organizations'
120-
className='data-[state=active]:border-primary rounded-none border-b-2 border-transparent bg-transparent px-0 py-3 text-sm font-medium text-zinc-500 data-[state=active]:text-white md:hidden'
123+
className={`${TAB_CLASS} md:hidden`}
121124
>
122125
Organizations
123126
</TabsTrigger>
@@ -163,6 +166,10 @@ export function ProfileData({ username }: PublicProfileDataProps) {
163166
<ProjectsTabPublic user={userData} />
164167
</TabsContent>
165168

169+
<TabsContent value='earnings' className='mt-0'>
170+
<PublicEarningsTab username={username} />
171+
</TabsContent>
172+
166173
{isAuthenticated && isOwnProfile && (
167174
<TabsContent value='organizations' className='mt-0'>
168175
<OrganizationsTab organizations={organizationsData} />

app/me/earnings/page.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,9 @@ const BreakdownItem: React.FC<BreakdownItemProps> = ({
8181
</div>
8282
<span className='font-medium'>{label}</span>
8383
</div>
84-
<span className='font-semibold'>${value.toLocaleString()}</span>
84+
<span className='font-semibold'>
85+
${(Number(value) || 0).toLocaleString()}
86+
</span>
8587
</div>
8688
);
8789

@@ -106,7 +108,9 @@ const ActivityItem: React.FC<ActivityItemProps> = ({ activity }) => (
106108
</div>
107109
</div>
108110
<div className='text-right'>
109-
<p className='text-lg font-bold'>${activity.amount.toLocaleString()}</p>
111+
<p className='text-lg font-bold'>
112+
${(Number(activity.amount) || 0).toLocaleString()}
113+
</p>
110114
{activity.currency && (
111115
<p className='text-muted-foreground text-xs'>{activity.currency}</p>
112116
)}
@@ -118,7 +122,7 @@ const ActivityItem: React.FC<ActivityItemProps> = ({ activity }) => (
118122
* EarningsSkeleton component for loading states.
119123
*/
120124
const EarningsSkeleton: React.FC = () => (
121-
<div className='container mx-auto space-y-8 py-8'>
125+
<div className='container mx-auto space-y-8 px-6 py-8'>
122126
<div className='space-y-2'>
123127
<Skeleton className='h-10 w-[250px]' />
124128
<Skeleton className='h-6 w-[350px]' />
@@ -146,8 +150,10 @@ const EarningsPage: React.FC = () => {
146150
const fetchData = async () => {
147151
try {
148152
const res = await getUserEarnings();
149-
if (res.success && res.data) {
153+
if (res.success) {
150154
setData(res.data);
155+
} else {
156+
toast.error(res.error || 'Failed to load earnings data');
151157
}
152158
} catch (error) {
153159
console.error('Failed to fetch earnings:', error);
@@ -174,7 +180,7 @@ const EarningsPage: React.FC = () => {
174180
}
175181

176182
return (
177-
<div className='container mx-auto space-y-8 py-8'>
183+
<div className='container mx-auto space-y-8 px-6 py-8'>
178184
<motion.div
179185
initial={{ opacity: 0, y: 20 }}
180186
animate={{ opacity: 1, y: 0 }}

0 commit comments

Comments
 (0)