From 68cbe2132984d3692bb1c6d97ba320d4d6ff6ea5 Mon Sep 17 00:00:00 2001 From: Michaelkingsdev Date: Sat, 28 Feb 2026 19:54:05 +0100 Subject: [PATCH 01/11] feat: implement the participating page --- app/me/layout.tsx | 33 ++- app/me/participating/page.tsx | 189 ++++++++++++++++++ components/EmptyState.tsx | 4 +- components/app-sidebar.tsx | 21 +- components/hackathons/ProgressIndicator.tsx | 57 ++++++ .../landing-page/hackathon/HackathonCard.tsx | 13 +- 6 files changed, 295 insertions(+), 22 deletions(-) create mode 100644 app/me/participating/page.tsx create mode 100644 components/hackathons/ProgressIndicator.tsx diff --git a/app/me/layout.tsx b/app/me/layout.tsx index 865b6234..29e40b42 100644 --- a/app/me/layout.tsx +++ b/app/me/layout.tsx @@ -4,20 +4,12 @@ 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(); - if (isLoading) { - return ( -
- -
- ); - } - const { name = '', email = '', profile, image: userImage = '' } = user || {}; const userData = { @@ -26,6 +18,23 @@ export default function MeLayout({ children }: { children: React.ReactNode }) { image: profile?.image || userImage, }; + const hackathonsCount = useMemo(() => { + if (!profile) return 0; + const mergedIds = new Set([ + ...(profile.hackathonsAsParticipant?.map((h: any) => h.id) || []), + ...(profile.userHackathons?.map((h: any) => h.id) || []), + ]); + return mergedIds.size; + }, [profile]); + + if (isLoading) { + return ( +
+ +
+ ); + } + return ( - +
{children}
diff --git a/app/me/participating/page.tsx b/app/me/participating/page.tsx new file mode 100644 index 00000000..a9b27306 --- /dev/null +++ b/app/me/participating/page.tsx @@ -0,0 +1,189 @@ +'use client'; + +import { useMemo, useState } from 'react'; +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'; + +interface ExtendedUser { + profile?: { + hackathonsAsParticipant?: Hackathon[]; + userHackathons?: Hackathon[]; + hackathonSubmissionsAsParticipant?: Array<{ + id: string; + hackathonId: string; + status: string; + submittedAt: string; + }>; + }; +} + +export default function ParticipatingPage() { + const { user, isLoading } = useAuthStatus() as { + user: ExtendedUser | null; + isLoading: boolean; + }; + const [activeTab, setActiveTab] = useState<'all' | 'hackathons' | 'projects'>( + 'all' + ); + + const unifiedList = useMemo(() => { + if (!user?.profile) return []; + + const hackathonsAsParticipant = user.profile.hackathonsAsParticipant || []; + const userHackathons = user.profile.userHackathons || []; + + // Merge and deduplicate by ID + const merged = [...hackathonsAsParticipant, ...userHackathons]; + const seen = new Set(); + const deduplicated = merged.filter(h => { + if (seen.has(h.id)) return false; + seen.add(h.id); + return true; + }); + + // Sort logic: active/ongoing first, then upcoming, then completed + return deduplicated.sort((a, b) => { + const getPriority = (h: Hackathon) => { + const now = new Date().getTime(); + const start = new Date(h.startDate).getTime(); + const deadline = new Date(h.submissionDeadline).getTime(); + + if (now >= start && now <= deadline) return 0; // Ongoing + if (now < start) return 1; // Upcoming + return 2; // Completed + }; + + return getPriority(a) - getPriority(b); + }); + }, [user]); + + const filteredList = useMemo(() => { + if (activeTab === 'all') return unifiedList; + if (activeTab === 'hackathons') { + // In this context, "hackathons" might mean ones you are participating in vs "projects" (ones you created/lead) + // But the prompt says "Each tab filters from the unified list". + // Usually "Projects" refers to hackathons where you have a submission. + return unifiedList; // Placeholder for specific filter logic if needed + } + return unifiedList; + }, [unifiedList, activeTab]); + + const getSubmissionStage = (hackathonId: string): SubmissionStage => { + const submission = user?.profile?.hackathonSubmissionsAsParticipant?.find( + s => s.hackathonId === hackathonId + ); + + if (!submission) return 'Not Started'; + + const status = submission.status.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'; + }; + + if (isLoading) { + return ( +
+
+
+ ); + } + + return ( +
+
+
+

+ Participating +

+

+ Track your active hackathons, projects, and pending submissions. +

+
+ + setActiveTab(v as any)} + className='w-full md:w-auto' + > + + {['all', 'hackathons', 'projects'].map(tab => ( + + {tab} + {activeTab === tab && ( + + )} + + ))} + + +
+ + + {filteredList.length > 0 ? ( + + {filteredList.map(hackathon => ( + + +
+ +
+
+ ))} +
+ ) : ( + (window.location.href = '/hackathons')} + /> + )} +
+
+ ); +} diff --git a/components/EmptyState.tsx b/components/EmptyState.tsx index 2796bca4..ba50d859 100644 --- a/components/EmptyState.tsx +++ b/components/EmptyState.tsx @@ -66,11 +66,11 @@ const EmptyState: React.FC = ({ 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)]`; } }; diff --git a/components/app-sidebar.tsx b/components/app-sidebar.tsx index ad72176a..ba762952 100644 --- a/components/app-sidebar.tsx +++ b/components/app-sidebar.tsx @@ -27,7 +27,7 @@ import { import Image from 'next/image'; import Link from 'next/link'; -const navigationData = { +const getNavigationData = (counts?: { participating?: number }) => ({ main: [ { title: 'Overview', @@ -56,9 +56,9 @@ const navigationData = { hackathons: [ { title: 'Participating', - url: '/me/hackathons', + url: '/me/participating', icon: IconShieldCheck, - badge: '2', + badge: counts?.participating?.toString(), }, { title: 'Submissions', @@ -91,16 +91,27 @@ const navigationData = { badge: '5', }, ], -}; +}); + interface userData { name: string; email: string; image: string; } + export function AppSidebar({ user, + counts, ...props -}: { user: userData } & React.ComponentProps) { +}: { + user: userData; + counts?: { participating?: number }; +} & React.ComponentProps) { + const navigationData = React.useMemo( + () => getNavigationData(counts), + [counts] + ); + return (
diff --git a/components/hackathons/ProgressIndicator.tsx b/components/hackathons/ProgressIndicator.tsx new file mode 100644 index 00000000..bf7e5ac3 --- /dev/null +++ b/components/hackathons/ProgressIndicator.tsx @@ -0,0 +1,57 @@ +'use client'; + +import React from 'react'; +import { cn } from '@/lib/utils'; + +export type SubmissionStage = + | 'Not Started' + | 'In Progress' + | 'Submitted' + | 'Under Review' + | 'Results Pending'; + +interface ProgressIndicatorProps { + stage: SubmissionStage; + className?: string; +} + +const STAGE_CONFIG: Record = + { + 'Not Started': { + color: 'bg-zinc-500/20 text-zinc-400', + label: 'Not Started', + }, + 'In Progress': { + color: 'bg-blue-500/20 text-blue-400', + label: 'In Progress', + }, + Submitted: { color: 'bg-green-500/20 text-green-400', label: 'Submitted' }, + 'Under Review': { + color: 'bg-purple-500/20 text-purple-400', + label: 'Under Review', + }, + 'Results Pending': { + color: 'bg-yellow-500/20 text-yellow-400', + label: 'Results Pending', + }, + }; + +export function ProgressIndicator({ + stage, + className, +}: ProgressIndicatorProps) { + const config = STAGE_CONFIG[stage]; + + return ( +
+
+ {config.label} +
+ ); +} diff --git a/components/landing-page/hackathon/HackathonCard.tsx b/components/landing-page/hackathon/HackathonCard.tsx index dd135c30..8355acf5 100644 --- a/components/landing-page/hackathon/HackathonCard.tsx +++ b/components/landing-page/hackathon/HackathonCard.tsx @@ -4,6 +4,7 @@ import Image from 'next/image'; import { MapPinIcon } from 'lucide-react'; import { useEffect, useState } from 'react'; import { Hackathon } from '@/lib/api/hackathons'; +import { cn } from '@/lib/utils'; // type HackathonCardProps = { // id: string; @@ -176,8 +177,8 @@ function HackathonCard({ categories, prizeTiers, isFullWidth = false, - // className, -}: Hackathon & { isFullWidth?: boolean }) { + className, +}: Hackathon & { isFullWidth?: boolean; className?: string }) { const router = useRouter(); const [timeRemaining, setTimeRemaining] = useState({ days: 0, @@ -386,9 +387,11 @@ function HackathonCard({ return (
{/* Image */}
From 504a581a42a7a40e8018e265fe1eb36962fe5880 Mon Sep 17 00:00:00 2001 From: Michaelkingsdev Date: Sun, 1 Mar 2026 20:30:34 +0100 Subject: [PATCH 02/11] fix: fix participating page and ui refinements --- app/me/layout.tsx | 9 +- app/me/participating/page.tsx | 126 +++++++++++++----- .../landing-page/hackathon/HackathonCard.tsx | 42 ++++-- components/profile/ProfileDataClient.tsx | 4 +- components/profile/ProfileOverview.tsx | 8 +- 5 files changed, 134 insertions(+), 55 deletions(-) diff --git a/app/me/layout.tsx b/app/me/layout.tsx index 29e40b42..3a3958ca 100644 --- a/app/me/layout.tsx +++ b/app/me/layout.tsx @@ -20,11 +20,10 @@ export default function MeLayout({ children }: { children: React.ReactNode }) { const hackathonsCount = useMemo(() => { if (!profile) return 0; - const mergedIds = new Set([ - ...(profile.hackathonsAsParticipant?.map((h: any) => h.id) || []), - ...(profile.userHackathons?.map((h: any) => h.id) || []), - ]); - return mergedIds.size; + + const joined = (profile as any)?.user?.joinedHackathons || []; + + return joined.length; }, [profile]); if (isLoading) { diff --git a/app/me/participating/page.tsx b/app/me/participating/page.tsx index a9b27306..7ab48c07 100644 --- a/app/me/participating/page.tsx +++ b/app/me/participating/page.tsx @@ -36,50 +36,92 @@ export default function ParticipatingPage() { ); const unifiedList = useMemo(() => { - if (!user?.profile) return []; + if (!user?.profile) { + return []; + } + + const profile = user.profile as any; + const joinedHackathons = profile.user?.joinedHackathons || []; + const hackathonsAsParticipant = profile.hackathonsAsParticipant || []; + const submissions = profile.user?.hackathonSubmissionsAsParticipant || []; + + // Map hackathons from joined list + const typedJoinedHackathons = joinedHackathons.map((h: any) => { + const hackathonData = h.hackathon || h; + return { + ...hackathonData, + type: 'hackathon' as const, + }; + }); + + // Map hackathons from participating list (preferred source based on logs) + const typedParticipatingHackathons = hackathonsAsParticipant.map( + (p: any) => { + const hackathonData = p.hackathon; + return { + ...hackathonData, + type: 'hackathon' as const, + }; + } + ); - const hackathonsAsParticipant = user.profile.hackathonsAsParticipant || []; - const userHackathons = user.profile.userHackathons || []; + // Map hackathons from submissions + const typedSubmissionHackathons = submissions + .filter((s: any) => s.hackathon) + .map((s: any) => ({ + ...s.hackathon, + type: 'hackathon' as const, + })); // Merge and deduplicate by ID - const merged = [...hackathonsAsParticipant, ...userHackathons]; + const merged = [ + ...typedParticipatingHackathons, + ...typedJoinedHackathons, + ...typedSubmissionHackathons, + ]; + const seen = new Set(); - const deduplicated = merged.filter(h => { - if (seen.has(h.id)) return false; - seen.add(h.id); + const deduplicated = merged.filter((item: any) => { + if (!item.id || seen.has(item.id)) return false; + seen.add(item.id); return true; }); // Sort logic: active/ongoing first, then upcoming, then completed - return deduplicated.sort((a, b) => { - const getPriority = (h: Hackathon) => { + const sorted = deduplicated.sort((a: any, b: any) => { + const getPriority = (h: any) => { 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; // Ongoing - if (now < start) return 1; // Upcoming - return 2; // Completed + 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 === 'all') return unifiedList; + if (activeTab === 'projects') return []; // Keep projects tab empty as requested + + let result = unifiedList; if (activeTab === 'hackathons') { - // In this context, "hackathons" might mean ones you are participating in vs "projects" (ones you created/lead) - // But the prompt says "Each tab filters from the unified list". - // Usually "Projects" refers to hackathons where you have a submission. - return unifiedList; // Placeholder for specific filter logic if needed + result = unifiedList.filter((item: any) => item.type === 'hackathon'); } - return unifiedList; + return result; }, [unifiedList, activeTab]); const getSubmissionStage = (hackathonId: string): SubmissionStage => { - const submission = user?.profile?.hackathonSubmissionsAsParticipant?.find( - s => s.hackathonId === hackathonId + const submission = ( + user?.profile as any + )?.user?.hackathonSubmissionsAsParticipant?.find( + (s: any) => s.hackathonId === hackathonId ); if (!submission) return 'Not Started'; @@ -162,25 +204,45 @@ export default function ParticipatingPage() { initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} - className='relative' + className='group relative' > - -
- +
+ +
+ +
))} ) : ( (window.location.href = '/hackathons')} + title={ + activeTab === 'projects' + ? 'No active projects' + : 'No active engagements' + } + description={ + activeTab === 'projects' + ? "You haven't participating in any projects yet. Explore our community projects to get started!" + : "You haven't joined any hackathons yet. Explore our open events to get started!" + } + buttonText={ + activeTab === 'projects' + ? 'Explore Projects' + : 'Explore Hackathons' + } + onAddClick={() => + (window.location.href = + activeTab === 'projects' ? '/projects' : '/hackathons') + } /> )} diff --git a/components/landing-page/hackathon/HackathonCard.tsx b/components/landing-page/hackathon/HackathonCard.tsx index 8355acf5..0363ef90 100644 --- a/components/landing-page/hackathon/HackathonCard.tsx +++ b/components/landing-page/hackathon/HackathonCard.tsx @@ -158,7 +158,13 @@ function calculateTimeRemaining(targetDate: string): TimeRemaining { // } // } -function HackathonCard({ +interface HackathonCardProps extends Hackathon { + isFullWidth?: boolean; + className?: string; + target?: string; +} + +export const HackathonCard = ({ id, slug, name, @@ -178,7 +184,8 @@ function HackathonCard({ prizeTiers, isFullWidth = false, className, -}: Hackathon & { isFullWidth?: boolean; className?: string }) { + target, +}: HackathonCardProps) => { const router = useRouter(); const [timeRemaining, setTimeRemaining] = useState({ days: 0, @@ -190,7 +197,12 @@ function HackathonCard({ const handleClick = () => { const slugPath = slug || id || ''; - router.push(`/hackathons/${slugPath}`); + const url = `/hackathons/${slugPath}`; + if (target === '_blank') { + window.open(url, '_blank'); + } else { + router.push(url); + } }; // Determine top badge status using raw dates @@ -353,9 +365,9 @@ function HackathonCard({ // })(); const CategoriesDisplay = ({ - categoriesList, + categoriesList = [], }: { - categoriesList: string[]; + categoriesList?: string[]; }) => { const MAX_VISIBLE = 3; @@ -414,13 +426,17 @@ function HackathonCard({
-
- - {organization.name} - + {organization?.logo && ( +
+ )} + {organization?.name && ( + + {organization.name} + + )}
@@ -481,6 +497,6 @@ function HackathonCard({
); -} +}; export default HackathonCard; diff --git a/components/profile/ProfileDataClient.tsx b/components/profile/ProfileDataClient.tsx index 1e2dd6fe..b5507cf9 100644 --- a/components/profile/ProfileDataClient.tsx +++ b/components/profile/ProfileDataClient.tsx @@ -46,8 +46,8 @@ export default function ProfileDataClient({ user }: ProfileDataClientProps) { const isOwnProfile = userData.id === currentUser?.id; const organizationsData = userData.members?.map(org => ({ - name: org.organization.name, - avatarUrl: org.organization.logo || '/blog1.jpg', + name: org.organization?.name || 'Unknown Organization', + avatarUrl: org.organization?.logo || '/blog1.jpg', })) || []; return ( diff --git a/components/profile/ProfileOverview.tsx b/components/profile/ProfileOverview.tsx index a966adf7..3113e36f 100644 --- a/components/profile/ProfileOverview.tsx +++ b/components/profile/ProfileOverview.tsx @@ -21,9 +21,11 @@ export default function ProfileOverview({ isAuthenticated, isOwnProfile, }: ProfileOverviewProps) { + const nameParts = user.name?.split(' ') || []; const profileData: UserProfile = { username: user.username, - displayName: `${user.name?.split(' ')[0] || ''} ${user.name?.split(' ').slice(1).join(' ') || ''}`, + displayName: + `${nameParts[0] || ''} ${nameParts.slice(1).join(' ') || ''}`.trim(), bio: user.profile?.bio || 'No bio available', avatarUrl: user.image || '/', socialLinks: user.profile?.socialLinks || {}, @@ -39,8 +41,8 @@ export default function ProfileOverview({ const organizationsData: Organization[] = user.members?.map(org => { return { - name: org.organization.name, - avatarUrl: org.organization.logo || '/blog1.jpg', + name: org.organization?.name || 'Unknown Organization', + avatarUrl: org.organization?.logo || '/blog1.jpg', id: org.organizationId, }; }) || []; From 8f2010db57d03000c99401d9139fe4ae66aa15c6 Mon Sep 17 00:00:00 2001 From: Michaelkingsdev Date: Sun, 1 Mar 2026 21:37:39 +0100 Subject: [PATCH 03/11] fix: fix sidebar active links and rstored earnings page --- app/me/earnings/page.tsx | 271 +++++++++++++++++++++++++++++++++++++ components/app-sidebar.tsx | 6 + components/nav-main.tsx | 3 +- lib/api/user/earnings.ts | 64 +++++++++ 4 files changed, 343 insertions(+), 1 deletion(-) create mode 100644 app/me/earnings/page.tsx create mode 100644 lib/api/user/earnings.ts diff --git a/app/me/earnings/page.tsx b/app/me/earnings/page.tsx new file mode 100644 index 00000000..8cf55434 --- /dev/null +++ b/app/me/earnings/page.tsx @@ -0,0 +1,271 @@ +'use client'; + +import React, { useEffect, useState } from 'react'; +import { motion } from 'framer-motion'; +import { + IconCurrencyDollar, + IconClock, + IconCheck, + IconTrophy, + IconBriefcase, + IconUsers, + IconTarget, +} from '@tabler/icons-react'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { Skeleton } from '@/components/ui/skeleton'; +import { + getUserEarnings, + EarningsData, + EarningActivity, +} from '@/lib/api/user/earnings'; +import { toast } from 'sonner'; + +/** + * Interface for SummaryCard props. + */ +interface SummaryCardProps { + title: string; + value: string; + icon: React.ReactNode; + description: string; +} + +/** + * SummaryCard component for displaying high-level stats. + */ +const SummaryCard: React.FC = ({ + title, + value, + icon, + description, +}) => ( + + + {title} + {icon} + + +
{value}
+

{description}

+
+
+); + +/** + * Interface for BreakdownItem props. + */ +interface BreakdownItemProps { + label: string; + value: number; + icon: React.ReactNode; +} + +/** + * BreakdownItem component for showing source-specific earnings. + */ +const BreakdownItem: React.FC = ({ + label, + value, + icon, +}) => ( +
+
+
+ {icon} +
+ {label} +
+ ${value.toLocaleString()} +
+); + +/** + * Interface for ActivityItem props. + */ +interface ActivityItemProps { + activity: EarningActivity; +} + +/** + * ActivityItem component for displaying a single reward entry. + */ +const ActivityItem: React.FC = ({ activity }) => ( +
+
+
{activity.title}
+
+ {activity.source} + + {new Date(activity.occurredAt).toLocaleDateString()} +
+
+
+

${activity.amount.toLocaleString()}

+ {activity.currency && ( +

{activity.currency}

+ )} +
+
+); + +/** + * EarningsSkeleton component for loading states. + */ +const EarningsSkeleton: React.FC = () => ( +
+
+ + +
+
+ + + +
+
+ + +
+
+); + +/** + * EarningsPage component for managing and tracking user rewards. + */ +const EarningsPage: React.FC = () => { + const [loading, setLoading] = useState(true); + const [data, setData] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + const res = await getUserEarnings(); + if (res.success && res.data) { + setData(res.data); + } + } catch (error) { + console.error('Failed to fetch earnings:', error); + toast.error('Failed to load earnings data'); + } finally { + setLoading(false); + } + }; + fetchData(); + }, []); + + if (loading) { + return ; + } + + if (!data) { + return ( +
+

+ No earnings data found. +

+
+ ); + } + + return ( +
+ +

+ Earnings Dashboard +

+

+ Manage and track your rewards across the platform. +

+
+ + {/* Summary Cards */} +
+ } + description='Lifetime earnings' + /> + } + description='Awaiting processing' + /> + } + description='Successfully cashed out' + /> +
+ +
+ {/* Breakdown */} + + + Source Breakdown + + Earnings categorized by activity type + + + + } + /> + } + /> + } + /> + } + /> + + + + {/* Activity Feed */} + + + Recent Activity + Your latest wins and rewards + + +
+ {data.activities.length === 0 ? ( +

+ No recent activity found. +

+ ) : ( + data.activities.map(activity => ( + + )) + )} +
+
+
+
+
+ ); +}; + +export default EarningsPage; diff --git a/components/app-sidebar.tsx b/components/app-sidebar.tsx index ba762952..3005e5ff 100644 --- a/components/app-sidebar.tsx +++ b/components/app-sidebar.tsx @@ -4,6 +4,7 @@ import * as React from 'react'; import { IconBell, IconChartBar, + IconCurrencyDollar, IconDashboard, IconFileText, IconFolder, @@ -39,6 +40,11 @@ const getNavigationData = (counts?: { participating?: number }) => ({ url: '/me/analytics', icon: IconChartBar, }, + { + title: 'Earnings', + url: '/me/earnings', + icon: IconCurrencyDollar, + }, ], projects: [ { diff --git a/components/nav-main.tsx b/components/nav-main.tsx index 0bec9fcb..0648e509 100644 --- a/components/nav-main.tsx +++ b/components/nav-main.tsx @@ -41,7 +41,8 @@ export function NavMain({ {items.map(item => { const isActive = - pathname === item.url || pathname?.startsWith(`${item.url}/`); + pathname === item.url || + (item.url !== '/me' && pathname?.startsWith(`${item.url}/`)); return ( diff --git a/lib/api/user/earnings.ts b/lib/api/user/earnings.ts new file mode 100644 index 00000000..145aaf41 --- /dev/null +++ b/lib/api/user/earnings.ts @@ -0,0 +1,64 @@ +import { api } from '../api'; +import { ApiResponse } from '../types'; + +export interface EarningActivity { + id: string; + source: 'hackathons' | 'grants' | 'crowdfunding' | 'bounties'; + title: string; + amount: number; + currency: string; + occurredAt: string; +} + +export interface EarningsData { + summary: { + totalEarned: number; + pendingWithdrawal: number; + completedWithdrawal: number; + }; + breakdown: { + hackathons: number; + grants: number; + crowdfunding: number; + bounties: number; + }; + activities: EarningActivity[]; +} + +export interface GetEarningsResponse extends ApiResponse { + success: true; + data: EarningsData; +} + +export interface ClaimEarningRequest { + activityId: string; +} + +export interface ClaimEarningResponse extends ApiResponse { + success: boolean; + message: string; + data?: { + transactionHash: string; + }; +} + +/** + * Get user earnings data + */ +export const getUserEarnings = async (): Promise => { + const res = await api.get('/users/earnings'); + return res.data; +}; + +/** + * Claim a specific earning + */ +export const claimEarning = async ( + data: ClaimEarningRequest +): Promise => { + const res = await api.post( + '/users/earnings/claim', + data + ); + return res.data; +}; From e038b15ee5f154b4fc907da76d8ce5a0323e48db Mon Sep 17 00:00:00 2001 From: Michaelkingsdev Date: Sun, 1 Mar 2026 22:06:30 +0100 Subject: [PATCH 04/11] fix: fix coderabbit corrections --- app/me/earnings/page.tsx | 12 +- app/me/layout.tsx | 4 +- app/me/participating/page.tsx | 103 +++++++++--------- components/app-sidebar.tsx | 11 +- components/hackathons/ProgressIndicator.tsx | 2 +- .../landing-page/hackathon/HackathonCard.tsx | 2 +- components/nav-user.tsx | 6 +- hooks/use-auth.ts | 2 + lib/api/types.ts | 16 +++ lib/api/user/earnings.ts | 7 +- 10 files changed, 98 insertions(+), 67 deletions(-) diff --git a/app/me/earnings/page.tsx b/app/me/earnings/page.tsx index 8cf55434..7174a242 100644 --- a/app/me/earnings/page.tsx +++ b/app/me/earnings/page.tsx @@ -81,7 +81,9 @@ const BreakdownItem: React.FC = ({
{label}
- ${value.toLocaleString()} + + ${(Number(value) || 0).toLocaleString()} +
); @@ -106,7 +108,9 @@ const ActivityItem: React.FC = ({ activity }) => (
-

${activity.amount.toLocaleString()}

+

+ ${(Number(activity.amount) || 0).toLocaleString()} +

{activity.currency && (

{activity.currency}

)} @@ -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); diff --git a/app/me/layout.tsx b/app/me/layout.tsx index 3a3958ca..bcb176d3 100644 --- a/app/me/layout.tsx +++ b/app/me/layout.tsx @@ -15,12 +15,14 @@ export default function MeLayout({ children }: { children: React.ReactNode }) { const userData = { name: name || '', email, - image: profile?.image || userImage, + 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; diff --git a/app/me/participating/page.tsx b/app/me/participating/page.tsx index 7ab48c07..1c5886a2 100644 --- a/app/me/participating/page.tsx +++ b/app/me/participating/page.tsx @@ -1,6 +1,7 @@ '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'; @@ -13,61 +14,59 @@ import { cn } from '@/lib/utils'; import { Hackathon } from '@/lib/api/hackathons'; import EmptyState from '@/components/EmptyState'; -interface ExtendedUser { - profile?: { - hackathonsAsParticipant?: Hackathon[]; - userHackathons?: Hackathon[]; - hackathonSubmissionsAsParticipant?: Array<{ - id: string; - hackathonId: string; - status: string; - submittedAt: string; - }>; - }; +type TabType = 'all' | 'hackathons' | 'projects'; + +interface UnifiedItem extends Hackathon { + type: 'hackathon'; } export default function ParticipatingPage() { - const { user, isLoading } = useAuthStatus() as { - user: ExtendedUser | null; - isLoading: boolean; + const router = useRouter(); + const { user, isLoading } = useAuthStatus(); + const [activeTab, setActiveTab] = useState('all'); + + const handleTabChange = (value: string) => { + setActiveTab(value as TabType); }; - const [activeTab, setActiveTab] = useState<'all' | 'hackathons' | 'projects'>( - 'all' - ); - const unifiedList = useMemo(() => { - if (!user?.profile) { + const unifiedList = useMemo(() => { + const profile = user?.profile; + if (!profile) { return []; } - const profile = user.profile as any; const joinedHackathons = profile.user?.joinedHackathons || []; const hackathonsAsParticipant = profile.hackathonsAsParticipant || []; const submissions = profile.user?.hackathonSubmissionsAsParticipant || []; // Map hackathons from joined list - const typedJoinedHackathons = joinedHackathons.map((h: any) => { - const hackathonData = h.hackathon || h; - return { - ...hackathonData, - type: 'hackathon' as const, - }; - }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const typedJoinedHackathons: UnifiedItem[] = joinedHackathons.map( + (h: any) => { + const hackathonData = h.hackathon || h; + return { + ...hackathonData, + type: 'hackathon' as const, + }; + } + ); // Map hackathons from participating list (preferred source based on logs) - const typedParticipatingHackathons = hackathonsAsParticipant.map( - (p: any) => { + const typedParticipatingHackathons: UnifiedItem[] = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + hackathonsAsParticipant.map((p: any) => { const hackathonData = p.hackathon; return { ...hackathonData, type: 'hackathon' as const, }; - } - ); + }); // Map hackathons from submissions - const typedSubmissionHackathons = 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, @@ -80,16 +79,16 @@ export default function ParticipatingPage() { ...typedSubmissionHackathons, ]; - const seen = new Set(); - const deduplicated = merged.filter((item: any) => { + const seen = new Set(); + const deduplicated = merged.filter(item => { if (!item.id || seen.has(item.id)) return false; seen.add(item.id); return true; }); // Sort logic: active/ongoing first, then upcoming, then completed - const sorted = deduplicated.sort((a: any, b: any) => { - const getPriority = (h: any) => { + const sorted = deduplicated.sort((a, b) => { + const getPriority = (h: UnifiedItem) => { const now = new Date().getTime(); if (!h.startDate || !h.submissionDeadline) return 1; @@ -112,21 +111,24 @@ export default function ParticipatingPage() { let result = unifiedList; if (activeTab === 'hackathons') { - result = unifiedList.filter((item: any) => item.type === 'hackathon'); + result = unifiedList.filter(item => item.type === 'hackathon'); } return result; }, [unifiedList, activeTab]); const getSubmissionStage = (hackathonId: string): SubmissionStage => { - const submission = ( - user?.profile as any - )?.user?.hackathonSubmissionsAsParticipant?.find( - (s: any) => s.hackathonId === hackathonId - ); + 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 status = submission.status.toUpperCase(); + 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'; @@ -135,6 +137,10 @@ export default function ParticipatingPage() { return 'In Progress'; }; + const handleEmptyStateClick = () => { + router.push(activeTab === 'projects' ? '/projects' : '/hackathons'); + }; + if (isLoading) { return (
@@ -157,7 +163,7 @@ export default function ParticipatingPage() { setActiveTab(v as any)} + onValueChange={handleTabChange} className='w-full md:w-auto' > @@ -231,18 +237,15 @@ export default function ParticipatingPage() { } description={ activeTab === 'projects' - ? "You haven't participating in any projects yet. Explore our community projects to get started!" - : "You haven't joined any hackathons yet. Explore our open events to get started!" + ? "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={() => - (window.location.href = - activeTab === 'projects' ? '/projects' : '/hackathons') - } + onAddClick={handleEmptyStateClick} /> )} diff --git a/components/app-sidebar.tsx b/components/app-sidebar.tsx index 3005e5ff..bdc0ce57 100644 --- a/components/app-sidebar.tsx +++ b/components/app-sidebar.tsx @@ -64,7 +64,10 @@ const getNavigationData = (counts?: { participating?: number }) => ({ title: 'Participating', url: '/me/participating', icon: IconShieldCheck, - badge: counts?.participating?.toString(), + badge: + counts?.participating && counts.participating > 0 + ? counts.participating.toString() + : undefined, }, { title: 'Submissions', @@ -99,10 +102,10 @@ const getNavigationData = (counts?: { participating?: number }) => ({ ], }); -interface userData { +interface UserData { name: string; email: string; - image: string; + image: string | null; } export function AppSidebar({ @@ -110,7 +113,7 @@ export function AppSidebar({ counts, ...props }: { - user: userData; + user: UserData; counts?: { participating?: number }; } & React.ComponentProps) { const navigationData = React.useMemo( diff --git a/components/hackathons/ProgressIndicator.tsx b/components/hackathons/ProgressIndicator.tsx index bf7e5ac3..98748abd 100644 --- a/components/hackathons/ProgressIndicator.tsx +++ b/components/hackathons/ProgressIndicator.tsx @@ -50,7 +50,7 @@ export function ProgressIndicator({ className )} > -
+
{config.label}
); diff --git a/components/landing-page/hackathon/HackathonCard.tsx b/components/landing-page/hackathon/HackathonCard.tsx index 0363ef90..f46339dc 100644 --- a/components/landing-page/hackathon/HackathonCard.tsx +++ b/components/landing-page/hackathon/HackathonCard.tsx @@ -345,7 +345,7 @@ export const HackathonCard = ({ return () => clearInterval(interval); } - }, [status, startDate, submissionDeadline]); + }, [status, startDate, submissionDeadline, getTopBadgeStatus]); const bottomStatusInfo = getBottomStatusInfo(); const topBadgeStatus = getTopBadgeStatus(); diff --git a/components/nav-user.tsx b/components/nav-user.tsx index 9abede73..183f684f 100644 --- a/components/nav-user.tsx +++ b/components/nav-user.tsx @@ -32,7 +32,7 @@ export function NavUser({ user: { name: string; email: string; - image: string; + image: string | null; }; }) { const { isMobile } = useSidebar(); @@ -47,7 +47,7 @@ export function NavUser({ className='data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground hover:bg-sidebar-accent/50 transition-colors' > - + {user.name .split(' ') @@ -75,7 +75,7 @@ export function NavUser({
- + {user.name .split(' ') diff --git a/hooks/use-auth.ts b/hooks/use-auth.ts index de50467c..fa402b30 100644 --- a/hooks/use-auth.ts +++ b/hooks/use-auth.ts @@ -11,6 +11,7 @@ export function useAuth(requireAuth = true) { } = authClient.useSession(); const router = useRouter(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const [userProfile, setUserProfile] = useState(null); const [profileLoading, setProfileLoading] = useState(false); @@ -97,6 +98,7 @@ export function useOptionalAuth() { export function useAuthStatus() { const { data: session, isPending: sessionPending } = authClient.useSession(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const [userProfile, setUserProfile] = useState(null); const [profileLoading, setProfileLoading] = useState(false); diff --git a/lib/api/types.ts b/lib/api/types.ts index 45584207..3c6d39a2 100644 --- a/lib/api/types.ts +++ b/lib/api/types.ts @@ -111,6 +111,15 @@ export interface User { status: string; rank?: number | null; submittedAt: string; + hackathonId: string; + hackathon?: any; + }>; + joinedHackathons?: Array<{ + id: string; + userId: string; + hackathonId: string; + registrationDate: string; + hackathon?: any; }>; profile?: Record; stats?: { @@ -252,6 +261,13 @@ export interface GetMeResponse { project?: Record; organization?: Record; }>; + hackathonsAsParticipant?: Array<{ + id: string; + hackathonId: string; + participantId: string; + status: string; + hackathon?: any; + }>; } // Logout diff --git a/lib/api/user/earnings.ts b/lib/api/user/earnings.ts index 145aaf41..31e5a90a 100644 --- a/lib/api/user/earnings.ts +++ b/lib/api/user/earnings.ts @@ -25,10 +25,9 @@ export interface EarningsData { activities: EarningActivity[]; } -export interface GetEarningsResponse extends ApiResponse { - success: true; - data: EarningsData; -} +export type GetEarningsResponse = + | { success: true; data: EarningsData; message?: string } + | { success: false; error: string; message?: string }; export interface ClaimEarningRequest { activityId: string; From 955b8d3678fb4f9b31336783e01282fb79d16a82 Mon Sep 17 00:00:00 2001 From: Michaelkingsdev Date: Mon, 2 Mar 2026 11:26:54 +0100 Subject: [PATCH 05/11] fix: fix coderabbit corrections --- app/me/participating/page.tsx | 34 ++-- .../landing-page/hackathon/HackathonCard.tsx | 9 +- package-lock.json | 173 +++++++++++++----- 3 files changed, 152 insertions(+), 64 deletions(-) diff --git a/app/me/participating/page.tsx b/app/me/participating/page.tsx index 1c5886a2..142b2613 100644 --- a/app/me/participating/page.tsx +++ b/app/me/participating/page.tsx @@ -39,28 +39,31 @@ export default function ParticipatingPage() { const hackathonsAsParticipant = profile.hackathonsAsParticipant || []; const submissions = profile.user?.hackathonSubmissionsAsParticipant || []; - // Map hackathons from joined list // eslint-disable-next-line @typescript-eslint/no-explicit-any - const typedJoinedHackathons: UnifiedItem[] = joinedHackathons.map( - (h: 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 (preferred source based on logs) - const typedParticipatingHackathons: UnifiedItem[] = + // 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 - hackathonsAsParticipant.map((p: any) => { - const hackathonData = p.hackathon; - return { - ...hackathonData, - type: 'hackathon' as const, - }; - }); + .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 @@ -86,7 +89,6 @@ export default function ParticipatingPage() { return true; }); - // Sort logic: active/ongoing first, then upcoming, then completed const sorted = deduplicated.sort((a, b) => { const getPriority = (h: UnifiedItem) => { const now = new Date().getTime(); @@ -107,7 +109,7 @@ export default function ParticipatingPage() { }, [user]); const filteredList = useMemo(() => { - if (activeTab === 'projects') return []; // Keep projects tab empty as requested + if (activeTab === 'projects') return []; let result = unifiedList; if (activeTab === 'hackathons') { diff --git a/components/landing-page/hackathon/HackathonCard.tsx b/components/landing-page/hackathon/HackathonCard.tsx index f46339dc..e700bfb8 100644 --- a/components/landing-page/hackathon/HackathonCard.tsx +++ b/components/landing-page/hackathon/HackathonCard.tsx @@ -2,7 +2,7 @@ import { useRouter } from 'nextjs-toploader/app'; import Image from 'next/image'; import { MapPinIcon } from 'lucide-react'; -import { useEffect, useState } from 'react'; +import { useEffect, useState, useCallback } from 'react'; import { Hackathon } from '@/lib/api/hackathons'; import { cn } from '@/lib/utils'; @@ -205,8 +205,9 @@ export const HackathonCard = ({ } }; - // Determine top badge status using raw dates - const getTopBadgeStatus = () => { + // Determine top badge status using raw dates — memoised so it can safely + // appear in the useEffect dependency array without triggering infinite loops. + const getTopBadgeStatus = useCallback(() => { if (status === 'ARCHIVED') { return 'Archived'; } @@ -229,7 +230,7 @@ export const HackathonCard = ({ // Otherwise it's upcoming return 'Upcoming'; - }; + }, [status, startDate, submissionDeadline]); const getTopBadgeColor = () => { const badgeStatus = getTopBadgeStatus(); diff --git a/package-lock.json b/package-lock.json index 6c07d737..65287815 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6198,9 +6198,9 @@ } }, "node_modules/@trezor/connect": { - "version": "9.7.1", - "resolved": "https://registry.npmjs.org/@trezor/connect/-/connect-9.7.1.tgz", - "integrity": "sha512-W2ym0bs4FVmXByEr9gANBp+bRErzNcmqqqYzSJLOVkawxikqYXag2aCpdiXU3LlZbFbhFhIsT/fpDLfwiLRySA==", + "version": "9.7.2", + "resolved": "https://registry.npmjs.org/@trezor/connect/-/connect-9.7.2.tgz", + "integrity": "sha512-Sn6F4mNH+yi2vAHy29kwhs50bRLn92drg3znm3pkY+8yEBxI4MmuP8sKYjdgUEJnQflWh80KlcvEDeVa4olVRA==", "license": "SEE LICENSE IN LICENSE.md", "peer": true, "dependencies": { @@ -6216,18 +6216,18 @@ "@solana-program/token-2022": "^0.4.2", "@solana/kit": "^2.3.0", "@trezor/blockchain-link": "2.6.1", - "@trezor/blockchain-link-types": "1.5.0", - "@trezor/blockchain-link-utils": "1.5.1", + "@trezor/blockchain-link-types": "1.5.1", + "@trezor/blockchain-link-utils": "1.5.2", "@trezor/connect-analytics": "1.4.0", - "@trezor/connect-common": "0.5.0", + "@trezor/connect-common": "0.5.1", "@trezor/crypto-utils": "1.2.0", - "@trezor/device-authenticity": "1.1.1", + "@trezor/device-authenticity": "1.1.2", "@trezor/device-utils": "1.2.0", "@trezor/env-utils": "^1.5.0", - "@trezor/protobuf": "1.5.1", + "@trezor/protobuf": "1.5.2", "@trezor/protocol": "1.3.0", "@trezor/schema-utils": "1.4.0", - "@trezor/transport": "1.6.1", + "@trezor/transport": "1.6.2", "@trezor/type-utils": "1.2.0", "@trezor/utils": "9.5.0", "@trezor/utxo-lib": "2.5.0", @@ -6256,9 +6256,9 @@ } }, "node_modules/@trezor/connect-common": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@trezor/connect-common/-/connect-common-0.5.0.tgz", - "integrity": "sha512-WE71iaFcWmfQxDCiTUNynj2DccRgUiLBJ+g3nrqCBJqEYzu+cD6eZ5k/OLtZ3hfh5gyB5EQwXdGvRT07iNdxAA==", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@trezor/connect-common/-/connect-common-0.5.1.tgz", + "integrity": "sha512-wdpVCwdylBh4SBO5Ys40tB/d59UlfjmxgBHDkkLgaR+JcqkthCfiw5VlUrV9wu65lquejAZhA5KQL4mUUUhCow==", "license": "SEE LICENSE IN LICENSE.md", "peer": true, "dependencies": { @@ -6614,6 +6614,74 @@ "base-x": "^5.0.0" } }, + "node_modules/@trezor/connect/node_modules/@stellar/stellar-sdk": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-14.2.0.tgz", + "integrity": "sha512-7nh2ogzLRMhfkIC0fGjn1LHUzk3jqVw8tjAuTt5ADWfL9CSGBL18ILucE9igz2L/RU2AZgeAvhujAnW91Ut/oQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@stellar/stellar-base": "^14.0.1", + "axios": "^1.12.2", + "bignumber.js": "^9.3.1", + "eventsource": "^2.0.2", + "feaxios": "^0.0.23", + "randombytes": "^2.1.0", + "toml": "^3.0.0", + "urijs": "^1.19.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@trezor/connect/node_modules/@trezor/blockchain-link-types": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@trezor/blockchain-link-types/-/blockchain-link-types-1.5.1.tgz", + "integrity": "sha512-Idavz6LwLBW8sXc69fh5AJEnl666EDl2Nt3io7updvBgOR0/P12I900DgjNhCKtiWuv66A33/5RE7zLcj3lfnw==", + "license": "See LICENSE.md in repo root", + "peer": true, + "dependencies": { + "@trezor/utils": "9.5.0", + "@trezor/utxo-lib": "2.5.0" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/connect/node_modules/@trezor/blockchain-link-utils": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@trezor/blockchain-link-utils/-/blockchain-link-utils-1.5.2.tgz", + "integrity": "sha512-OSS5OEE98FMnYfjoEALPjBt7ebjC/FKnq3HOolHdEWXBpVlXZNN2+Vo1R9J6WbZUU087sHuUTJJy/GJYWY13Tg==", + "license": "See LICENSE.md in repo root", + "peer": true, + "dependencies": { + "@mobily/ts-belt": "^3.13.1", + "@stellar/stellar-sdk": "14.2.0", + "@trezor/env-utils": "1.5.0", + "@trezor/protobuf": "1.5.2", + "@trezor/utils": "9.5.0", + "xrpl": "4.4.3" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/connect/node_modules/@trezor/protobuf": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@trezor/protobuf/-/protobuf-1.5.2.tgz", + "integrity": "sha512-zViaL1jKue8DUTVEDg0C/lMipqNMd/Z3kr29/+MeZOoupjaXIQ2Lqp3WAMe8hvNTKKX8aNQH9JrbapJ6w9FMXw==", + "license": "See LICENSE.md in repo root", + "peer": true, + "dependencies": { + "@trezor/schema-utils": "1.4.0", + "long": "5.2.5", + "protobufjs": "7.4.0" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, "node_modules/@trezor/connect/node_modules/base-x": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", @@ -6642,15 +6710,15 @@ } }, "node_modules/@trezor/device-authenticity": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@trezor/device-authenticity/-/device-authenticity-1.1.1.tgz", - "integrity": "sha512-WlYbQgc5l0pWUVP9GkMp+Oj3rVAqMKsWF0HyxujoymNjEB7rLTl2hXs+GFjlz7VnldaSslECc6EBex/eQiNOnA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@trezor/device-authenticity/-/device-authenticity-1.1.2.tgz", + "integrity": "sha512-313uSXYR4XKDv3CjtCpgHA+yEe9xxqN7EFl/D68FEn70SPsuWI0+2zUvjPPh6TIOh/EcLv7hCO/QTHUAGd7ZWQ==", "license": "See LICENSE.md in repo root", "peer": true, "dependencies": { "@noble/curves": "^2.0.1", "@trezor/crypto-utils": "1.2.0", - "@trezor/protobuf": "1.5.1", + "@trezor/protobuf": "1.5.2", "@trezor/schema-utils": "1.4.0", "@trezor/utils": "9.5.0" } @@ -6684,6 +6752,21 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@trezor/device-authenticity/node_modules/@trezor/protobuf": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@trezor/protobuf/-/protobuf-1.5.2.tgz", + "integrity": "sha512-zViaL1jKue8DUTVEDg0C/lMipqNMd/Z3kr29/+MeZOoupjaXIQ2Lqp3WAMe8hvNTKKX8aNQH9JrbapJ6w9FMXw==", + "license": "See LICENSE.md in repo root", + "peer": true, + "dependencies": { + "@trezor/schema-utils": "1.4.0", + "long": "5.2.5", + "protobufjs": "7.4.0" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, "node_modules/@trezor/device-utils": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@trezor/device-utils/-/device-utils-1.2.0.tgz", @@ -6758,13 +6841,13 @@ } }, "node_modules/@trezor/transport": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@trezor/transport/-/transport-1.6.1.tgz", - "integrity": "sha512-RQNQingZ1TOVKSJu3Av9bmQovsu9n1NkcAYJ64+ZfapORfl/AzmZizRflhxU3FlIujQJK1gbIaW79+L54g7a8w==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@trezor/transport/-/transport-1.6.2.tgz", + "integrity": "sha512-w0HlD1fU+qTGO3tefBGHF/YS/ts/TWFja9FGIJ4+7+Z9NphvIG06HGvy2HzcD9AhJy9pvDeIsyoM2TTZTiyjkQ==", "license": "SEE LICENSE IN LICENSE.md", "peer": true, "dependencies": { - "@trezor/protobuf": "1.5.1", + "@trezor/protobuf": "1.5.2", "@trezor/protocol": "1.3.0", "@trezor/type-utils": "1.2.0", "@trezor/utils": "9.5.0", @@ -6775,6 +6858,21 @@ "tslib": "^2.6.2" } }, + "node_modules/@trezor/transport/node_modules/@trezor/protobuf": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@trezor/protobuf/-/protobuf-1.5.2.tgz", + "integrity": "sha512-zViaL1jKue8DUTVEDg0C/lMipqNMd/Z3kr29/+MeZOoupjaXIQ2Lqp3WAMe8hvNTKKX8aNQH9JrbapJ6w9FMXw==", + "license": "See LICENSE.md in repo root", + "peer": true, + "dependencies": { + "@trezor/schema-utils": "1.4.0", + "long": "5.2.5", + "protobufjs": "7.4.0" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, "node_modules/@trezor/type-utils": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@trezor/type-utils/-/type-utils-1.2.0.tgz", @@ -7588,37 +7686,24 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", - "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" + "balanced-match": "^1.0.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.6.tgz", - "integrity": "sha512-kQAVowdR33euIqeA0+VZTDqU+qo1IeVY+hrKYtZMio3Pg0P0vuh/kwRylLUddJhB6pf3q/botcOvRtx4IN1wqQ==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -15813,9 +15898,9 @@ "license": "MIT" }, "node_modules/minimatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", - "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { From da8de17925753fcbfbe1d452c8909cc2c2f27d71 Mon Sep 17 00:00:00 2001 From: Michaelkingsdev Date: Tue, 3 Mar 2026 00:25:47 +0100 Subject: [PATCH 06/11] feat: Implement unified hackathon submissions dashbaord --- app/me/hackathons/submissions/page.tsx | 325 +++++++++++- .../submissions/submission-components.tsx | 477 ++++++++++++++++++ app/me/layout.tsx | 26 +- components/app-sidebar.tsx | 11 +- components/profile/PublicEarningsTab.tsx | 2 +- docs/project-detail-redesign.md | 241 +++++---- 6 files changed, 965 insertions(+), 117 deletions(-) create mode 100644 app/me/hackathons/submissions/submission-components.tsx diff --git a/app/me/hackathons/submissions/page.tsx b/app/me/hackathons/submissions/page.tsx index c1ff1b6d..0cbcd23b 100644 --- a/app/me/hackathons/submissions/page.tsx +++ b/app/me/hackathons/submissions/page.tsx @@ -1,7 +1,322 @@ -import { redirect } from 'next/navigation'; +'use client'; -const page = () => { - redirect('/coming-soon'); -}; +import { useState, useMemo } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { useAuthStatus } from '@/hooks/use-auth'; +import BoundlessSheet from '@/components/sheet/boundless-sheet'; +import EmptyState from '@/components/EmptyState'; +import { useRouter } from 'next/navigation'; +import { Trophy } from 'lucide-react'; +import { + SortField, + SortDir, + SubmissionRow, + SortIcon, + SubmissionsSheetContent, + TableRow, +} from './submission-components'; -export default page; +export default function SubmissionsPage() { + const router = useRouter(); + const { user, isLoading } = useAuthStatus(); + + const [sortField, setSortField] = useState('submittedAt'); + const [sortDir, setSortDir] = useState('desc'); + const [selectedSubmission, setSelectedSubmission] = + useState(null); + const [sheetOpen, setSheetOpen] = useState(false); + + // Pull submissions data from session — no extra API calls + const rawSubmissions: SubmissionRow[] = useMemo(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const profile = user?.profile as any; + if (!profile) return []; + + // Primary path: profile.user.hackathonSubmissionsAsParticipant + const fromUser: any[] = + profile?.user?.hackathonSubmissionsAsParticipant || []; + // Secondary alias (some API shapes expose it at profile level) + const fromProfile: any[] = profile?.hackathonSubmissionsAsParticipant || []; + + // Merge & deduplicate by id + const merged = [...fromUser, ...fromProfile]; + const seen = new Set(); + const deduped = merged.filter(s => { + const id = s?.id || s?._id; + if (!id || seen.has(id)) return false; + seen.add(id); + return true; + }); + + return deduped.map((s: any) => ({ + id: s.id || s._id || '', + projectName: s.projectName || s.title || s.name || 'Untitled Submission', + description: s.description, + introduction: s.introduction, + logo: s.logo, + videoUrl: s.videoUrl, + category: s.category, + links: s.links, + status: s.status || 'draft', + rank: s.rank ?? null, + submittedAt: s.submittedAt || s.submissionDate || s.createdAt || '', + votes: s.votes, + comments: s.comments, + hackathon: s.hackathon, + disqualificationReason: s.disqualificationReason, + })); + }, [user]); + + const sorted = useMemo(() => { + return [...rawSubmissions].sort((a, b) => { + let aVal: any; + let bVal: any; + + switch (sortField) { + case 'projectName': + aVal = (a.projectName || '').toLowerCase(); + bVal = (b.projectName || '').toLowerCase(); + break; + case 'hackathon': + aVal = (a.hackathon?.title || a.hackathon?.name || '').toLowerCase(); + bVal = (b.hackathon?.title || b.hackathon?.name || '').toLowerCase(); + break; + case 'status': + aVal = (a.status || '').toLowerCase(); + bVal = (b.status || '').toLowerCase(); + break; + case 'submittedAt': + aVal = a.submittedAt ? new Date(a.submittedAt).getTime() : 0; + bVal = b.submittedAt ? new Date(b.submittedAt).getTime() : 0; + break; + case 'rank': + aVal = a.rank ?? Infinity; + bVal = b.rank ?? Infinity; + break; + default: + return 0; + } + + if (aVal < bVal) return sortDir === 'asc' ? -1 : 1; + if (aVal > bVal) return sortDir === 'asc' ? 1 : -1; + return 0; + }); + }, [rawSubmissions, sortField, sortDir]); + + const handleSort = (field: SortField) => { + if (sortField === field) { + setSortDir(d => (d === 'asc' ? 'desc' : 'asc')); + } else { + setSortField(field); + setSortDir('asc'); + } + }; + + const handleRowClick = (submission: SubmissionRow) => { + setSelectedSubmission(submission); + setSheetOpen(true); + }; + + const thClass = + 'cursor-pointer select-none whitespace-nowrap py-3 text-xs font-semibold uppercase tracking-wider text-zinc-500 transition-colors hover:text-zinc-300'; + + if (isLoading) { + return ( +
+
+
+ ); + } + + return ( +
+ {/* Page header */} + +

+ My Submissions +

+

+ Track the full lifecycle of every hackathon submission you've + made. +

+
+ + {/* Summary strip */} + {rawSubmissions.length > 0 && ( + + {( + [ + { + label: 'Total', + value: rawSubmissions.length, + color: 'text-white', + }, + { + label: 'Ranked', + value: rawSubmissions.filter(s => { + const st = (s.status || '').toLowerCase(); + return ( + st === 'ranked' || st === 'shortlisted' || st === 'winner' + ); + }).length, + color: 'text-primary', + }, + { + label: 'Under Review', + value: rawSubmissions.filter(s => { + const st = (s.status || '').toLowerCase(); + return st === 'under_review' || st === 'submitted'; + }).length, + color: 'text-amber-400', + }, + { + label: 'Draft', + value: rawSubmissions.filter( + s => (s.status || '').toLowerCase() === 'draft' + ).length, + color: 'text-zinc-400', + }, + ] as const + ).map(stat => ( +
+ + {stat.value} + + {stat.label} +
+ ))} +
+ )} + + {/* Table */} + + {sorted.length > 0 ? ( + +
+ + + + + + + + + + + + {sorted.map((submission, i) => ( + handleRowClick(submission)} + /> + ))} + +
handleSort('projectName')} + > + Project + + handleSort('status')} + > + Status + + +
+
+
+ ) : ( + + router.push('/hackathons')} + /> + + )} +
+ + {/* Detail sheet */} + + {selectedSubmission && ( + + )} + +
+ ); +} diff --git a/app/me/hackathons/submissions/submission-components.tsx b/app/me/hackathons/submissions/submission-components.tsx new file mode 100644 index 00000000..659f229e --- /dev/null +++ b/app/me/hackathons/submissions/submission-components.tsx @@ -0,0 +1,477 @@ +'use client'; + +import { motion } from 'framer-motion'; +import { + ChevronUp, + ChevronDown, + ChevronsUpDown, + CalendarDays, + Trophy, + ExternalLink, + MessageCircle, + ThumbsUp, + FileText, + Layers, +} from 'lucide-react'; +import { Separator } from '@/components/ui/separator'; +import Image from 'next/image'; +import { format } from 'date-fns'; + +export type SortField = + | 'projectName' + | 'hackathon' + | 'status' + | 'submittedAt' + | 'rank'; +export type SortDir = 'asc' | 'desc'; + +export type SubmissionRow = { + id: string; + projectName: string; + description?: string; + introduction?: string; + logo?: string; + videoUrl?: string; + category?: string; + links?: Array<{ type: string; url: string }>; + status: string; + rank?: number | null; + submittedAt: string; + votes?: number | any[]; + comments?: number | any[]; + hackathon?: { + id?: string; + title?: string; + name?: string; + startDate?: string; + submissionDeadline?: string; + banner?: string; + }; +}; + +// ─────────────────────────── Status badge ─────────────────────────── + +export function getStatusConfig(status: string): { + label: string; + className: string; +} { + const s = (status || '').toLowerCase(); + + if ( + s === 'ranked' || + s === 'shortlisted' || + s === 'winner' || + s === 'completed' + ) { + return { + label: + s === 'shortlisted' ? 'Ranked' : s.charAt(0).toUpperCase() + s.slice(1), + className: 'text-primary bg-primary/10', + }; + } + if (s === 'under_review' || s === 'under review') { + return { + label: 'Under Review', + className: 'text-amber-400 bg-amber-400/10', + }; + } + if (s === 'submitted') { + return { + label: 'Submitted', + className: 'text-blue-400 bg-blue-400/10', + }; + } + if (s === 'disqualified') { + return { + label: 'Disqualified', + className: 'text-red-400 bg-red-400/10', + }; + } + // draft / default + return { + label: + s === 'draft' + ? 'Draft' + : s.charAt(0).toUpperCase() + s.slice(1) || 'Draft', + className: 'text-gray-400 bg-gray-800/20', + }; +} + +export function StatusBadge({ status }: { status: string }) { + const cfg = getStatusConfig(status); + return ( + + {cfg.label} + + ); +} + +// ─────────────────────────── Sort icon ─────────────────────────── + +export function SortIcon({ + field, + sortField, + sortDir, +}: { + field: SortField; + sortField: SortField; + sortDir: SortDir; +}) { + if (sortField !== field) + return ; + return sortDir === 'asc' ? ( + + ) : ( + + ); +} + +export function SubmissionsSheetContent({ + submission, +}: { + submission: SubmissionRow; +}) { + const hackathonName = + submission.hackathon?.title || + submission.hackathon?.name || + 'Unknown Hackathon'; + + const voteCount = + typeof submission.votes === 'number' + ? submission.votes + : Array.isArray(submission.votes) + ? submission.votes.length + : 0; + + const commentCount = + typeof submission.comments === 'number' + ? submission.comments + : Array.isArray(submission.comments) + ? submission.comments.length + : 0; + + const formatDate = (dateString?: string) => { + if (!dateString) return '—'; + try { + return format(new Date(dateString), 'MMM d, yyyy'); + } catch { + return dateString; + } + }; + + const viewUrl = `/projects/${submission.id}?type=submission`; + + return ( +
+ {/* Header */} + + + {/* Banner / Logo */} + {(submission.hackathon?.banner || submission.logo) && ( +
+ {submission.projectName} + {submission.logo && submission.hackathon?.banner && ( +
+
+ Project logo +
+
+ )} +
+ )} + + {/* Metadata strip */} +
+ {submission.rank != null && ( + + + + Rank #{submission.rank} + + + )} + + + Submitted {formatDate(submission.submittedAt)} + + {submission.category && ( + + + {submission.category} + + )} + {voteCount > 0 && ( + + + {voteCount} vote{voteCount !== 1 ? 's' : ''} + + )} + {commentCount > 0 && ( + + + {commentCount} comment{commentCount !== 1 ? 's' : ''} + + )} +
+ + {/* Hackathon dates */} + {(submission.hackathon?.startDate || + submission.hackathon?.submissionDeadline) && ( +
+ {submission.hackathon.startDate && ( +
+

Hackathon Start

+

+ {formatDate(submission.hackathon.startDate)} +

+
+ )} + {submission.hackathon.submissionDeadline && ( +
+

Submission Deadline

+

+ {formatDate(submission.hackathon.submissionDeadline)} +

+
+ )} +
+ )} + + + + {/* Description */} + {submission.description && ( +
+

+ Description +

+

+ {submission.description} +

+
+ )} + + {/* Introduction */} + {submission.introduction && ( +
+

+ Introduction +

+

+ {submission.introduction} +

+
+ )} + + {/* Video link */} + {submission.videoUrl && ( +
+

+ Demo Video +

+ + + Watch Demo + +
+ )} + + {/* Project links */} + {submission.links && submission.links.length > 0 && ( +
+

+ Project Links +

+
+ {submission.links.map((link, i) => ( + + + {link.type || link.url} + + ))} +
+
+ )} + + {/* Disqualification reason */} + {(submission.status || '').toLowerCase() === 'disqualified' && + (submission as any).disqualificationReason && ( +
+

+ Disqualification Reason +

+

+ {(submission as any).disqualificationReason} +

+
+ )} +
+ ); +} + +export function TableRow({ + submission, + index, + onClick, +}: { + submission: SubmissionRow; + index: number; + onClick: () => void; +}) { + const hackathonName = + submission.hackathon?.title || submission.hackathon?.name || '—'; + + const viewUrl = `/projects/${submission.id}?type=submission`; + + const formatDate = (dateString?: string) => { + if (!dateString) return '—'; + try { + return format(new Date(dateString), 'MMM d, yyyy'); + } catch { + return dateString; + } + }; + + const handleClick = (e: React.MouseEvent) => { + // Ctrl/Cmd+click or middle-click → open in new tab + if (e.ctrlKey || e.metaKey || e.button === 1) { + window.open(viewUrl, '_blank', 'noopener,noreferrer'); + return; + } + onClick(); + }; + + return ( + e.key === 'Enter' && onClick()} + aria-label={`View details for ${submission.projectName}`} + > + {/* Project */} + + + + + {/* Hackathon */} + + + {hackathonName} + + + + {/* Status */} + + + + + {/* Date */} + + + {formatDate(submission.submittedAt)} + + + + {/* Rank */} + + {submission.rank != null ? ( + + #{submission.rank} + + ) : ( + + )} + + + {/* Chevron */} + + + + + ); +} diff --git a/app/me/layout.tsx b/app/me/layout.tsx index bcb176d3..5309c061 100644 --- a/app/me/layout.tsx +++ b/app/me/layout.tsx @@ -21,13 +21,30 @@ export default function MeLayout({ children }: { children: React.ReactNode }) { 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]); + const submissionsCount = useMemo(() => { + if (!profile) return 0; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const fromUser = + (profile as any)?.user?.hackathonSubmissionsAsParticipant || []; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const fromProfile = + (profile as any)?.hackathonSubmissionsAsParticipant || []; + // Deduplicate by id before counting + const merged = [...fromUser, ...fromProfile]; + const seen = new Set(); + return merged.filter((s: any) => { + const id = s?.id || s?._id; + if (!id || seen.has(id)) return false; + seen.add(id); + return true; + }).length; + }, [profile]); + if (isLoading) { return (
@@ -47,7 +64,10 @@ export default function MeLayout({ children }: { children: React.ReactNode }) { > diff --git a/components/app-sidebar.tsx b/components/app-sidebar.tsx index bdc0ce57..63b9e338 100644 --- a/components/app-sidebar.tsx +++ b/components/app-sidebar.tsx @@ -28,7 +28,10 @@ import { import Image from 'next/image'; import Link from 'next/link'; -const getNavigationData = (counts?: { participating?: number }) => ({ +const getNavigationData = (counts?: { + participating?: number; + submissions?: number; +}) => ({ main: [ { title: 'Overview', @@ -73,6 +76,10 @@ const getNavigationData = (counts?: { participating?: number }) => ({ title: 'Submissions', url: '/me/hackathons/submissions', icon: IconUsers, + badge: + counts?.submissions && counts.submissions > 0 + ? counts.submissions.toString() + : undefined, }, ], crowdfunding: [ @@ -114,7 +121,7 @@ export function AppSidebar({ ...props }: { user: UserData; - counts?: { participating?: number }; + counts?: { participating?: number; submissions?: number }; } & React.ComponentProps) { const navigationData = React.useMemo( () => getNavigationData(counts), diff --git a/components/profile/PublicEarningsTab.tsx b/components/profile/PublicEarningsTab.tsx index 1df678cf..fa50f6ae 100644 --- a/components/profile/PublicEarningsTab.tsx +++ b/components/profile/PublicEarningsTab.tsx @@ -197,7 +197,7 @@ const PublicEarningsTab = ({ {(earnings.activities?.length ?? 0) > 0 && (
-

+

Verified Activity

diff --git a/docs/project-detail-redesign.md b/docs/project-detail-redesign.md index 6957bd63..5e5c01ff 100644 --- a/docs/project-detail-redesign.md +++ b/docs/project-detail-redesign.md @@ -8,12 +8,13 @@ The objective is to create a simpler, clearer, and more professional experience --- Design Principles -* Clear primary action above the fold -* Strong visual hierarchy -* Reduced visual noise -* Consistent spacing and typography -* Intentional use of the green accent -* Professional and trustworthy product feel + +- Clear primary action above the fold +- Strong visual hierarchy +- Reduced visual noise +- Consistent spacing and typography +- Intentional use of the green accent +- Professional and trustworthy product feel --- @@ -21,65 +22,72 @@ Layout Structure Desktop Left: Sticky summary sidebar -* Project logo -* Title -* Status and short description -* Progress (votes/funding where applicable) -* Primary CTA (Vote or Back) -* Secondary actions (Share) -* Creator info and external links + +- Project logo +- Title +- Status and short description +- Progress (votes/funding where applicable) +- Primary CTA (Vote or Back) +- Secondary actions (Share) +- Creator info and external links Right: Main content area -* Horizontal tab navigation -* Dynamic tab content -* Optimized reading width for long-form content + +- Horizontal tab navigation +- Dynamic tab content +- Optimized reading width for long-form content > The sidebar remains visible while scrolling to maintain action visibility and improve engagement. Mobile -* Sidebar collapses into a condensed header block -* Horizontal scrollable tab navigation -* Single-column content layout -* Primary CTA positioned prominently without overwhelming the interface + +- Sidebar collapses into a condensed header block +- Horizontal scrollable tab navigation +- Single-column content layout +- Primary CTA positioned prominently without overwhelming the interface > No feature loss between desktop and mobile. --- Component Redesign + 1. Loading State -A simplified skeleton layout that mirrors final structure: -* Sidebar placeholder block -* Tab row skeleton -* Content block placeholders -* Reduced visual motion for a clean, professional feel + A simplified skeleton layout that mirrors final structure: + +- Sidebar placeholder block +- Tab row skeleton +- Content block placeholders +- Reduced visual motion for a clean, professional feel > The loading state communicates layout structure without clutter. --- 2. Sidebar (Desktop) / Header Block (Mobile) -Improvements: -* Strong title hierarchy -* Short description positioned clearly under title -* Simplified progress visualization -* One clearly emphasized primary action -* Secondary actions styled with lower visual weight -* Creator avatar and name placed below primary CTA -* External links grouped and visually subtle + Improvements: + +- Strong title hierarchy +- Short description positioned clearly under title +- Simplified progress visualization +- One clearly emphasized primary action +- Secondary actions styled with lower visual weight +- Creator avatar and name placed below primary CTA +- External links grouped and visually subtle > The goal is clarity and action focus. --- 3. Tab Bar -Tabs: Details, Team, Milestones, Voters (optional), Backers (optional), Comments -Improvements: -* Clear selected state -* Subtle hover state -* Consistent spacing and typography -* Scrollable on mobile -* Optional tabs hidden gracefully when not applicable + Tabs: Details, Team, Milestones, Voters (optional), Backers (optional), Comments + Improvements: + +- Clear selected state +- Subtle hover state +- Consistent spacing and typography +- Scrollable on mobile +- Optional tabs hidden gracefully when not applicable > The tab system is visually lightweight but structurally strong. @@ -87,66 +95,78 @@ Improvements: Tab-Level Redesign Details Tab -* Controlled reading width for markdown content -* Clear heading hierarchy -* Improved paragraph spacing -* Links styled consistently -* Optional media block positioned after introduction -* Improved vertical rhythm + +- Controlled reading width for markdown content +- Clear heading hierarchy +- Improved paragraph spacing +- Links styled consistently +- Optional media block positioned after introduction +- Improved vertical rhythm > Focus: readability and professional presentation. Team Tab -* Grid layout on desktop -* Vertical list on mobile -* Avatar, name, and role hierarchy clearly defined -* Clean spacing between members + +- Grid layout on desktop +- Vertical list on mobile +- Avatar, name, and role hierarchy clearly defined +- Clean spacing between members Empty State Example: + > No team members yet. This project is currently solo. Milestones Tab -* Vertical timeline layout -* Status indicators (Upcoming, Active, Completed) -* Clean separation between stages -* Optional filter alignment + +- Vertical timeline layout +- Status indicators (Upcoming, Active, Completed) +- Clean separation between stages +- Optional filter alignment Empty State Example: + > No milestones have been added yet. Voters Tab -* Clean list layout -* Vote indicators clearly distinguished -* Sorting control aligned with header + +- Clean list layout +- Vote indicators clearly distinguished +- Sorting control aligned with header Empty State Example: + > No votes yet. Be the first to support this project. Backers Tab -* Supporter card or structured list layout -* Clear contribution display -* Encouraging but subtle empty state + +- Supporter card or structured list layout +- Clear contribution display +- Encouraging but subtle empty state Empty State Example: + > Be the first to back this project. Comments Tab -* Structured threaded layout -* Controlled indentation depth -* Clear reply visibility -* Sorting dropdown aligned to section header -* Clean comment input field with clear submit action + +- Structured threaded layout +- Controlled indentation depth +- Clear reply visibility +- Sorting dropdown aligned to section header +- Clean comment input field with clear submit action Empty State Example: + > No comments yet. Start the discussion. --- Empty State Strategy Each tab includes a purposeful empty state to avoid dead screens: -* Encourage action -* Maintain tone consistency -* Support engagement goals + +- Encourage action +- Maintain tone consistency +- Support engagement goals --- @@ -154,70 +174,79 @@ Error State Strategy Each tab and interactive component must handle failures gracefully to maintain clarity, trust, and engagement. General Principles -* Display a clear, concise error message in context -* Offer a retry action when appropriate -* Maintain the same layout structure as loading and empty states -* Keep messaging professional and consistent with tone + +- Display a clear, concise error message in context +- Offer a retry action when appropriate +- Maintain the same layout structure as loading and empty states +- Keep messaging professional and consistent with tone Tab-Level Examples Details / Team / Milestones / Voters / Backers / Comments Tabs - * Error message: “Something went wrong while loading this content.” - * Retry CTA: “Try again” button positioned centrally within the tab content area - * Maintain spacing and typography consistency with other states + +- Error message: “Something went wrong while loading this content.” +- Retry CTA: “Try again” button positioned centrally within the tab content area +- Maintain spacing and typography consistency with other states Comment Submission Failure - * Inline error: “Your comment could not be submitted.” - * Preserve typed input so users don’t lose their content - * Include a retry button adjacent to the input field - Vote / Back Action Failure - * Inline or toast notification: “Your vote could not be recorded.” - * Provide immediate retry option - * Ensure visual distinction from primary actions to avoid confusion +- Inline error: “Your comment could not be submitted.” +- Preserve typed input so users don’t lose their content +- Include a retry button adjacent to the input field + +Vote / Back Action Failure + +- Inline or toast notification: “Your vote could not be recorded.” +- Provide immediate retry option +- Ensure visual distinction from primary actions to avoid confusion Deliverables for Implementation -* Error-state mockups for all tabs and key actions -* Retry interaction designs and user flow diagrams -* Copy for all error messages and notifications + +- Error-state mockups for all tabs and key actions +- Retry interaction designs and user flow diagrams +- Copy for all error messages and notifications --- UX Improvements -* Single primary CTA above the fold -* Sticky action area improves conversion -* Stronger reading experience -* Consistent spacing system -* Clear separation of primary vs secondary actions -* Reduced cognitive load + +- Single primary CTA above the fold +- Sticky action area improves conversion +- Stronger reading experience +- Consistent spacing system +- Clear separation of primary vs secondary actions +- Reduced cognitive load --- Visual System Adjustments -* Reduced shadow usage -* Controlled border radius for consistency -* Green accent reserved primarily for key actions and highlights -* Improved typography hierarchy -* Balanced whitespace for clarity + +- Reduced shadow usage +- Controlled border radius for consistency +- Green accent reserved primarily for key actions and highlights +- Improved typography hierarchy +- Balanced whitespace for clarity > The page now feels lighter, more modern, and more trustworthy. --- Responsiveness Strategy -* Sidebar collapses into header on smaller screens -* Horizontal tab scroll -* Content reflows naturally -* No feature disparity between device sizes + +- Sidebar collapses into header on smaller screens +- Horizontal tab scroll +- Content reflows naturally +- No feature disparity between device sizes --- Deliverables -* Desktop full-page mockups (all tabs) -* Mobile full-page mockups (all tabs) -* Component-level breakdown -* Loading, empty, and error state designs -* Retry interaction and UX flow documentation -* UX recommendations + +- Desktop full-page mockups (all tabs) +- Mobile full-page mockups (all tabs) +- Component-level breakdown +- Loading, empty, and error state designs +- Retry interaction and UX flow documentation +- UX recommendations Figma link: https://www.figma.com/design/EMNGAQl1SGObXcsoa24krt/Boundless_Project-Details?node-id=0-1&t=TAP62qLgaqjB5B1K-1 From 4788c79ec0a23bf49ab0b95b27a38d3acaf92f42 Mon Sep 17 00:00:00 2001 From: Michaelkingsdev Date: Tue, 3 Mar 2026 01:24:32 +0100 Subject: [PATCH 07/11] fix: fix coderabbit corrections --- app/me/hackathons/submissions/page.tsx | 157 ++++++++++++------ .../submissions/submission-components.tsx | 55 +++--- 2 files changed, 137 insertions(+), 75 deletions(-) diff --git a/app/me/hackathons/submissions/page.tsx b/app/me/hackathons/submissions/page.tsx index 0cbcd23b..afe1d974 100644 --- a/app/me/hackathons/submissions/page.tsx +++ b/app/me/hackathons/submissions/page.tsx @@ -7,6 +7,13 @@ import BoundlessSheet from '@/components/sheet/boundless-sheet'; import EmptyState from '@/components/EmptyState'; import { useRouter } from 'next/navigation'; import { Trophy } from 'lucide-react'; +import { + Table, + TableBody, + TableHead, + TableHeader, + TableRow as ShadcnTableRow, +} from '@/components/ui/table'; import { SortField, SortDir, @@ -117,8 +124,13 @@ export default function SubmissionsPage() { setSheetOpen(true); }; + const getAriaSort = (field: SortField) => { + if (sortField !== field) return 'none'; + return sortDir === 'asc' ? 'ascending' : 'descending'; + }; + const thClass = - 'cursor-pointer select-none whitespace-nowrap py-3 text-xs font-semibold uppercase tracking-wider text-zinc-500 transition-colors hover:text-zinc-300'; + 'h-12 px-2 text-left align-middle font-medium text-zinc-400 [&:has([role=checkbox])]:pr-0'; if (isLoading) { return ( @@ -213,68 +225,103 @@ export default function SubmissionsPage() { className='overflow-hidden rounded-xl border border-white/5 bg-white/[0.025]' >
- - - - - - - - - - - + + + + + + {sorted.map((submission, i) => ( handleRowClick(submission)} /> ))} - -
+ + + handleSort('projectName')} + aria-sort={getAriaSort('projectName')} > - Project - - handleSort('projectName')} + className='flex w-full items-center gap-1 rounded-sm text-xs font-semibold tracking-wider uppercase hover:text-white focus-visible:ring-2 focus-visible:ring-[#a7f950] focus-visible:ring-offset-1 focus-visible:ring-offset-[#0e0c0c] focus-visible:outline-none' + aria-label='Sort by Project Name' + > + Project + + + + handleSort('hackathon')} + className='flex w-full items-center gap-1 rounded-sm text-xs font-semibold tracking-wider uppercase hover:text-white focus-visible:ring-2 focus-visible:ring-[#a7f950] focus-visible:ring-offset-1 focus-visible:ring-offset-[#0e0c0c] focus-visible:outline-none' + aria-label='Sort by Hackathon' + > + Hackathon + + + + handleSort('status')} + aria-sort={getAriaSort('status')} > - Status - - handleSort('status')} + className='flex w-full items-center gap-1 rounded-sm text-xs font-semibold tracking-wider uppercase hover:text-white focus-visible:ring-2 focus-visible:ring-[#a7f950] focus-visible:ring-offset-1 focus-visible:ring-offset-[#0e0c0c] focus-visible:outline-none' + aria-label='Sort by Status' + > + Status + + + + handleSort('submittedAt')} + className='flex w-full items-center gap-1 rounded-sm text-xs font-semibold tracking-wider uppercase hover:text-white focus-visible:ring-2 focus-visible:ring-[#a7f950] focus-visible:ring-offset-1 focus-visible:ring-offset-[#0e0c0c] focus-visible:outline-none' + aria-label='Sort by Submitted Date' + > + Submitted + + + + -
+ +
) : ( diff --git a/app/me/hackathons/submissions/submission-components.tsx b/app/me/hackathons/submissions/submission-components.tsx index 659f229e..a296f4dc 100644 --- a/app/me/hackathons/submissions/submission-components.tsx +++ b/app/me/hackathons/submissions/submission-components.tsx @@ -14,6 +14,7 @@ import { Layers, } from 'lucide-react'; import { Separator } from '@/components/ui/separator'; +import { TableCell, TableRow as ShadcnTableRow } from '@/components/ui/table'; import Image from 'next/image'; import { format } from 'date-fns'; @@ -379,13 +380,27 @@ export function TableRow({ } }; - const handleClick = (e: React.MouseEvent) => { - // Ctrl/Cmd+click or middle-click → open in new tab - if (e.ctrlKey || e.metaKey || e.button === 1) { + const handleLeftClick = (e: React.MouseEvent) => { + // Left click only + if (e.button !== 0) return; + onClick(); + }; + + const handleAuxClick = (e: React.MouseEvent) => { + // Middle click only -> open in new tab + if (e.button === 1) { + e.stopPropagation(); window.open(viewUrl, '_blank', 'noopener,noreferrer'); - return; } - onClick(); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + onClick(); + } else if (e.key === ' ') { + e.preventDefault(); // prevent page scrolling + onClick(); + } }; return ( @@ -393,16 +408,16 @@ export function TableRow({ initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: index * 0.03, duration: 0.25 }} - onClick={handleClick} - onAuxClick={handleClick} + onClick={handleLeftClick} + onAuxClick={handleAuxClick} className='group cursor-pointer border-b border-white/5 transition-colors duration-150 hover:bg-white/[0.04]' role='button' tabIndex={0} - onKeyDown={e => e.key === 'Enter' && onClick()} + onKeyDown={handleKeyDown} aria-label={`View details for ${submission.projectName}`} > {/* Project */} - +
{submission.logo ? (
@@ -436,29 +451,29 @@ export function TableRow({
- +
{/* Hackathon */} - + {hackathonName} - + {/* Status */} - + - + {/* Date */} - + {formatDate(submission.submittedAt)} - + {/* Rank */} - + {submission.rank != null ? ( #{submission.rank} @@ -466,12 +481,12 @@ export function TableRow({ ) : ( )} - + {/* Chevron */} - + - + ); } From 26129b2e137f923a66b8df6610b1b2f0bee31f22 Mon Sep 17 00:00:00 2001 From: Michaelkingsdev Date: Tue, 3 Mar 2026 01:56:57 +0100 Subject: [PATCH 08/11] fix: fix coderabbit corrections --- app/me/hackathons/submissions/page.tsx | 12 ++-- .../submissions/submission-components.tsx | 57 ++++++++++++++----- 2 files changed, 48 insertions(+), 21 deletions(-) diff --git a/app/me/hackathons/submissions/page.tsx b/app/me/hackathons/submissions/page.tsx index afe1d974..251d9936 100644 --- a/app/me/hackathons/submissions/page.tsx +++ b/app/me/hackathons/submissions/page.tsx @@ -2,7 +2,7 @@ import { useState, useMemo } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; -import { useAuthStatus } from '@/hooks/use-auth'; +import { useSession } from 'next-auth/react'; import BoundlessSheet from '@/components/sheet/boundless-sheet'; import EmptyState from '@/components/EmptyState'; import { useRouter } from 'next/navigation'; @@ -25,7 +25,7 @@ import { export default function SubmissionsPage() { const router = useRouter(); - const { user, isLoading } = useAuthStatus(); + const { data: session, status } = useSession(); const [sortField, setSortField] = useState('submittedAt'); const [sortDir, setSortDir] = useState('desc'); @@ -35,8 +35,8 @@ export default function SubmissionsPage() { // Pull submissions data from session — no extra API calls const rawSubmissions: SubmissionRow[] = useMemo(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const profile = user?.profile as any; + const sessionUser = session?.user as any; + const profile = sessionUser?.profile; if (!profile) return []; // Primary path: profile.user.hackathonSubmissionsAsParticipant @@ -72,7 +72,7 @@ export default function SubmissionsPage() { hackathon: s.hackathon, disqualificationReason: s.disqualificationReason, })); - }, [user]); + }, [session?.user]); const sorted = useMemo(() => { return [...rawSubmissions].sort((a, b) => { @@ -132,7 +132,7 @@ export default function SubmissionsPage() { const thClass = 'h-12 px-2 text-left align-middle font-medium text-zinc-400 [&:has([role=checkbox])]:pr-0'; - if (isLoading) { + if (status === 'loading') { return (
diff --git a/app/me/hackathons/submissions/submission-components.tsx b/app/me/hackathons/submissions/submission-components.tsx index a296f4dc..2a6704b8 100644 --- a/app/me/hackathons/submissions/submission-components.tsx +++ b/app/me/hackathons/submissions/submission-components.tsx @@ -17,6 +17,22 @@ import { Separator } from '@/components/ui/separator'; import { TableCell, TableRow as ShadcnTableRow } from '@/components/ui/table'; import Image from 'next/image'; import { format } from 'date-fns'; +import { useState } from 'react'; + +// ─────────────────────────── Url Sanitization ─────────────────────────── + +export function getSafeUrl(urlString?: string): string | undefined { + if (!urlString) return undefined; + try { + const parsed = new URL(urlString); + if (['http:', 'https:', 'mailto:'].includes(parsed.protocol)) { + return parsed.href; + } + return undefined; + } catch { + return undefined; + } +} export type SortField = | 'projectName' @@ -301,13 +317,13 @@ export function SubmissionsSheetContent({ )} {/* Video link */} - {submission.videoUrl && ( + {submission.videoUrl && getSafeUrl(submission.videoUrl) && (

Demo Video

- {submission.links.map((link, i) => ( - - - {link.type || link.url} - - ))} + {submission.links.map((link, i) => { + const safeUrl = getSafeUrl(link.url); + if (!safeUrl) return null; + return ( + + + {link.type || link.url} + + ); + })}
)} @@ -370,6 +390,7 @@ export function TableRow({ submission.hackathon?.title || submission.hackathon?.name || '—'; const viewUrl = `/projects/${submission.id}?type=submission`; + const [isHoverOrFocus, setIsHoverOrFocus] = useState(false); const formatDate = (dateString?: string) => { if (!dateString) return '—'; @@ -411,6 +432,8 @@ export function TableRow({ onClick={handleLeftClick} onAuxClick={handleAuxClick} className='group cursor-pointer border-b border-white/5 transition-colors duration-150 hover:bg-white/[0.04]' + onMouseEnter={() => setIsHoverOrFocus(true)} + onMouseLeave={() => setIsHoverOrFocus(false)} role='button' tabIndex={0} onKeyDown={handleKeyDown} @@ -443,8 +466,12 @@ export function TableRow({ target='_blank' rel='noopener noreferrer' onClick={e => e.stopPropagation()} + onFocus={() => setIsHoverOrFocus(true)} + onBlur={() => setIsHoverOrFocus(false)} + tabIndex={isHoverOrFocus ? 0 : -1} + aria-hidden={!isHoverOrFocus} title='Open submission in new tab' - className='text-zinc-500 opacity-0 transition-opacity duration-150 group-hover:opacity-100 hover:text-zinc-200' + className='rounded-sm text-zinc-500 opacity-0 transition-opacity duration-150 group-hover:opacity-100 hover:text-zinc-200 focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-[#a7f950] focus-visible:ring-offset-1 focus-visible:ring-offset-zinc-900 focus-visible:outline-none' aria-label={`Open ${submission.projectName} in new tab`} > From 1dd31a14460d19dbaa477162892102d2abdf29b1 Mon Sep 17 00:00:00 2001 From: Michaelkingsdev Date: Tue, 3 Mar 2026 02:37:29 +0100 Subject: [PATCH 09/11] fix: normalize the status by collapsing spaces,underscores,dashes before comparing --- app/me/hackathons/submissions/page.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/me/hackathons/submissions/page.tsx b/app/me/hackathons/submissions/page.tsx index 251d9936..9dc1f2c0 100644 --- a/app/me/hackathons/submissions/page.tsx +++ b/app/me/hackathons/submissions/page.tsx @@ -186,7 +186,9 @@ export default function SubmissionsPage() { { label: 'Under Review', value: rawSubmissions.filter(s => { - const st = (s.status || '').toLowerCase(); + const st = (s.status || '') + .toLowerCase() + .replace(/[\s\-]+/g, '_'); return st === 'under_review' || st === 'submitted'; }).length, color: 'text-amber-400', From 08b4ce687ad812aa31784b08701a097f0e30e170 Mon Sep 17 00:00:00 2001 From: Michaelkingsdev Date: Tue, 3 Mar 2026 02:47:41 +0100 Subject: [PATCH 10/11] fix: normalize the status by collapsing spaces,underscores,dashes before comparing --- app/me/hackathons/submissions/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/me/hackathons/submissions/page.tsx b/app/me/hackathons/submissions/page.tsx index 9dc1f2c0..f524269f 100644 --- a/app/me/hackathons/submissions/page.tsx +++ b/app/me/hackathons/submissions/page.tsx @@ -188,7 +188,7 @@ export default function SubmissionsPage() { value: rawSubmissions.filter(s => { const st = (s.status || '') .toLowerCase() - .replace(/[\s\-]+/g, '_'); + .replace(/[\s\-_]+/g, '_'); return st === 'under_review' || st === 'submitted'; }).length, color: 'text-amber-400', From 8860d5787d57139c333ad514889dc765bb53a99a Mon Sep 17 00:00:00 2001 From: Michaelkingsdev Date: Tue, 3 Mar 2026 03:00:07 +0100 Subject: [PATCH 11/11] fix: fix coderabbit corrections --- app/me/hackathons/submissions/page.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/app/me/hackathons/submissions/page.tsx b/app/me/hackathons/submissions/page.tsx index f524269f..107919de 100644 --- a/app/me/hackathons/submissions/page.tsx +++ b/app/me/hackathons/submissions/page.tsx @@ -2,7 +2,7 @@ import { useState, useMemo } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; -import { useSession } from 'next-auth/react'; +import { useAuthStatus } from '@/hooks/use-auth'; import BoundlessSheet from '@/components/sheet/boundless-sheet'; import EmptyState from '@/components/EmptyState'; import { useRouter } from 'next/navigation'; @@ -25,7 +25,7 @@ import { export default function SubmissionsPage() { const router = useRouter(); - const { data: session, status } = useSession(); + const { user, isLoading } = useAuthStatus(); const [sortField, setSortField] = useState('submittedAt'); const [sortDir, setSortDir] = useState('desc'); @@ -33,10 +33,9 @@ export default function SubmissionsPage() { useState(null); const [sheetOpen, setSheetOpen] = useState(false); - // Pull submissions data from session — no extra API calls + // Pull submissions data from auth state — no extra API calls const rawSubmissions: SubmissionRow[] = useMemo(() => { - const sessionUser = session?.user as any; - const profile = sessionUser?.profile; + const profile = (user as any)?.profile; if (!profile) return []; // Primary path: profile.user.hackathonSubmissionsAsParticipant @@ -72,7 +71,7 @@ export default function SubmissionsPage() { hackathon: s.hackathon, disqualificationReason: s.disqualificationReason, })); - }, [session?.user]); + }, [user]); const sorted = useMemo(() => { return [...rawSubmissions].sort((a, b) => { @@ -132,7 +131,7 @@ export default function SubmissionsPage() { const thClass = 'h-12 px-2 text-left align-middle font-medium text-zinc-400 [&:has([role=checkbox])]:pr-0'; - if (status === 'loading') { + if (isLoading) { return (