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
16 changes: 11 additions & 5 deletions app/me/earnings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ const BreakdownItem: React.FC<BreakdownItemProps> = ({
</div>
<span className='font-medium'>{label}</span>
</div>
<span className='font-semibold'>${value.toLocaleString()}</span>
<span className='font-semibold'>
${(Number(value) || 0).toLocaleString()}
</span>
</div>
);

Expand All @@ -106,7 +108,9 @@ const ActivityItem: React.FC<ActivityItemProps> = ({ activity }) => (
</div>
</div>
<div className='text-right'>
<p className='text-lg font-bold'>${activity.amount.toLocaleString()}</p>
<p className='text-lg font-bold'>
${(Number(activity.amount) || 0).toLocaleString()}
</p>
{activity.currency && (
<p className='text-muted-foreground text-xs'>{activity.currency}</p>
)}
Expand All @@ -118,7 +122,7 @@ const ActivityItem: React.FC<ActivityItemProps> = ({ activity }) => (
* EarningsSkeleton component for loading states.
*/
const EarningsSkeleton: React.FC = () => (
<div className='container mx-auto space-y-8 py-8'>
<div className='container mx-auto space-y-8 px-6 py-8'>
<div className='space-y-2'>
<Skeleton className='h-10 w-[250px]' />
<Skeleton className='h-6 w-[350px]' />
Expand Down Expand Up @@ -146,8 +150,10 @@ const EarningsPage: React.FC = () => {
const fetchData = async () => {
try {
const res = await getUserEarnings();
if (res.success && res.data) {
if (res.success) {
setData(res.data);
} else {
toast.error(res.error || 'Failed to load earnings data');
}
} catch (error) {
console.error('Failed to fetch earnings:', error);
Expand All @@ -174,7 +180,7 @@ const EarningsPage: React.FC = () => {
}

return (
<div className='container mx-auto space-y-8 py-8'>
<div className='container mx-auto space-y-8 px-6 py-8'>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
Expand Down
34 changes: 24 additions & 10 deletions app/me/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,30 @@ import { AppSidebar } from '@/components/app-sidebar';
import { SiteHeader } from '@/components/site-header';
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
import { useAuthStatus } from '@/hooks/use-auth';
import React from 'react';
import React, { useMemo } from 'react';
import LoadingSpinner from '@/components/LoadingSpinner';

export default function MeLayout({ children }: { children: React.ReactNode }) {
const { user, isLoading } = useAuthStatus();

const { name = '', email = '', profile, image: userImage = '' } = user || {};

const userData = {
name: name || '',
email,
image:
(profile as any)?.user?.image || (profile as any)?.image || userImage,
};

const hackathonsCount = useMemo(() => {
if (!profile) return 0;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const joined = (profile as any)?.user?.joinedHackathons || [];

return joined.length;
}, [profile]);
Comment on lines +22 to +29

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, find the useAuthStatus hook definition
rg -n "useAuthStatus" --type=ts --type=tsx -A 15 | head -100

Repository: boundlessfi/boundless

Length of output: 92


🏁 Script executed:

#!/bin/bash
# Search for joinedHackathons definition or references
rg -n "joinedHackathons" --type=ts --type=tsx -B 3 -A 3

Repository: boundlessfi/boundless

Length of output: 92


🏁 Script executed:

#!/bin/bash
# Check for Profile or AuthStatus type definitions
rg -n "interface.*Profile|type.*Profile|interface.*AuthStatus|type.*AuthStatus" --type=ts --type=tsx -A 10

Repository: boundlessfi/boundless

Length of output: 92


🏁 Script executed:

#!/bin/bash
# Read the app/me/layout.tsx file to understand context
wc -l app/me/layout.tsx

Repository: boundlessfi/boundless

Length of output: 85


🏁 Script executed:

#!/bin/bash
# Read the layout.tsx file to see the full context
cat -n app/me/layout.tsx

Repository: boundlessfi/boundless

Length of output: 2006


🏁 Script executed:

#!/bin/bash
# Search for useAuthStatus hook definition using glob pattern
rg -n "useAuthStatus" -A 15 --glob="*.ts" --glob="*.tsx" | head -100

Repository: boundlessfi/boundless

Length of output: 6735


🏁 Script executed:

#!/bin/bash
# Search for joinedHackathons references
rg -n "joinedHackathons" -B 3 -A 3 --glob="*.ts" --glob="*.tsx"

Repository: boundlessfi/boundless

Length of output: 1257


🏁 Script executed:

#!/bin/bash
# Search for Profile type or interface definitions
rg -n "interface.*Profile|type.*Profile" -A 10 --glob="*.ts" --glob="*.tsx" | head -150

Repository: boundlessfi/boundless

Length of output: 6339


Define a proper type for profile structure instead of casting to any.

The profile returned from useAuthStatus() is typed as any (see hooks/use-auth.ts line 100), and the code accesses profile.user?.joinedHackathons without type safety. This pattern is repeated elsewhere in the codebase (e.g., app/me/participating/page.tsx), leaving the code vulnerable to API response structure changes.

Define a type for the profile object to replace the as any cast:

♻️ Suggested approach
+ interface UserProfileWithHackathons {
+   user?: {
+     joinedHackathons?: Array<{ hackathon?: { id: string; name: string } }>;
+   };
+ }

  const hackathonsCount = useMemo(() => {
    if (!profile) return 0;
-   const joined = (profile as any)?.user?.joinedHackathons || [];
+   const joined = (profile as UserProfileWithHackathons)?.user?.joinedHackathons || [];
    return joined.length;
  }, [profile]);

Consider updating hooks/use-auth.ts to properly type userProfile instead of useState<any>, which would eliminate the need for casts throughout the codebase.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const hackathonsCount = useMemo(() => {
if (!profile) return 0;
const joined = (profile as any)?.user?.joinedHackathons || [];
return joined.length;
}, [profile]);
interface UserProfileWithHackathons {
user?: {
joinedHackathons?: Array<{ hackathon?: { id: string; name: string } }>;
};
}
const hackathonsCount = useMemo(() => {
if (!profile) return 0;
const joined = (profile as UserProfileWithHackathons)?.user?.joinedHackathons || [];
return joined.length;
}, [profile]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/me/layout.tsx` around lines 21 - 27, The code is using an unsafe any cast
for profile when computing hackathonsCount; define a proper TypeScript interface
for the auth/profile shape (including user and joinedHackathons: any[] or a more
specific type) and use it instead of any. Update the hook useAuthStatus / the
userProfile state in hooks/use-auth.ts to return/hold this typed Profile
interface, then change the cast in app/me/layout.tsx so hackathonsCount reads
profile.user?.joinedHackathons with the new Profile type (remove "as any");
ensure the new type is exported and used across other files (e.g.,
app/me/participating/page.tsx) to eliminate repeated unsafe casts.


if (isLoading) {
return (
<div className='flex h-screen items-center justify-center'>
Expand All @@ -18,14 +36,6 @@ export default function MeLayout({ children }: { children: React.ReactNode }) {
);
}

const { name = '', email = '', profile, image: userImage = '' } = user || {};

const userData = {
name: name || '',
email,
image: profile?.image || userImage,
};

return (
<SidebarProvider
style={
Expand All @@ -35,7 +45,11 @@ export default function MeLayout({ children }: { children: React.ReactNode }) {
} as React.CSSProperties
}
>
<AppSidebar user={userData} variant='inset' />
<AppSidebar
user={userData}
counts={{ participating: hackathonsCount }}
variant='inset'
/>
<SidebarInset className='bg-[#0e0c0c]'>
<SiteHeader />
<div className='flex flex-1 flex-col'>{children}</div>
Expand Down
256 changes: 256 additions & 0 deletions app/me/participating/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
'use client';

import { useMemo, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuthStatus } from '@/hooks/use-auth';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { motion, AnimatePresence } from 'framer-motion';
import HackathonCard from '@/components/landing-page/hackathon/HackathonCard';
import {
ProgressIndicator,
SubmissionStage,
} from '@/components/hackathons/ProgressIndicator';
import { cn } from '@/lib/utils';
import { Hackathon } from '@/lib/api/hackathons';
import EmptyState from '@/components/EmptyState';

type TabType = 'all' | 'hackathons' | 'projects';

interface UnifiedItem extends Hackathon {
type: 'hackathon';
}

export default function ParticipatingPage() {
const router = useRouter();
const { user, isLoading } = useAuthStatus();
const [activeTab, setActiveTab] = useState<TabType>('all');

const handleTabChange = (value: string) => {
setActiveTab(value as TabType);
};

const unifiedList = useMemo<UnifiedItem[]>(() => {
const profile = user?.profile;
if (!profile) {
return [];
}

const joinedHackathons = profile.user?.joinedHackathons || [];
const hackathonsAsParticipant = profile.hackathonsAsParticipant || [];
const submissions = profile.user?.hackathonSubmissionsAsParticipant || [];

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const typedJoinedHackathons: UnifiedItem[] = joinedHackathons
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.filter((h: any) => {
const data = h?.hackathon || h;
return data && data.id;
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.map((h: any) => {
const hackathonData = h.hackathon || h;
return {
...hackathonData,
type: 'hackathon' as const,
};
});

// Map hackathons from participating list — filter first to ensure p.hackathon is defined
const typedParticipatingHackathons: UnifiedItem[] = hackathonsAsParticipant
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.filter((p: any) => p && p.hackathon && p.hackathon.id)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.map((p: any) => ({
...p.hackathon,
type: 'hackathon' as const,
}));

// Map hackathons from submissions
const typedSubmissionHackathons: UnifiedItem[] = submissions
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.filter((s: any) => s.hackathon)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.map((s: any) => ({
...s.hackathon,
type: 'hackathon' as const,
}));

// Merge and deduplicate by ID
const merged = [
...typedParticipatingHackathons,
...typedJoinedHackathons,
...typedSubmissionHackathons,
];

const seen = new Set<string>();
const deduplicated = merged.filter(item => {
if (!item.id || seen.has(item.id)) return false;
seen.add(item.id);
return true;
});

const sorted = deduplicated.sort((a, b) => {
const getPriority = (h: UnifiedItem) => {
const now = new Date().getTime();
if (!h.startDate || !h.submissionDeadline) return 1;

const start = new Date(h.startDate).getTime();
const deadline = new Date(h.submissionDeadline).getTime();

if (now >= start && now <= deadline) return 0;
if (now < start) return 1;
return 2;
};

return getPriority(a) - getPriority(b);
});

return sorted;
}, [user]);

const filteredList = useMemo(() => {
if (activeTab === 'projects') return [];

let result = unifiedList;
if (activeTab === 'hackathons') {
result = unifiedList.filter(item => item.type === 'hackathon');
}
return result;
}, [unifiedList, activeTab]);

const getSubmissionStage = (hackathonId: string): SubmissionStage => {
const submission =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
user?.profile?.user?.hackathonSubmissionsAsParticipant?.find(
(s: any) => s.hackathonId === hackathonId
);

if (!submission) return 'Not Started';

const statusRaw = submission.status;
if (!statusRaw || typeof statusRaw !== 'string') return 'In Progress';

const status = statusRaw.toUpperCase();
if (status === 'DRAFT') return 'In Progress';
if (status === 'SUBMITTED') return 'Submitted';
if (status === 'UNDER_REVIEW') return 'Under Review';
if (status === 'WINNER' || status === 'COMPLETED') return 'Results Pending';

return 'In Progress';
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const handleEmptyStateClick = () => {
router.push(activeTab === 'projects' ? '/projects' : '/hackathons');
};

if (isLoading) {
return (
<div className='flex h-[400px] items-center justify-center'>
<div className='border-primary h-8 w-8 animate-spin rounded-full border-2 border-t-transparent' />
</div>
);
}

return (
<div className='container mx-auto max-w-7xl px-4 py-8 md:px-6 lg:py-12'>
<div className='mb-10 flex flex-col gap-6 md:flex-row md:items-center md:justify-between'>
<div>
<h1 className='text-3xl font-bold tracking-tight text-white md:text-4xl'>
Participating
</h1>
<p className='mt-2 text-zinc-400'>
Track your active hackathons, projects, and pending submissions.
</p>
</div>

<Tabs
value={activeTab}
onValueChange={handleTabChange}
className='w-full md:w-auto'
>
<TabsList className='relative h-11 w-full justify-start rounded-full bg-zinc-900/50 p-1 md:w-auto'>
{['all', 'hackathons', 'projects'].map(tab => (
<TabsTrigger
key={tab}
value={tab}
className={cn(
'relative z-10 h-9 rounded-full px-6 text-sm font-medium capitalize transition-colors duration-200',
activeTab === tab
? 'text-white'
: 'text-zinc-400 hover:text-zinc-200'
)}
>
{tab}
{activeTab === tab && (
<motion.div
layoutId='activeTabGlow'
className='absolute inset-0 -z-10 rounded-full bg-white/10 shadow-[0_0_15px_rgba(255,255,255,0.1)]'
initial={false}
transition={{ type: 'spring', bounce: 0.2, duration: 0.6 }}
/>
)}
</TabsTrigger>
))}
</TabsList>
</Tabs>
</div>

<AnimatePresence mode='popLayout'>
{filteredList.length > 0 ? (
<motion.div
layout
className='grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3'
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.4, ease: 'easeOut' }}
>
{filteredList.map(hackathon => (
<motion.div
key={hackathon.id}
layout
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className='group relative'
>
<div className='relative overflow-hidden rounded-4xl'>
<HackathonCard
{...hackathon}
isFullWidth
target='_blank'
className='hover:shadow-primary/5 border-white/5 transition-all duration-500 hover:border-white/20 hover:shadow-2xl'
/>
<div className='pointer-events-none absolute right-4 bottom-4 z-20 transition-transform duration-300 group-hover:scale-105'>
<ProgressIndicator
stage={getSubmissionStage(hackathon.id)}
/>
</div>
</div>
</motion.div>
))}
</motion.div>
) : (
<EmptyState
title={
activeTab === 'projects'
? 'No active projects'
: 'No active engagements'
}
description={
activeTab === 'projects'
? "You haven't participated in any projects yet. Explore our community projects to get started!"
: "You haven't participated in any hackathons yet. Explore our open events to get started!"
}
buttonText={
activeTab === 'projects'
? 'Explore Projects'
: 'Explore Hackathons'
}
onAddClick={handleEmptyStateClick}
/>
)}
</AnimatePresence>
</div>
);
}
4 changes: 2 additions & 2 deletions components/EmptyState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,11 @@ const EmptyState: React.FC<EmptyStateProps> = ({

switch (type) {
case 'compact':
return `${baseStyle} px-4 py-2 text-sm bg-[#00D2A4] text-black hover:bg-[#00B894] focus:ring-[#00D2A4] shadow-sm`;
return `${baseStyle} px-4 py-2 text-sm bg-primary text-primary-foreground hover:bg-primary/90 focus:ring-primary shadow-sm`;
case 'custom':
return `${baseStyle} px-6 py-3 bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500 shadow-md`;
default:
return `${baseStyle} px-6 py-3 bg-[#00D2A4] text-black hover:bg-[#00B894] focus:ring-[#00D2A4] shadow-[0_2px_8px_rgba(0,210,164,0.2)]`;
return `${baseStyle} px-6 py-3 bg-primary text-primary-foreground hover:bg-primary/90 focus:ring-primary shadow-[0_2px_8px_rgba(167,249,80,0.2)]`;
}
};

Expand Down
Loading
Loading