diff --git a/app/(landing)/hackathons/[slug]/page.tsx b/app/(landing)/hackathons/[slug]/page.tsx
index 43e983bb..b74bf434 100644
--- a/app/(landing)/hackathons/[slug]/page.tsx
+++ b/app/(landing)/hackathons/[slug]/page.tsx
@@ -13,6 +13,7 @@ import { HackathonResources } from '@/components/hackathons/resources/resources'
import SubmissionTab from '@/components/hackathons/submissions/submissionTab';
import { HackathonDiscussions } from '@/components/hackathons/discussion/comment';
import { TeamFormationTab } from '@/components/hackathons/team-formation/TeamFormationTab';
+import { WinnersTab } from '@/components/hackathons/winners/WinnersTab';
import LoadingScreen from '@/features/projects/components/CreateProjectModal/LoadingScreen';
import { useTimelineEvents } from '@/hooks/hackathon/use-timeline-events';
import { toast } from 'sonner';
@@ -22,6 +23,7 @@ import { HackathonParticipants } from '@/components/hackathons/participants/hack
import { useCommentSystem } from '@/hooks/use-comment-system';
import { CommentEntityType } from '@/types/comment';
import { useTeamPosts } from '@/hooks/hackathon/use-team-posts';
+import { HackathonWinner } from '@/lib/api/hackathons';
export default function HackathonPage() {
const router = useRouter();
@@ -31,6 +33,7 @@ export default function HackathonPage() {
const {
currentHackathon,
submissions,
+ winners,
loading,
setCurrentHackathon,
refreshCurrentHackathon,
@@ -75,6 +78,19 @@ export default function HackathonPage() {
const isTabEnabled =
currentHackathon?.enabledTabs?.includes('joinATeamTab') !== false;
+ // For testing: Use mock winners if real winners are empty
+ // const displayWinners =
+ // winners && winners.length > 0 ? winners : MOCK_WINNERS;
+ // const hasWinners = displayWinners.length > 0;
+ const hasWinners = winners && winners.length > 0;
+
+ // For testing: Force enable winners tab
+ // const isWinnersTabEnabled =
+ // currentHackathon?.enabledTabs?.includes('winnersTab') !== false;
+ // const isWinnersTabEnabled = true;
+ const isWinnersTabEnabled =
+ currentHackathon?.enabledTabs?.includes('winnersTab') !== false;
+
const tabs = [
{ id: 'overview', label: 'Overview' },
...(hasParticipants
@@ -115,6 +131,13 @@ export default function HackathonPage() {
});
}
+ if (hasWinners && isWinnersTabEnabled) {
+ tabs.push({
+ id: 'winners',
+ label: 'Winners',
+ });
+ }
+
return tabs;
}, [
currentHackathon?.participants,
@@ -126,6 +149,7 @@ export default function HackathonPage() {
discussionComments.comments.length,
teamPosts.length,
hackathonId,
+ winners,
]);
// Refresh hackathon data
@@ -342,11 +366,9 @@ export default function HackathonPage() {
)}
{activeTab === 'resources' &&
- currentHackathon.resources?.length > 0 && ( // Direct array check
-
- )}
+ currentHackathon.resources?.length > 0 && }
{activeTab === 'participants' &&
- currentHackathon.participants?.length > 0 && ( // Direct array check
+ currentHackathon.participants?.length > 0 && (
)}
@@ -365,7 +387,14 @@ export default function HackathonPage() {
)}
{activeTab === 'team-formation' && (
-
+
+ )}
+
+ {activeTab === 'winners' && (
+
)}
{activeTab === 'resources' && currentHackathon?.resources?.[0] && (
diff --git a/app/(landing)/hackathons/layout.tsx b/app/(landing)/hackathons/layout.tsx
index 9bc3e940..68565993 100644
--- a/app/(landing)/hackathons/layout.tsx
+++ b/app/(landing)/hackathons/layout.tsx
@@ -1,4 +1,5 @@
import { HackathonDataProvider } from '@/lib/providers/hackathonProvider';
+import { OrganizationProvider } from '@/lib/providers/OrganizationProvider';
import { use } from 'react';
interface HackathonLayoutProps {
@@ -15,8 +16,10 @@ export default function HackathonLayout({
const resolvedParams = use(params);
return (
-
- {children}
-
+
+
+ {children}
+
+
);
}
diff --git a/app/(landing)/organizations/[id]/hackathons/[hackathonId]/page.tsx b/app/(landing)/organizations/[id]/hackathons/[hackathonId]/page.tsx
index a9ed2fcd..346e2a8a 100644
--- a/app/(landing)/organizations/[id]/hackathons/[hackathonId]/page.tsx
+++ b/app/(landing)/organizations/[id]/hackathons/[hackathonId]/page.tsx
@@ -1,7 +1,13 @@
'use client';
import { useParams } from 'next/navigation';
-import { Loader2, AlertCircle, Calendar, TrendingUp } from 'lucide-react';
+import {
+ Loader2,
+ AlertCircle,
+ Calendar,
+ TrendingUp,
+ Check,
+} from 'lucide-react';
import { useHackathons } from '@/hooks/use-hackathons';
import { useEffect } from 'react';
import { useHackathonAnalytics } from '@/hooks/use-hackathon-analytics';
@@ -23,8 +29,10 @@ export default function HackathonPage() {
autoFetch: false,
});
- const { statistics, statisticsLoading, timeSeriesData, timeSeriesLoading } =
- useHackathonAnalytics(organizationId, hackathonId);
+ const { analytics, loading: analyticsLoading } = useHackathonAnalytics(
+ organizationId,
+ hackathonId
+ );
useEffect(() => {
if (organizationId && hackathonId) {
@@ -60,6 +68,29 @@ export default function HackathonPage() {
);
}
+ // Adapt key metrics
+ const statistics = analytics?.summary || null;
+
+ // Adapt charts data
+ const timeSeriesData = analytics?.trends
+ ? {
+ submissions: {
+ daily: analytics.trends.submissionsOverTime.map(p => ({
+ date: p.date,
+ count: p.count,
+ })),
+ weekly: [],
+ },
+ participants: {
+ daily: analytics.trends.participantSignupsOverTime.map(p => ({
+ date: p.date,
+ count: p.count,
+ })),
+ weekly: [],
+ },
+ }
+ : null;
+
return (
}>
@@ -69,11 +100,6 @@ export default function HackathonPage() {
{currentHackathon?.name || 'Hackathon Dashboard'}
- {/* {currentHackathon?.information?.description && (
-
- {currentHackathon.information.description}
-
- )} */}
@@ -89,7 +115,7 @@ export default function HackathonPage() {
@@ -97,7 +123,7 @@ export default function HackathonPage() {
@@ -109,21 +135,97 @@ export default function HackathonPage() {
Timeline
- ({
- name: phase.name || '',
- startDate: phase.startDate || '',
- endDate: phase.endDate || '',
- })),
- }}
- />
+
+ {/* Render new timeline from analytics */}
+
+
+ {(() => {
+ const timelineEvents = analytics?.timeline || [];
+ const hasWinnerAnnouncement = timelineEvents.some(
+ e => e.phase === 'Winner Announcement'
+ );
+
+ // Manually append Winner Announcement if missing and date exists
+ const fullTimeline = [...timelineEvents];
+ if (!hasWinnerAnnouncement && currentHackathon?.endDate) {
+ const winnerDate = new Date(currentHackathon.endDate);
+ const now = new Date();
+ // Simple status logic for single date event
+ // If date is passed, completed. If today (roughly), ongoing?
+ // Or just use 'upcoming' if future, 'completed' if past.
+ // Ideally we'd match the phase logic, but for a single date event:
+ let status: 'completed' | 'ongoing' | 'upcoming' =
+ 'upcoming';
+ if (now > winnerDate) {
+ status = 'completed';
+ }
+ // For "Winner Announcement", it might be "ongoing" on the day of?
+ // keeping simple for now.
+
+ fullTimeline.push({
+ phase: 'Winner Announcement',
+ description:
+ 'Final results published and prizes distributed to winners.',
+ date: currentHackathon.endDate,
+ status: status,
+ });
+ }
+
+ return fullTimeline.map((phase, index) => {
+ const isLast = index === fullTimeline.length - 1;
+ const isActive = phase.status === 'ongoing';
+ const isCompleted = phase.status === 'completed';
+
+ return (
+
+
+ {isActive ? (
+
+ ) : isCompleted ? (
+
+
+
+ ) : (
+
+ )}
+ {!isLast && (
+
+ )}
+
+
+
+
+ {phase.phase}
+
+
+ {phase.description}
+
+
+
+ {new Date(phase.date).toLocaleDateString()}
+
+
+
+ );
+ });
+ })()}
+
+
diff --git a/app/(landing)/organizations/[id]/hackathons/[hackathonId]/settings/page.tsx b/app/(landing)/organizations/[id]/hackathons/[hackathonId]/settings/page.tsx
index 56ff60fc..2ba98388 100644
--- a/app/(landing)/organizations/[id]/hackathons/[hackathonId]/settings/page.tsx
+++ b/app/(landing)/organizations/[id]/hackathons/[hackathonId]/settings/page.tsx
@@ -12,6 +12,7 @@ import {
Trophy,
Handshake,
Sliders,
+ Eye,
} from 'lucide-react';
import { toast } from 'sonner';
import { api } from '@/lib/api/api';
@@ -21,6 +22,7 @@ import ParticipantSettingsTab from '@/components/organization/hackathons/setting
import RewardsSettingsTab from '@/components/organization/hackathons/settings/RewardsSettingsTab';
import CollaborationSettingsTab from '@/components/organization/hackathons/settings/CollaborationSettingsTab';
import AdvancedSettingsTab from '@/components/organization/hackathons/settings/AdvancedSettingsTab';
+import SubmissionVisibilitySettingsTab from '@/components/organization/hackathons/settings/SubmissionVisibilitySettingsTab';
import { AuthGuard } from '@/components/auth';
import Loading from '@/components/Loading';
@@ -189,6 +191,13 @@ export default function SettingsPage() {
Advanced
+
+
+ Submissions
+
@@ -253,6 +262,13 @@ export default function SettingsPage() {
isLoading={isSaving}
/>
+
+
+
+
diff --git a/app/(landing)/organizations/[id]/hackathons/page.tsx b/app/(landing)/organizations/[id]/hackathons/page.tsx
index 8a324e41..1f0fb3e0 100644
--- a/app/(landing)/organizations/[id]/hackathons/page.tsx
+++ b/app/(landing)/organizations/[id]/hackathons/page.tsx
@@ -157,7 +157,7 @@ export default function HackathonsPage() {
? (item.data as HackathonDraft).data.information?.categories
?.join(',')
?.toLowerCase() || ''
- : ''; // Categories filtering only applies to drafts for now
+ : '';
return category.includes(categoryFilter.toLowerCase());
});
}
@@ -494,9 +494,11 @@ export default function HackathonsPage() {
);
}
+ const publishedHackathon = hackathon as Hackathon;
+
return (
@@ -506,15 +508,15 @@ export default function HackathonsPage() {
- {hackathon.status === 'PUBLISHED'
+ {publishedHackathon.status === 'PUBLISHED'
? 'Live'
- : hackathon.status}
+ : publishedHackathon.status}
{endDate && (
@@ -531,11 +533,19 @@ export default function HackathonsPage() {
- 0 participants
+
+ {publishedHackathon.participants?.length ||
+ publishedHackathon._count?.participants ||
+ 0}{' '}
+ participants
+
- 0 submissions
+
+ {publishedHackathon._count?.submissions || 0}{' '}
+ submissions
+
{totalPrize > 0 && (
<>
@@ -560,7 +570,7 @@ export default function HackathonsPage() {
router.push(
- `/organizations/${organizationId}/hackathons/${hackathon.id}`
+ `/organizations/${organizationId}/hackathons/${publishedHackathon.id}`
)
}
className='flex h-9 w-9 items-center justify-center rounded-lg border border-zinc-800 bg-zinc-900/50 text-zinc-400 transition-all hover:border-zinc-700 hover:text-white'
@@ -571,7 +581,7 @@ export default function HackathonsPage() {
router.push(
- `/organizations/${organizationId}/hackathons/${hackathon.id}/settings`
+ `/organizations/${organizationId}/hackathons/${publishedHackathon.id}/settings`
)
}
className='flex h-9 w-9 items-center justify-center rounded-lg border border-zinc-800 bg-zinc-900/50 text-zinc-400 transition-all hover:border-zinc-700 hover:text-white'
@@ -580,7 +590,9 @@ export default function HackathonsPage() {
handleDeleteClick(hackathon.id)}
+ onClick={() =>
+ handleDeleteClick(publishedHackathon.id)
+ }
className='flex h-9 w-9 items-center justify-center rounded-lg border border-zinc-800 bg-zinc-900/50 text-zinc-400 transition-all hover:border-red-600 hover:text-red-500'
title='Delete Hackathon'
disabled={isDeleting}
diff --git a/components/hackathons/HackathonsPage.tsx b/components/hackathons/HackathonsPage.tsx
index 9643f954..9259882f 100644
--- a/components/hackathons/HackathonsPage.tsx
+++ b/components/hackathons/HackathonsPage.tsx
@@ -10,6 +10,7 @@ import { useHackathonTransform } from '@/hooks/hackathon/use-hackathon-transform
import { BoundlessButton } from '../buttons';
import { ArrowDownIcon, XIcon } from 'lucide-react';
import LoadingScreen from '@/features/projects/components/CreateProjectModal/LoadingScreen';
+import EmptyState from '@/components/EmptyState';
interface HackathonsPageProps {
className?: string;
@@ -123,8 +124,8 @@ export default function HackathonsPage({
{!loading && !error && hackathons.length === 0 && (
- {/*
*/}
+ />
{(filters.search ||
filters.category ||
@@ -144,7 +145,7 @@ export default function HackathonsPage({
Clear all filters
diff --git a/components/hackathons/submissions/submissionTab.tsx b/components/hackathons/submissions/submissionTab.tsx
index 7460dbaf..d67f65e6 100644
--- a/components/hackathons/submissions/submissionTab.tsx
+++ b/components/hackathons/submissions/submissionTab.tsx
@@ -264,15 +264,22 @@ const SubmissionTab: React.FC
= ({
: 'Pending'
}
upvotes={
- typeof mySubmission.votes === 'number' ? mySubmission.votes : 0
+ typeof mySubmission.votes === 'number'
+ ? mySubmission.votes
+ : Array.isArray(mySubmission.votes)
+ ? mySubmission.votes.length
+ : 0
}
comments={
typeof mySubmission.comments === 'number'
? mySubmission.comments
- : 0
+ : Array.isArray(mySubmission.comments)
+ ? mySubmission.comments.length
+ : 0
}
submittedDate={mySubmission.submissionDate}
image={mySubmission.logo || '/placeholder.svg'}
+ submissionId={mySubmission.id}
isPinned={true}
isMySubmission={true}
onViewClick={() => handleViewSubmission(mySubmission.id)}
diff --git a/components/hackathons/team-formation/TeamFormationTab.tsx b/components/hackathons/team-formation/TeamFormationTab.tsx
index 7cf89e71..d4334999 100644
--- a/components/hackathons/team-formation/TeamFormationTab.tsx
+++ b/components/hackathons/team-formation/TeamFormationTab.tsx
@@ -32,11 +32,13 @@ import { MyInvitationsList } from './MyInvitationsList';
interface TeamFormationTabProps {
hackathonSlugOrId?: string;
organizationId?: string;
+ isRegistered?: boolean;
}
export function TeamFormationTab({
hackathonSlugOrId,
organizationId,
+ isRegistered,
}: TeamFormationTabProps) {
const params = useParams();
const { isAuthenticated, user } = useAuthStatus();
@@ -359,7 +361,7 @@ export function TeamFormationTab({
? 'Try adjusting your filters or search term'
: 'Be the first to create a team post!'}
- {isAuthenticated && hackathonId && (
+ {isAuthenticated && isRegistered && hackathonId && (
{
setEditingPost(null);
diff --git a/components/hackathons/winners/WinnersTab.tsx b/components/hackathons/winners/WinnersTab.tsx
new file mode 100644
index 00000000..78f11f75
--- /dev/null
+++ b/components/hackathons/winners/WinnersTab.tsx
@@ -0,0 +1,173 @@
+'use client';
+
+import React from 'react';
+import Image from 'next/image';
+import Link from 'next/link';
+import { HackathonWinner } from '@/lib/api/hackathons';
+import { Trophy, Medal, Award } from 'lucide-react';
+import { motion } from 'framer-motion';
+
+interface WinnersTabProps {
+ winners: HackathonWinner[];
+ hackathonSlug?: string;
+}
+
+export const WinnersTab = ({ winners, hackathonSlug }: WinnersTabProps) => {
+ if (!winners || winners.length === 0) {
+ return (
+
+
+
+ Winners Not Announced Yet
+
+
+ The results are still being calculated. Check back soon to see who
+ took home the prizes!
+
+
+ );
+ }
+
+ // Sort by rank just in case
+ const sortedWinners = [...winners].sort((a, b) => a.rank - b.rank);
+
+ return (
+
+
+ {sortedWinners.map((winner, index) => (
+
+ ))}
+
+
+ );
+};
+
+const WinnerCard = ({
+ winner,
+ index,
+ hackathonSlug,
+}: {
+ winner: HackathonWinner;
+ index: number;
+ hackathonSlug?: string;
+}) => {
+ const getRankIcon = (rank: number) => {
+ switch (rank) {
+ case 1:
+ return ;
+ case 2:
+ return ;
+ case 3:
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ const getRankColor = (rank: number) => {
+ switch (rank) {
+ case 1:
+ return 'border-yellow-500/50 bg-yellow-500/5 hover:border-yellow-500';
+ case 2:
+ return 'border-gray-300/50 bg-gray-300/5 hover:border-gray-300';
+ case 3:
+ return 'border-amber-600/50 bg-amber-600/5 hover:border-amber-600';
+ default:
+ return 'border-white/10 bg-white/5 hover:border-white/20';
+ }
+ };
+
+ const CardContent = (
+
+
+
+ {getRankIcon(winner.rank)}
+
+
+ Rank #{winner.rank}
+
+
+
+
+ {winner.projectName}
+
+
+
+
+ {winner.participants.map((p, i) => (
+
+ {p.avatar ? (
+
+ ) : (
+
+ {p.username.charAt(0).toUpperCase()}
+
+ )}
+
+ ))}
+
+
+ {winner.teamName ||
+ (winner.participants.length === 1
+ ? winner.participants[0].username
+ : 'Team')}
+
+
+
+
+
+ Prize
+ {winner.prize}
+
+
+
+ );
+
+ if (winner.projectId) {
+ return (
+
+
+ {CardContent}
+
+
+ );
+ }
+
+ return (
+
+ {CardContent}
+
+ );
+};
diff --git a/components/organization/hackathons/details/HackathonCharts.tsx b/components/organization/hackathons/details/HackathonCharts.tsx
index 7c7acd2e..e161b5a8 100644
--- a/components/organization/hackathons/details/HackathonCharts.tsx
+++ b/components/organization/hackathons/details/HackathonCharts.tsx
@@ -29,7 +29,7 @@ export const HackathonCharts: React.FC = ({
}) => {
const submissionsChartData = useMemo(() => {
if (!timeSeriesData) return [];
- const daily = timeSeriesData.submissions.daily;
+ const daily = timeSeriesData.submissions?.daily || [];
return daily.map(point => ({
month: new Date(point.date).toLocaleDateString('en-US', {
month: 'short',
@@ -40,7 +40,7 @@ export const HackathonCharts: React.FC = ({
const participantsChartData = useMemo(() => {
if (!timeSeriesData) return [];
- const daily = timeSeriesData.participants.daily;
+ const daily = timeSeriesData.participants?.daily || [];
return daily.map(point => ({
month: new Date(point.date).toLocaleDateString('en-US', {
month: 'short',
@@ -51,7 +51,7 @@ export const HackathonCharts: React.FC = ({
return (
-
+
Submissions over time
@@ -99,7 +99,7 @@ export const HackathonCharts: React.FC
= ({
)}
-
+
Participants sign-ups trend
diff --git a/components/organization/hackathons/settings/SubmissionVisibilitySettingsTab.tsx b/components/organization/hackathons/settings/SubmissionVisibilitySettingsTab.tsx
new file mode 100644
index 00000000..8bf72f28
--- /dev/null
+++ b/components/organization/hackathons/settings/SubmissionVisibilitySettingsTab.tsx
@@ -0,0 +1,236 @@
+'use client';
+
+import React, { useState, useEffect } from 'react';
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+ FormDescription,
+} from '@/components/ui/form';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { z } from 'zod';
+import { BoundlessButton } from '@/components/buttons';
+import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
+import { toast } from 'sonner';
+import {
+ SubmissionVisibility,
+ SubmissionStatusVisibility,
+ updateSubmissionVisibility,
+ getHackathon,
+} from '@/lib/api/hackathons';
+import Loading from '@/components/Loading';
+
+const visibilitySettingsSchema = z.object({
+ submissionVisibility: z.nativeEnum(SubmissionVisibility),
+ submissionStatusVisibility: z.nativeEnum(SubmissionStatusVisibility),
+});
+
+type VisibilitySettingsFormData = z.infer
;
+
+interface SubmissionVisibilitySettingsTabProps {
+ organizationId: string;
+ hackathonId: string;
+}
+
+export default function SubmissionVisibilitySettingsTab({
+ organizationId,
+ hackathonId,
+}: SubmissionVisibilitySettingsTabProps) {
+ const [isLoading, setIsLoading] = useState(true);
+ const [isSaving, setIsSaving] = useState(false);
+
+ const form = useForm({
+ resolver: zodResolver(visibilitySettingsSchema),
+ defaultValues: {
+ submissionVisibility: SubmissionVisibility.PUBLIC,
+ submissionStatusVisibility: SubmissionStatusVisibility.ALL,
+ },
+ });
+
+ useEffect(() => {
+ const fetchSettings = async () => {
+ try {
+ const response = await getHackathon(hackathonId);
+ if (response?.data) {
+ form.reset({
+ submissionVisibility:
+ response.data.submissionVisibility || SubmissionVisibility.PUBLIC,
+ submissionStatusVisibility:
+ response.data.submissionStatusVisibility ||
+ SubmissionStatusVisibility.ALL,
+ });
+ }
+ } catch (error) {
+ toast.error('Failed to fetch visibility settings');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchSettings();
+ }, [hackathonId]);
+
+ const onSubmit = async (data: VisibilitySettingsFormData) => {
+ setIsSaving(true);
+ try {
+ await updateSubmissionVisibility(organizationId, hackathonId, data);
+ toast.success('Visibility settings updated successfully!');
+ } catch (error) {
+ toast.error('Failed to update visibility settings');
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ if (isLoading) {
+ return ;
+ }
+
+ return (
+
+
+
+
+ Submission Visibility
+
+
+ Control who can view submissions and which projects are displayed in
+ the showcase.
+
+
+
+
+
+
+
+ );
+}
diff --git a/hooks/hackathon/use-submissions.ts b/hooks/hackathon/use-submissions.ts
index e714c10b..84b99a06 100644
--- a/hooks/hackathon/use-submissions.ts
+++ b/hooks/hackathon/use-submissions.ts
@@ -1,8 +1,56 @@
import { useState, useMemo } from 'react';
import { useHackathonData } from '@/lib/providers/hackathonProvider';
+import { useOrganizationUtils } from '@/lib/providers/useOrganization';
+import {
+ SubmissionVisibility,
+ SubmissionStatusVisibility,
+} from '@/lib/api/hackathons';
export function useSubmissions() {
- const { submissions } = useHackathonData();
+ const {
+ currentHackathon,
+ submissions: privateSubmissions,
+ exploreSubmissions,
+ } = useHackathonData();
+ const { canManage } = useOrganizationUtils();
+
+ const isOrganizer = currentHackathon
+ ? canManage(currentHackathon.organizationId)
+ : false;
+ const isParticipant = currentHackathon?.isParticipant ?? false;
+
+ // For the public showcase, we prioritize the exploreSubmissions endpoint.
+ // If we're an organizer, we might want to see the full list of private submissions.
+ const allSubmissions =
+ exploreSubmissions.length > 0 ? exploreSubmissions : privateSubmissions;
+
+ const submissions = useMemo(() => {
+ if (isOrganizer) return allSubmissions;
+
+ let filtered = allSubmissions;
+
+ // Check who can view submissions
+ if (
+ currentHackathon?.submissionVisibility ===
+ SubmissionVisibility.PARTICIPANTS_ONLY &&
+ !isParticipant
+ ) {
+ return [];
+ }
+
+ // Check which submission statuses are visible
+ if (
+ currentHackathon?.submissionStatusVisibility ===
+ SubmissionStatusVisibility.ACCEPTED_SHORTLISTED
+ ) {
+ filtered = filtered.filter(
+ sub => sub.status?.toLowerCase() === 'approved'
+ );
+ }
+
+ return filtered;
+ }, [allSubmissions, isOrganizer, isParticipant, currentHackathon]);
+
const [searchTerm, setSearchTerm] = useState('');
const [selectedSort, setSelectedSort] = useState('newest');
const [selectedCategory, setSelectedCategory] = useState('All Categories');
diff --git a/hooks/use-hackathon-analytics.ts b/hooks/use-hackathon-analytics.ts
index 32b83fd1..263a0324 100644
--- a/hooks/use-hackathon-analytics.ts
+++ b/hooks/use-hackathon-analytics.ts
@@ -1,104 +1,64 @@
import { useEffect, useState } from 'react';
+import { getHackathonAnalytics } from '@/lib/api/hackathon';
import {
- getHackathonStatistics,
- getHackathonTimeSeries,
- type HackathonStatistics,
- type HackathonTimeSeriesData,
+ type HackathonAnalyticsSummary,
+ type HackathonAnalyticsTrends,
+ type TimelineEvent,
} from '@/lib/api/hackathons';
interface UseHackathonAnalyticsReturn {
- statistics: HackathonStatistics | null;
- statisticsLoading: boolean;
- statisticsError: string | null;
- timeSeriesData: HackathonTimeSeriesData | null;
- timeSeriesLoading: boolean;
- timeSeriesError: string | null;
+ analytics: {
+ summary: HackathonAnalyticsSummary;
+ trends: HackathonAnalyticsTrends;
+ timeline: TimelineEvent[];
+ } | null;
+ loading: boolean;
+ error: string | null;
}
export const useHackathonAnalytics = (
organizationId: string | undefined,
hackathonId: string | undefined
): UseHackathonAnalyticsReturn => {
- const [statistics, setStatistics] = useState(
- null
- );
- const [statisticsLoading, setStatisticsLoading] = useState(false);
- const [statisticsError, setStatisticsError] = useState(null);
-
- const [timeSeriesData, setTimeSeriesData] =
- useState(null);
- const [timeSeriesLoading, setTimeSeriesLoading] = useState(false);
- const [timeSeriesError, setTimeSeriesError] = useState(null);
+ const [data, setData] =
+ useState(null);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
useEffect(() => {
- const fetchStatistics = async () => {
+ const fetchAnalytics = async () => {
if (!organizationId || !hackathonId) return;
- setStatisticsLoading(true);
- setStatisticsError(null);
+ setLoading(true);
+ setError(null);
try {
- const response = await getHackathonStatistics(
+ const response = await getHackathonAnalytics(
organizationId,
hackathonId
);
- setStatistics(response.data);
- } catch (error) {
- const errorMessage =
- error instanceof Error ? error.message : 'Failed to load statistics';
- setStatisticsError(errorMessage);
- setStatistics({
- participantsCount: 0,
- submissionsCount: 0,
- activeJudges: 0,
- completedMilestones: 0,
+ setData({
+ summary: response.data.summary,
+ trends: response.data.trends,
+ timeline: response.data.timeline,
});
- } finally {
- setStatisticsLoading(false);
- }
- };
-
- if (organizationId && hackathonId) {
- void fetchStatistics();
- }
- }, [organizationId, hackathonId]);
-
- useEffect(() => {
- const fetchTimeSeries = async () => {
- if (!organizationId || !hackathonId) return;
-
- setTimeSeriesLoading(true);
- setTimeSeriesError(null);
- try {
- const response = await getHackathonTimeSeries(
- organizationId,
- hackathonId
- );
- setTimeSeriesData(response.data);
- } catch (error) {
+ } catch (err) {
const errorMessage =
- error instanceof Error ? error.message : 'Failed to load analytics';
- setTimeSeriesError(errorMessage);
-
- setTimeSeriesData({
- submissions: { daily: [], weekly: [] },
- participants: { daily: [], weekly: [] },
- });
+ err instanceof Error ? err.message : 'Failed to load analytics';
+ setError(errorMessage);
+ setData(null);
} finally {
- setTimeSeriesLoading(false);
+ setLoading(false);
}
};
if (organizationId && hackathonId) {
- void fetchTimeSeries();
+ void fetchAnalytics();
}
}, [organizationId, hackathonId]);
return {
- statistics,
- statisticsLoading,
- statisticsError,
- timeSeriesData,
- timeSeriesLoading,
- timeSeriesError,
+ analytics: data,
+ loading,
+ error,
};
};
diff --git a/lib/api/hackathon.ts b/lib/api/hackathon.ts
index a05205c8..b450807e 100644
--- a/lib/api/hackathon.ts
+++ b/lib/api/hackathon.ts
@@ -1,7 +1,12 @@
import { api } from './api';
import { SubmissionCardProps, ParticipantsResponse } from '@/types/hackathon';
-// Discussion type removed - using generic Comment type from @/types/comment
-import { GetHackathonResponse, Hackathon } from '@/lib/api/hackathons';
+import {
+ GetHackathonResponse,
+ Hackathon,
+ GetHackathonWinnersResponse,
+ HackathonWinner,
+ GetHackathonAnalyticsResponse,
+} from '@/lib/api/hackathons';
export interface HackathonListResponse {
success: boolean;
@@ -46,13 +51,6 @@ export const getHackathons = async (): Promise => {
return response.data;
};
-// Get single hackathon by slug
-// export const getHackathon = async (
-// slug: string
-// ): Promise => {
-// const response = await api.get(`/hackathons/${slug}`);
-// return response.data;
-// };
export const getHackathon = async (
slug: string
): Promise => {
@@ -86,6 +84,15 @@ export const getHackathonParticipants = async (
return response.data;
};
+export const getHackathonAnalytics = async (
+ organizationId: string,
+ hackathonId: string
+): Promise => {
+ const url = `/organizations/${organizationId}/hackathons/${hackathonId}/analytics`;
+ const res = await api.get(url);
+ return res.data;
+};
+
// Get submissions for a hackathon
export const getHackathonSubmissions = async (
slug: string,
@@ -103,8 +110,12 @@ export const getHackathonSubmissions = async (
return response.data;
};
-// Get discussions for a hackathon (you'll need to implement this endpoint)
-// export const getHackathonDiscussions = async (hackathonId: string): Promise => {
-// const response = await api.get(`/hackathons/${hackathonId}/discussions`);
-// return response.data;
-// };
+// Get winners for a hackathon
+export const getHackathonWinners = async (
+ idOrSlug: string
+): Promise => {
+ const response = await api.get(
+ `/hackathons/${idOrSlug}/winners`
+ );
+ return response.data;
+};
diff --git a/lib/api/hackathons.ts b/lib/api/hackathons.ts
index 39b63746..252856d3 100644
--- a/lib/api/hackathons.ts
+++ b/lib/api/hackathons.ts
@@ -29,6 +29,16 @@ export enum ParticipantType {
TEAM_OR_INDIVIDUAL = 'team_or_individual',
}
+export enum SubmissionVisibility {
+ PUBLIC = 'PUBLIC',
+ PARTICIPANTS_ONLY = 'PARTICIPANTS_ONLY',
+}
+
+export enum SubmissionStatusVisibility {
+ ALL = 'ALL',
+ ACCEPTED_SHORTLISTED = 'ACCEPTED_SHORTLISTED',
+}
+
export enum VenueType {
VIRTUAL = 'virtual',
PHYSICAL = 'physical',
@@ -411,6 +421,9 @@ export type Hackathon = {
telegram: string;
socialLinks: string[];
+ submissionVisibility?: SubmissionVisibility;
+ submissionStatusVisibility?: SubmissionStatusVisibility;
+
publishedAt: string;
createdAt: string;
updatedAt: string;
@@ -519,6 +532,49 @@ export interface GetHackathonsResponse extends ApiResponse {
}
// Statistics and Analytics Types
+export interface HackathonAnalyticsSummary {
+ participantsCount: number;
+ submissionsCount: number;
+ activeJudges: number;
+ completedMilestones: number;
+}
+
+export interface AnalyticsTrendPoint {
+ date: string;
+ count: number;
+}
+
+export interface HackathonAnalyticsTrends {
+ submissionsOverTime: AnalyticsTrendPoint[];
+ participantSignupsOverTime: AnalyticsTrendPoint[];
+}
+
+export interface TimelineEvent {
+ phase: string;
+ description: string;
+ date: string;
+ status: 'completed' | 'ongoing' | 'upcoming';
+}
+
+export interface GetHackathonAnalyticsResponse extends ApiResponse<{
+ hackathonId: string;
+ summary: HackathonAnalyticsSummary;
+ trends: HackathonAnalyticsTrends;
+ timeline: TimelineEvent[];
+}> {
+ success: true;
+ data: {
+ hackathonId: string;
+ summary: HackathonAnalyticsSummary;
+ trends: HackathonAnalyticsTrends;
+ timeline: TimelineEvent[];
+ };
+}
+
+// Deprecated or legacy statistics types (keeping if still used elsewhere, otherwise replacing if identical)
+// Checking usage, it seems these might be used by existing hooks.
+// Given the request asks for a specific response structure, I will add the new ones.
+
export interface HackathonStatistics {
participantsCount: number;
submissionsCount: number;
@@ -630,6 +686,46 @@ export interface ParticipantSubmission {
reviewedAt?: string | null;
}
+export interface ExploreSubmissionsResponse {
+ id: string;
+ hackathonId: string;
+ projectId: string;
+ participantId: string;
+ organizationId: string;
+ participationType: 'INDIVIDUAL' | 'TEAM' | 'TEAM_OR_INDIVIDUAL';
+ teamId?: string;
+ teamName?: string;
+ teamMembers?: Array<{
+ userId: string;
+ name: string;
+ username: string;
+ role: string;
+ avatar?: string;
+ }>;
+ projectName: string;
+ category: string;
+ description: string;
+ logo?: string;
+ videoUrl?: string;
+ introduction?: string;
+ links: Array<{
+ type: string;
+ url: string;
+ }>;
+ socialLinks: {
+ github?: string;
+ telegram?: string;
+ twitter?: string;
+ email?: string;
+ };
+ status: string;
+ rank?: number;
+ registeredAt: string;
+ submittedAt: string;
+ createdAt: string;
+ updatedAt: string;
+}
+
export interface Participant {
id: string;
userId: string;
@@ -1142,6 +1238,24 @@ export const acceptTeamInvitationToken = async (
return res.data;
};
+/**
+ * Update hackathon submission visibility settings
+ */
+export const updateSubmissionVisibility = async (
+ organizationId: string,
+ hackathonId: string,
+ data: {
+ submissionVisibility: SubmissionVisibility;
+ submissionStatusVisibility: SubmissionStatusVisibility;
+ }
+): Promise> => {
+ const res = await api.patch(
+ `/organizations/${organizationId}/hackathons/${hackathonId}/visibility`,
+ data
+ );
+ return res.data;
+};
+
/**
* Update an existing published hackathon
*/
@@ -1169,16 +1283,7 @@ export const getHackathon = async (
hackathonId: string
): Promise => {
const res = await api.get(`/hackathons/${hackathonId}`);
-
- return {
- success: true,
- data: res.data,
- message: 'Hackathon retrieved successfully',
- meta: {
- timestamp: new Date().toISOString(),
- requestId: '',
- },
- };
+ return res.data;
};
/**
@@ -1471,6 +1576,25 @@ export const getHackathonSubmissions = async (
return res.data;
};
+/**
+ * Explore hackathon submissions (Public showcase)
+ */
+export const getExploreSubmissions = async (
+ hackathonId: string,
+ page?: number,
+ limit?: number
+): Promise => {
+ const params = new URLSearchParams();
+ if (page) params.append('page', page.toString());
+ if (limit) params.append('limit', limit.toString());
+
+ const res = await api.get(
+ `/hackathons/${hackathonId}/submissions/explore${params.toString() ? `?${params.toString()}` : ''}`
+ );
+
+ return res.data;
+};
+
/**
* Register for a hackathon
* Supports both slug-based (public) and organization/hackathon ID (authenticated) endpoints
@@ -2594,3 +2718,25 @@ export const toggleRoleHired = async (
const res = await api.patch(url, data);
return res.data;
};
+
+export interface HackathonWinner {
+ rank: number;
+ projectName: string;
+ projectId?: string;
+ teamName: string | null;
+ participants: Array<{
+ userId?: string;
+ username: string;
+ avatar?: string;
+ }>;
+ prize: string;
+ submissionId: string;
+ slug?: string;
+}
+
+export interface GetHackathonWinnersResponse extends ApiResponse<{
+ hackathonId: string;
+ winners: HackathonWinner[];
+}> {
+ success: true;
+}
diff --git a/lib/providers/hackathonProvider.tsx b/lib/providers/hackathonProvider.tsx
index 342a7e1b..0f70a78e 100644
--- a/lib/providers/hackathonProvider.tsx
+++ b/lib/providers/hackathonProvider.tsx
@@ -11,13 +11,44 @@ import React, {
} from 'react';
import { SubmissionCardProps } from '@/types/hackathon';
import { Comment } from '@/types/comment';
-import { Hackathon, HackathonResourceItem } from '@/lib/api/hackathons';
+import {
+ Hackathon,
+ HackathonResourceItem,
+ getExploreSubmissions,
+ ExploreSubmissionsResponse,
+ HackathonWinner,
+} from '@/lib/api/hackathons';
import {
getHackathons,
getHackathon,
getHackathonSubmissions,
+ getHackathonWinners,
} from '@/lib/api/hackathon';
+// -------------------
+// Status Mapper
+// -------------------
+
+/**
+ * Maps API submission status to UI status values
+ * API: 'SUBMITTED' | 'SHORTLISTED' | 'DISQUALIFIED' | 'WITHDRAWN'
+ * UI: 'Pending' | 'Approved' | 'Rejected'
+ */
+function mapSubmissionStatus(apiStatus: string): SubmissionCardProps['status'] {
+ const normalized = apiStatus?.toUpperCase();
+
+ switch (normalized) {
+ case 'SHORTLISTED':
+ return 'Approved';
+ case 'DISQUALIFIED':
+ case 'WITHDRAWN':
+ return 'Rejected';
+ case 'SUBMITTED':
+ default:
+ return 'Pending';
+ }
+}
+
// -------------------
// Types
// -------------------
@@ -54,6 +85,8 @@ interface HackathonDataContextType {
currentHackathon: Hackathon | null;
discussions: Comment[]; // Using generic Comment type
submissions: SubmissionCardProps[];
+ exploreSubmissions: SubmissionCardProps[];
+ winners: HackathonWinner[];
// content: string;
timelineEvents: TimelineEvent[];
prizes: Prize[];
@@ -108,6 +141,10 @@ export function HackathonDataProvider({
string | null
>(hackathonSlug || null);
const [submissions, setSubmissions] = useState([]);
+ const [exploreSubmissions, setExploreSubmissions] = useState<
+ SubmissionCardProps[]
+ >([]);
+ const [winners, setWinners] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setErrorState] = useState(null);
@@ -204,6 +241,43 @@ export function HackathonDataProvider({
}
}, []);
+ const fetchExploreSubmissions = useCallback(async (hackathonId: string) => {
+ try {
+ const submissions = await getExploreSubmissions(hackathonId);
+ const mappedSubmissions: SubmissionCardProps[] = submissions.map(sub => ({
+ _id: sub.id,
+ title: sub.projectName,
+ description: sub.description,
+ submitterName:
+ sub.teamName || sub.teamMembers?.[0]?.name || 'Unknown Participant',
+ submitterAvatar: sub.teamMembers?.[0]?.avatar || sub.logo || '',
+ category: sub.category,
+ status: mapSubmissionStatus(sub.status),
+ upvotes: 0,
+ submittedDate: sub.submittedAt,
+ image: sub.logo || '/placeholder.svg',
+ }));
+ setExploreSubmissions(mappedSubmissions);
+ } catch {
+ /* ignore */
+ }
+ }, []);
+
+ // --------------------------------
+ // Fetch winners
+ // --------------------------------
+ const fetchWinners = useCallback(async (hackathonIdOrSlug: string) => {
+ try {
+ const response = await getHackathonWinners(hackathonIdOrSlug);
+ if (response.success && response.data) {
+ setWinners(response.data.winners);
+ }
+ } catch {
+ // If 404 or other error, simply don't show winners
+ setWinners([]);
+ }
+ }, []);
+
// --------------------------------
// Computed lists
// --------------------------------
@@ -239,10 +313,20 @@ export function HackathonDataProvider({
const data = await fetchHackathonBySlug(slug);
if (data) {
- await Promise.all([fetchSubmissions(slug)]);
+ await Promise.all([
+ fetchSubmissions(slug),
+ fetchExploreSubmissions(data.id),
+ fetchWinners(data.id), // Fetch by ID as per spec, but slug likely works too if API supports it
+ ]);
}
},
- [currentHackathonSlug, fetchHackathonBySlug, fetchSubmissions]
+ [
+ currentHackathonSlug,
+ fetchHackathonBySlug,
+ fetchSubmissions,
+ fetchExploreSubmissions,
+ fetchWinners,
+ ]
);
const refreshHackathons = async () => {
@@ -378,6 +462,8 @@ export function HackathonDataProvider({
currentHackathon,
discussions: mockDiscussions,
submissions,
+ exploreSubmissions,
+ winners,
// content: mockContent,
timelineEvents: mockTimelineEvents,
prizes: mockPrizes,
diff --git a/package-lock.json b/package-lock.json
index d36792e4..54b4ebf9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7609,13 +7609,13 @@
}
},
"node_modules/axios": {
- "version": "1.13.2",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
- "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
+ "version": "1.13.5",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
+ "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
"license": "MIT",
"dependencies": {
- "follow-redirects": "^1.15.6",
- "form-data": "^4.0.4",
+ "follow-redirects": "^1.15.11",
+ "form-data": "^4.0.5",
"proxy-from-env": "^1.1.0"
}
},