Skip to content

Commit c4aa566

Browse files
authored
Hackathon analytics (#378)
* fix: modify api.ts * fix: remove google auth buttom * fix: fixes responsive fixes on organization * fix: minor fixes * fix: modify create organization * fix: modify create organization * fix: fix organization permission * fix: merge into main * feat: hackathon overview page * feat: hackathon overview page * feat: implement participant overview * feat: implement participant overview * feat: implement resources tab * feat: implement the submission tab * feat: implement comment tab * fix: implement provider for hackathon * fix: implement provider for hackathon * fix: minor fixes * fix: hackathon banner * fix: hackathon banner * fix: fix organization page * fix: fix organization page * fix: use transform * fix: add tagline * fix: add tagline * fix: minor fixes * fix: minor fixes * fix: fix timeline and prizes * fix: correct timeline events * fix: implement registration deadline policy * fix: implement registration deadline policy * feat: implement leave hackathon * feat: implement leave hackathon * fix: delete hackathon * fix: implement invite participants * fix: implement participant profile viewing * feat: fetch participants team * fix: redesign hackathon banner * fix: fix hackthon card * fix: fix search bar in blog page * fix: fix search bar in blog page * fix: fix search bar in blog page * fix: fix error in fetching team posts * feat: implement create team, get my team * feat: implement create team, get my team * feat: implement hackathon project submission flow * feat: implement voting for submission * fix: team formation updates * fix: implement team invitation * feat: hackathon submissions bulk actions, ranking and ui refinements * fix: implement empty state for hackathons page * feat: Implement submission visibility and explore submissions * feat: Implement winners tab for hackathon winners display * feat: implement hackathon analytics * fix: fix coderabbit corrections * fix: fix coderabbit corrections
1 parent 275447f commit c4aa566

14 files changed

Lines changed: 957 additions & 136 deletions

File tree

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

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { HackathonResources } from '@/components/hackathons/resources/resources'
1313
import SubmissionTab from '@/components/hackathons/submissions/submissionTab';
1414
import { HackathonDiscussions } from '@/components/hackathons/discussion/comment';
1515
import { TeamFormationTab } from '@/components/hackathons/team-formation/TeamFormationTab';
16+
import { WinnersTab } from '@/components/hackathons/winners/WinnersTab';
1617
import LoadingScreen from '@/features/projects/components/CreateProjectModal/LoadingScreen';
1718
import { useTimelineEvents } from '@/hooks/hackathon/use-timeline-events';
1819
import { toast } from 'sonner';
@@ -22,6 +23,7 @@ import { HackathonParticipants } from '@/components/hackathons/participants/hack
2223
import { useCommentSystem } from '@/hooks/use-comment-system';
2324
import { CommentEntityType } from '@/types/comment';
2425
import { useTeamPosts } from '@/hooks/hackathon/use-team-posts';
26+
import { HackathonWinner } from '@/lib/api/hackathons';
2527

2628
export default function HackathonPage() {
2729
const router = useRouter();
@@ -31,6 +33,7 @@ export default function HackathonPage() {
3133
const {
3234
currentHackathon,
3335
submissions,
36+
winners,
3437
loading,
3538
setCurrentHackathon,
3639
refreshCurrentHackathon,
@@ -75,6 +78,19 @@ export default function HackathonPage() {
7578
const isTabEnabled =
7679
currentHackathon?.enabledTabs?.includes('joinATeamTab') !== false;
7780

81+
// For testing: Use mock winners if real winners are empty
82+
// const displayWinners =
83+
// winners && winners.length > 0 ? winners : MOCK_WINNERS;
84+
// const hasWinners = displayWinners.length > 0;
85+
const hasWinners = winners && winners.length > 0;
86+
87+
// For testing: Force enable winners tab
88+
// const isWinnersTabEnabled =
89+
// currentHackathon?.enabledTabs?.includes('winnersTab') !== false;
90+
// const isWinnersTabEnabled = true;
91+
const isWinnersTabEnabled =
92+
currentHackathon?.enabledTabs?.includes('winnersTab') !== false;
93+
7894
const tabs = [
7995
{ id: 'overview', label: 'Overview' },
8096
...(hasParticipants
@@ -115,6 +131,13 @@ export default function HackathonPage() {
115131
});
116132
}
117133

134+
if (hasWinners && isWinnersTabEnabled) {
135+
tabs.push({
136+
id: 'winners',
137+
label: 'Winners',
138+
});
139+
}
140+
118141
return tabs;
119142
}, [
120143
currentHackathon?.participants,
@@ -126,6 +149,7 @@ export default function HackathonPage() {
126149
discussionComments.comments.length,
127150
teamPosts.length,
128151
hackathonId,
152+
winners,
129153
]);
130154

131155
// Refresh hackathon data
@@ -363,7 +387,14 @@ export default function HackathonPage() {
363387
)}
364388

365389
{activeTab === 'team-formation' && (
366-
<TeamFormationTab hackathonSlugOrId={hackathonId} />
390+
<TeamFormationTab
391+
hackathonSlugOrId={hackathonId}
392+
isRegistered={isRegistered}
393+
/>
394+
)}
395+
396+
{activeTab === 'winners' && (
397+
<WinnersTab winners={winners} hackathonSlug={hackathonId} />
367398
)}
368399

369400
{activeTab === 'resources' && currentHackathon?.resources?.[0] && (

app/(landing)/hackathons/layout.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { HackathonDataProvider } from '@/lib/providers/hackathonProvider';
2+
import { OrganizationProvider } from '@/lib/providers/OrganizationProvider';
23
import { use } from 'react';
34

45
interface HackathonLayoutProps {
@@ -15,8 +16,10 @@ export default function HackathonLayout({
1516
const resolvedParams = use(params);
1617

1718
return (
18-
<HackathonDataProvider hackathonSlug={resolvedParams.slug}>
19-
{children}
20-
</HackathonDataProvider>
19+
<OrganizationProvider>
20+
<HackathonDataProvider hackathonSlug={resolvedParams.slug}>
21+
{children}
22+
</HackathonDataProvider>
23+
</OrganizationProvider>
2124
);
2225
}

app/(landing)/organizations/[id]/hackathons/[hackathonId]/page.tsx

Lines changed: 127 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
'use client';
22

33
import { useParams } from 'next/navigation';
4-
import { Loader2, AlertCircle, Calendar, TrendingUp } from 'lucide-react';
4+
import {
5+
Loader2,
6+
AlertCircle,
7+
Calendar,
8+
TrendingUp,
9+
Check,
10+
} from 'lucide-react';
511
import { useHackathons } from '@/hooks/use-hackathons';
612
import { useEffect } from 'react';
713
import { useHackathonAnalytics } from '@/hooks/use-hackathon-analytics';
@@ -23,8 +29,10 @@ export default function HackathonPage() {
2329
autoFetch: false,
2430
});
2531

26-
const { statistics, statisticsLoading, timeSeriesData, timeSeriesLoading } =
27-
useHackathonAnalytics(organizationId, hackathonId);
32+
const { analytics, loading: analyticsLoading } = useHackathonAnalytics(
33+
organizationId,
34+
hackathonId
35+
);
2836

2937
useEffect(() => {
3038
if (organizationId && hackathonId) {
@@ -60,6 +68,29 @@ export default function HackathonPage() {
6068
);
6169
}
6270

71+
// Adapt key metrics
72+
const statistics = analytics?.summary || null;
73+
74+
// Adapt charts data
75+
const timeSeriesData = analytics?.trends
76+
? {
77+
submissions: {
78+
daily: analytics.trends.submissionsOverTime.map(p => ({
79+
date: p.date,
80+
count: p.count,
81+
})),
82+
weekly: [],
83+
},
84+
participants: {
85+
daily: analytics.trends.participantSignupsOverTime.map(p => ({
86+
date: p.date,
87+
count: p.count,
88+
})),
89+
weekly: [],
90+
},
91+
}
92+
: null;
93+
6394
return (
6495
<AuthGuard redirectTo='/auth?mode=signin' fallback={<Loading />}>
6596
<div className='min-h-screen bg-black'>
@@ -69,11 +100,6 @@ export default function HackathonPage() {
69100
<h1 className='text-3xl font-light tracking-tight text-white sm:text-4xl'>
70101
{currentHackathon?.name || 'Hackathon Dashboard'}
71102
</h1>
72-
{/* {currentHackathon?.information?.description && (
73-
<p className='mt-3 max-w-2xl text-sm text-gray-400'>
74-
{currentHackathon.information.description}
75-
</p>
76-
)} */}
77103
</div>
78104
</div>
79105

@@ -89,15 +115,15 @@ export default function HackathonPage() {
89115
</div>
90116
<HackathonStatistics
91117
statistics={statistics}
92-
loading={statisticsLoading}
118+
loading={analyticsLoading}
93119
/>
94120
</section>
95121

96122
{/* Charts Section */}
97123
<section className='mb-16'>
98124
<HackathonCharts
99125
timeSeriesData={timeSeriesData}
100-
loading={timeSeriesLoading}
126+
loading={analyticsLoading}
101127
/>
102128
</section>
103129

@@ -109,21 +135,97 @@ export default function HackathonPage() {
109135
Timeline
110136
</h2>
111137
</div>
112-
<HackathonTimeline
113-
timeline={{
114-
startDate: currentHackathon?.startDate || '',
115-
submissionDeadline: currentHackathon?.submissionDeadline || '',
116-
judgingDate: currentHackathon?.endDate || '',
117-
winnerAnnouncementDate:
118-
currentHackathon?.registrationDeadline || '',
119-
timezone: currentHackathon?.timezone || '',
120-
phases: currentHackathon?.phases?.map(phase => ({
121-
name: phase.name || '',
122-
startDate: phase.startDate || '',
123-
endDate: phase.endDate || '',
124-
})),
125-
}}
126-
/>
138+
139+
{/* Render new timeline from analytics */}
140+
<div className='relative'>
141+
<div className='space-y-0'>
142+
{(() => {
143+
const timelineEvents = analytics?.timeline || [];
144+
const hasWinnerAnnouncement = timelineEvents.some(
145+
e => e.phase === 'Winner Announcement'
146+
);
147+
148+
// Manually append Winner Announcement if missing and date exists
149+
const fullTimeline = [...timelineEvents];
150+
if (!hasWinnerAnnouncement && currentHackathon?.endDate) {
151+
const winnerDate = new Date(currentHackathon.endDate);
152+
const now = new Date();
153+
// Simple status logic for single date event
154+
// If date is passed, completed. If today (roughly), ongoing?
155+
// Or just use 'upcoming' if future, 'completed' if past.
156+
// Ideally we'd match the phase logic, but for a single date event:
157+
let status: 'completed' | 'ongoing' | 'upcoming' =
158+
'upcoming';
159+
if (now > winnerDate) {
160+
status = 'completed';
161+
}
162+
// For "Winner Announcement", it might be "ongoing" on the day of?
163+
// keeping simple for now.
164+
165+
fullTimeline.push({
166+
phase: 'Winner Announcement',
167+
description:
168+
'Final results published and prizes distributed to winners.',
169+
date: currentHackathon.endDate,
170+
status: status,
171+
});
172+
}
173+
174+
return fullTimeline.map((phase, index) => {
175+
const isLast = index === fullTimeline.length - 1;
176+
const isActive = phase.status === 'ongoing';
177+
const isCompleted = phase.status === 'completed';
178+
179+
return (
180+
<div
181+
key={`${phase.phase}-${index}`}
182+
className={`relative flex items-start gap-3 sm:gap-4 ${!isLast ? 'pb-6' : ''}`}
183+
>
184+
<div className='relative flex flex-col items-center'>
185+
{isActive ? (
186+
<div className='bg-active-bg z-10 flex shrink-0 items-center justify-center rounded-full p-1'>
187+
<div className='bg-primary z-10 flex h-4 w-4 shrink-0 items-center justify-center rounded-full' />
188+
</div>
189+
) : isCompleted ? (
190+
<div className='z-10 flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-green-500/20 bg-green-500/10'>
191+
<Check className='h-3 w-3 text-green-500' />
192+
</div>
193+
) : (
194+
<div className='bg-inactive z-10 flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-[#1C1C1C] opacity-50' />
195+
)}
196+
{!isLast && (
197+
<div className='absolute top-6 left-1/2 h-6 w-0.5 -translate-x-1/2'>
198+
<div className='h-full border-l-2 border-dashed border-gray-600' />
199+
</div>
200+
)}
201+
</div>
202+
<div className='flex min-w-0 flex-1 flex-col gap-2 sm:flex-row sm:items-start sm:justify-between sm:gap-4'>
203+
<div className='min-w-0 flex-1'>
204+
<h3 className='mb-1 text-sm font-medium text-white sm:text-base'>
205+
{phase.phase}
206+
</h3>
207+
<p
208+
className={`text-xs sm:text-sm ${
209+
isCompleted
210+
? 'text-gray-400'
211+
: isActive
212+
? 'text-white/60'
213+
: 'text-white/40'
214+
}`}
215+
>
216+
{phase.description}
217+
</p>
218+
</div>
219+
<div className='flex-shrink-0 text-xs whitespace-nowrap text-white/60 sm:text-sm'>
220+
{new Date(phase.date).toLocaleDateString()}
221+
</div>
222+
</div>
223+
</div>
224+
);
225+
});
226+
})()}
227+
</div>
228+
</div>
127229
</section>
128230
</div>
129231
</div>

app/(landing)/organizations/[id]/hackathons/[hackathonId]/settings/page.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
Trophy,
1313
Handshake,
1414
Sliders,
15+
Eye,
1516
} from 'lucide-react';
1617
import { toast } from 'sonner';
1718
import { api } from '@/lib/api/api';
@@ -21,6 +22,7 @@ import ParticipantSettingsTab from '@/components/organization/hackathons/setting
2122
import RewardsSettingsTab from '@/components/organization/hackathons/settings/RewardsSettingsTab';
2223
import CollaborationSettingsTab from '@/components/organization/hackathons/settings/CollaborationSettingsTab';
2324
import AdvancedSettingsTab from '@/components/organization/hackathons/settings/AdvancedSettingsTab';
25+
import SubmissionVisibilitySettingsTab from '@/components/organization/hackathons/settings/SubmissionVisibilitySettingsTab';
2426
import { AuthGuard } from '@/components/auth';
2527
import Loading from '@/components/Loading';
2628

@@ -189,6 +191,13 @@ export default function SettingsPage() {
189191
<Sliders className='h-4 w-4' />
190192
Advanced
191193
</TabsTrigger>
194+
<TabsTrigger
195+
value='submissions'
196+
className={tabTriggerClassName}
197+
>
198+
<Eye className='h-4 w-4' />
199+
Submissions
200+
</TabsTrigger>
192201
</TabsList>
193202
</div>
194203
<ScrollBar orientation='horizontal' className='h-px' />
@@ -253,6 +262,13 @@ export default function SettingsPage() {
253262
isLoading={isSaving}
254263
/>
255264
</TabsContent>
265+
266+
<TabsContent value='submissions' className='mt-0'>
267+
<SubmissionVisibilitySettingsTab
268+
organizationId={organizationId}
269+
hackathonId={hackathonId}
270+
/>
271+
</TabsContent>
256272
</Tabs>
257273
</div>
258274
</div>

components/hackathons/submissions/submissionTab.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,15 +264,22 @@ const SubmissionTab: React.FC<SubmissionTabProps> = ({
264264
: 'Pending'
265265
}
266266
upvotes={
267-
typeof mySubmission.votes === 'number' ? mySubmission.votes : 0
267+
typeof mySubmission.votes === 'number'
268+
? mySubmission.votes
269+
: Array.isArray(mySubmission.votes)
270+
? mySubmission.votes.length
271+
: 0
268272
}
269273
comments={
270274
typeof mySubmission.comments === 'number'
271275
? mySubmission.comments
272-
: 0
276+
: Array.isArray(mySubmission.comments)
277+
? mySubmission.comments.length
278+
: 0
273279
}
274280
submittedDate={mySubmission.submissionDate}
275281
image={mySubmission.logo || '/placeholder.svg'}
282+
submissionId={mySubmission.id}
276283
isPinned={true}
277284
isMySubmission={true}
278285
onViewClick={() => handleViewSubmission(mySubmission.id)}

components/hackathons/team-formation/TeamFormationTab.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,13 @@ import { MyInvitationsList } from './MyInvitationsList';
3232
interface TeamFormationTabProps {
3333
hackathonSlugOrId?: string;
3434
organizationId?: string;
35+
isRegistered?: boolean;
3536
}
3637

3738
export function TeamFormationTab({
3839
hackathonSlugOrId,
3940
organizationId,
41+
isRegistered,
4042
}: TeamFormationTabProps) {
4143
const params = useParams();
4244
const { isAuthenticated, user } = useAuthStatus();
@@ -359,7 +361,7 @@ export function TeamFormationTab({
359361
? 'Try adjusting your filters or search term'
360362
: 'Be the first to create a team post!'}
361363
</p>
362-
{isAuthenticated && hackathonId && (
364+
{isAuthenticated && isRegistered && hackathonId && (
363365
<Button
364366
onClick={() => {
365367
setEditingPost(null);

0 commit comments

Comments
 (0)