diff --git a/app/(landing)/hackathons/[slug]/page.tsx b/app/(landing)/hackathons/[slug]/page.tsx
index 8429926da..7f517999b 100644
--- a/app/(landing)/hackathons/[slug]/page.tsx
+++ b/app/(landing)/hackathons/[slug]/page.tsx
@@ -6,7 +6,6 @@ import { useHackathonData } from '@/lib/providers/hackathonProvider';
import { useRegisterHackathon } from '@/hooks/hackathon/use-register-hackathon';
import { useLeaveHackathon } from '@/hooks/hackathon/use-leave-hackathon';
import { RegisterHackathonModal } from '@/components/hackathons/overview/RegisterHackathonModal';
-
import { HackathonBanner } from '@/components/hackathons/hackathonBanner';
import { HackathonNavTabs } from '@/components/hackathons/hackathonNavTabs';
import { HackathonOverview } from '@/components/hackathons/overview/hackathonOverview';
@@ -18,8 +17,8 @@ import { TeamFormationTab } from '@/components/hackathons/team-formation/TeamFor
import LoadingScreen from '@/components/landing-page/project/CreateProjectModal/LoadingScreen';
import { useTimelineEvents } from '@/hooks/hackathon/use-timeline-events';
import { toast } from 'sonner';
-import { Participant } from '@/lib/api/hackathons';
-// import { HackathonResources } from '@/components/hackathons/resources/resources';
+import type { Participant } from '@/lib/api/hackathons';
+import { HackathonStickyCard } from '@/components/hackathons/hackathonStickyCard';
export default function HackathonPage() {
const router = useRouter();
@@ -64,7 +63,6 @@ export default function HackathonPage() {
},
]
: []),
-
{
id: 'submission',
label: 'Submissions',
@@ -76,7 +74,6 @@ export default function HackathonPage() {
const participantType = currentHackathon?.participation?.participantType;
const isTeamHackathon =
participantType === 'team' || participantType === 'team_or_individual';
-
const isTabEnabled =
currentHackathon?.participation?.tabVisibility?.joinATeamTab !== false;
@@ -96,19 +93,18 @@ export default function HackathonPage() {
const [activeTab, setActiveTab] = useState('overview');
const [showRegisterModal, setShowRegisterModal] = useState(false);
- // Function to refresh all hackathon data
+ // Refresh hackathon data
const refreshHackathonData = useCallback(async () => {
if (hackathonId && refreshCurrentHackathon) {
await refreshCurrentHackathon();
}
}, [hackathonId, refreshCurrentHackathon]);
- // Registration logic - use the updated hook with setters
+ // Registration status
const {
isRegistered,
hasSubmitted,
checkStatus,
- // isChecking,
setIsRegistered,
setParticipant,
} = useRegisterHackathon({
@@ -117,29 +113,29 @@ export default function HackathonPage() {
autoCheck: !!hackathonId,
});
- // Leave hackathon logic
+ // Leave hackathon functionality
const { isLeaving, leave: leaveHackathon } = useLeaveHackathon({
hackathonSlugOrId: hackathonId,
organizationId: undefined,
});
- const isEnded = useMemo(() => {
- if (!currentHackathon?.deadline) return false;
- const deadline = new Date(currentHackathon.deadline);
- const now = new Date();
- return now > deadline;
- }, [currentHackathon?.deadline]);
+ // Check if hackathon has ended
+ // const isEnded = useMemo(() => {
+ // if (!currentHackathon?.deadline) return false
+ // const deadline = new Date(currentHackathon.deadline)
+ // const now = new Date()
+ // return now > deadline
+ // }, [currentHackathon?.deadline])
- // Check if team formation is available
+ // Team formation availability
const isTeamHackathon =
currentHackathon?.participation?.participantType === 'team' ||
currentHackathon?.participation?.participantType === 'team_or_individual';
-
const isTeamFormationEnabled =
isTeamHackathon &&
currentHackathon?.participation?.tabVisibility?.joinATeamTab !== false;
- // Button handlers
+ // Event handlers
const handleJoinClick = () => {
setShowRegisterModal(true);
};
@@ -148,11 +144,8 @@ export default function HackathonPage() {
try {
setIsRegistered(false);
setParticipant(null);
-
await leaveHackathon();
-
refreshHackathonData();
-
router.push('?tab=overview');
} catch (error) {
const errorMessage =
@@ -164,13 +157,9 @@ export default function HackathonPage() {
};
const handleRegisterSuccess = async (participantData: Participant) => {
- // IMMEDIATELY update state with the returned participant data
setIsRegistered(true);
setParticipant(participantData);
-
- // Refresh hackathon data in background (participants count)
await refreshHackathonData();
-
router.push('?tab=submission');
};
@@ -186,12 +175,14 @@ export default function HackathonPage() {
router.push('?tab=team-formation');
};
+ // Set current hackathon on mount
useEffect(() => {
if (hackathonId) {
setCurrentHackathon(hackathonId);
}
}, [hackathonId, setCurrentHackathon]);
+ // Handle tab changes from URL
useEffect(() => {
const tabFromUrl = searchParams.get('tab');
if (tabFromUrl && hackathonTabs.some(tab => tab.id === tabFromUrl)) {
@@ -206,10 +197,12 @@ export default function HackathonPage() {
router.push(`?${params.toString()}`, { scroll: false });
};
+ // Loading state
if (loading) {
return ;
}
+ // Hackathon not found
if (!currentHackathon) {
return (
@@ -225,33 +218,100 @@ export default function HackathonPage() {
);
}
+ // Shared props for banner and sticky card
+ const sharedActionProps = {
+ deadline: currentHackathon.deadline,
+ startDate: currentHackathon.startDate,
+ totalPrizePool: currentHackathon.totalPrizePool,
+ isRegistered,
+ hasSubmitted,
+ isTeamFormationEnabled,
+ registrationDeadlinePolicy: currentHackathon.registrationDeadlinePolicy as
+ | 'before_start'
+ | 'before_submission_deadline'
+ | 'custom',
+ registrationDeadline: currentHackathon.registrationDeadline,
+ onJoinClick: handleJoinClick,
+ onLeaveClick: handleLeaveClick,
+ isLeaving,
+ onSubmitClick: handleSubmitClick,
+ onViewSubmissionClick: handleViewSubmissionClick,
+ onFindTeamClick: handleFindTeamClick,
+ };
+
return (
-
- {/* Banner */}
-
+
+
+ {/* Main Content (2/3 width on desktop) */}
+
+ {/* Banner - Shows on all screens */}
+
+
+ {/* Navigation Tabs */}
+
+
+ {/* Tab Content */}
+
+ {activeTab === 'overview' && (
+
+ )}
+
+ {activeTab === 'participants' && participants.length > 0 && (
+
+ )}
+
+ {activeTab === 'submission' && (
+
+ )}
+
+ {activeTab === 'discussions' && (
+
+ )}
+
+ {activeTab === 'team-formation' && (
+
+ )}
+
+ {activeTab === 'resources' &&
+ currentHackathon?.resources?.resources?.[0] && (
+
+ )}
+
+
+
+ {/* Sidebar - Sticky Card (1/3 width on desktop, hidden on mobile) */}
+
+
+
+
{/* Registration Modal */}
{hackathonId && (
@@ -260,7 +320,7 @@ export default function HackathonPage() {
onOpenChange={setShowRegisterModal}
hackathonSlugOrId={hackathonId}
organizationId={undefined}
- onSuccess={handleRegisterSuccess} // Now passes participant data
+ onSuccess={handleRegisterSuccess}
participantType={
(currentHackathon?.participantType as
| 'team'
@@ -269,53 +329,6 @@ export default function HackathonPage() {
}
/>
)}
-
- {/* Tabs */}
-
-
- {/* Content */}
-
- {activeTab === 'overview' && (
-
- )}
-
- {activeTab === 'participants' && participants.length > 0 && (
-
- )}
-
- {activeTab === 'submission' && (
-
- )}
-
- {activeTab === 'discussions' && (
-
- )}
-
- {activeTab === 'team-formation' && (
-
- )}
- {activeTab === 'resources' &&
- currentHackathon?.resources?.resources?.[0] && (
-
- )}
-
);
}
diff --git a/app/(landing)/page.tsx b/app/(landing)/page.tsx
index ba79e4989..b25cccbd2 100644
--- a/app/(landing)/page.tsx
+++ b/app/(landing)/page.tsx
@@ -11,7 +11,7 @@ export default function LandingPage() {
return (
-
+
{/*
*/}
diff --git a/components/hackathons/hackathonBanner.tsx b/components/hackathons/hackathonBanner.tsx
index bcd250aae..666083637 100644
--- a/components/hackathons/hackathonBanner.tsx
+++ b/components/hackathons/hackathonBanner.tsx
@@ -1,13 +1,18 @@
'use client';
-
-import { Card } from '@/components/ui/card';
-import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
-import { formatDate } from '@/lib/utils';
-import { useEffect, useState, useRef, useMemo } from 'react';
-import { FileText, Users, ArrowRight, Calendar } from 'lucide-react';
+import { useMemo } from 'react';
+import {
+ FileText,
+ Users,
+ ArrowRight,
+ Calendar,
+ Clock,
+ Trophy,
+} from 'lucide-react';
import { useAuthStatus } from '@/hooks/use-auth';
import { useRouter, usePathname } from 'next/navigation';
+import Image from 'next/image';
+import { useHackathonStatus } from '@/hooks/hackathon/use-hackathon-status';
interface HackathonBannerProps {
title: string;
@@ -37,86 +42,9 @@ interface HackathonBannerProps {
isLeaving?: boolean;
}
-interface TimeRemaining {
- days: number;
- hours: number;
- minutes: number;
- seconds: number;
- total: number;
-}
-
-function calculateTimeRemaining(targetDate: string): TimeRemaining {
- const now = new Date().getTime();
- const target = new Date(targetDate).getTime();
- const difference = target - now;
-
- if (difference <= 0) {
- return { days: 0, hours: 0, minutes: 0, seconds: 0, total: 0 };
- }
-
- return {
- days: Math.floor(difference / (1000 * 60 * 60 * 24)),
- hours: Math.floor((difference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)),
- minutes: Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60)),
- seconds: Math.floor((difference % (1000 * 60)) / 1000),
- total: difference,
- };
-}
-
-function formatCountdown(time: TimeRemaining): string {
- if (time.total <= 0) return 'Ended';
- if (time.days > 0)
- return `${time.days} day${time.days !== 1 ? 's' : ''} left`;
- if (time.hours > 0)
- return `${time.hours} hour${time.hours !== 1 ? 's' : ''} left`;
- if (time.minutes > 0)
- return `${time.minutes} minute${time.minutes !== 1 ? 's' : ''} left`;
- return `${time.seconds} second${time.seconds !== 1 ? 's' : ''} left`;
-}
-
-const CategoriesDisplay = ({
- categoriesList,
-}: {
- categoriesList: string[];
-}) => {
- const ref = useRef
(null);
- const [showEllipsis, setShowEllipsis] = useState(false);
-
- useEffect(() => {
- const el = ref.current;
- if (!el) return;
- const check = () => setShowEllipsis(el.scrollWidth > el.clientWidth);
- check();
- window.addEventListener('resize', check);
- return () => window.removeEventListener('resize', check);
- }, [categoriesList]);
-
- return (
-
-
- {categoriesList.map((cat, i) => (
-
- {cat}
-
- ))}
-
-
- {showEllipsis && (
-
- ...
-
- )}
-
- );
-};
-
export function HackathonBanner({
title,
tagline,
- imageUrl,
deadline,
startDate,
categories,
@@ -134,76 +62,41 @@ export function HackathonBanner({
onFindTeamClick,
onLeaveClick,
}: HackathonBannerProps) {
- const [timeRemaining, setTimeRemaining] = useState({
- days: 0,
- hours: 0,
- minutes: 0,
- seconds: 0,
- total: 0,
- });
-
+ const { status, timeRemaining, formatCountdown } = useHackathonStatus(
+ startDate,
+ deadline
+ );
const { isAuthenticated } = useAuthStatus();
const router = useRouter();
const pathname = usePathname();
- const hackathonStatus = useRef<'upcoming' | 'ongoing' | 'ended'>('upcoming');
-
- // Determine status based on dates
- useEffect(() => {
- const now = new Date().getTime();
- const start = startDate ? new Date(startDate).getTime() : null;
- const submissionDeadline = deadline ? new Date(deadline).getTime() : null;
-
- if (submissionDeadline && now > submissionDeadline) {
- hackathonStatus.current = 'ended';
- } else if (start && now >= start) {
- hackathonStatus.current = 'ongoing';
- } else {
- hackathonStatus.current = 'upcoming';
- }
- }, [startDate, deadline]);
-
- // Check if registration is allowed based on registrationDeadlinePolicy
+ // Determine if registration is allowed
const canRegister = useMemo(() => {
const now = new Date();
-
- // If no policy is specified, default to 'before_submission_deadline'
const policy = registrationDeadlinePolicy || 'before_submission_deadline';
switch (policy) {
case 'before_start':
- // Can register only before hackathon starts
if (startDate) {
- const startDateObj = new Date(startDate);
- const canReg = now < startDateObj;
- return canReg;
+ return now < new Date(startDate);
}
return false;
-
case 'before_submission_deadline':
- // Can register before submission deadline
if (deadline) {
- const deadlineObj = new Date(deadline);
- const canReg = now < deadlineObj;
- return canReg;
+ return now < new Date(deadline);
}
return false;
-
case 'custom':
- // Can register before custom registration deadline
if (registrationDeadline) {
- const registrationDeadlineObj = new Date(registrationDeadline);
- const canReg = now < registrationDeadlineObj;
- return canReg;
+ return now < new Date(registrationDeadline);
}
return false;
-
default:
return false;
}
}, [registrationDeadlinePolicy, startDate, deadline, registrationDeadline]);
- // Determine button text based on registration policy and hackathon status
+ // Get appropriate register button text
const getRegisterButtonText = useMemo(() => {
if (!canRegister) return null;
@@ -213,19 +106,12 @@ export function HackathonBanner({
switch (registrationDeadlinePolicy || 'before_submission_deadline') {
case 'before_start':
return 'Register Before Start';
-
case 'before_submission_deadline':
- if (isBeforeStart) {
- return 'Early Register';
- } else {
- return 'Join Hackathon';
- }
-
+ return isBeforeStart ? 'Early Register' : 'Join Hackathon';
case 'custom':
if (registrationDeadline) {
const customDeadline = new Date(registrationDeadline);
const isBeforeCustomDeadline = now < customDeadline;
-
if (isBeforeStart && isBeforeCustomDeadline) {
return 'Register Interest';
} else if (!isBeforeStart && isBeforeCustomDeadline) {
@@ -233,7 +119,6 @@ export function HackathonBanner({
}
}
return 'Register Now';
-
default:
return 'Join Hackathon';
}
@@ -244,28 +129,31 @@ export function HackathonBanner({
registrationDeadline,
]);
+ // Redirect to auth screen
const handleRedirectToAuthScreen = () => {
const callbackUrl = encodeURIComponent(pathname);
router.push(`/auth?mode=signin&callbackUrl=${callbackUrl}`);
};
+ // Get status color based on hackathon status
const getStatusColor = () => {
- switch (hackathonStatus.current) {
+ switch (status) {
case 'ongoing':
- return 'text-green-400 bg-green-400/10';
+ return 'bg-green-500';
case 'upcoming':
- return 'text-blue-400 bg-blue-400/10';
+ return 'bg-blue-500';
case 'ended':
- return 'text-gray-400 bg-gray-800/20';
+ return 'bg-gray-500';
default:
- return '';
+ return 'bg-blue-500';
}
};
+ // Get status text
const getStatusText = () => {
- switch (hackathonStatus.current) {
+ switch (status) {
case 'ongoing':
- return 'Live';
+ return 'Live Now';
case 'upcoming':
return 'Upcoming';
case 'ended':
@@ -275,134 +163,55 @@ export function HackathonBanner({
}
};
- const getCountdownInfo = () => {
- if (timeRemaining.total <= 0)
- return { text: 'Ended', className: 'text-gray-500' };
-
- if (timeRemaining.days <= 3)
- return {
- text: formatCountdown(timeRemaining),
- className: 'text-red-400',
- };
-
- if (timeRemaining.days <= 15)
- return {
- text: formatCountdown(timeRemaining),
- className: 'text-yellow-400',
- };
-
- return {
- text: formatCountdown(timeRemaining),
- className: 'text-green-400',
- };
- };
-
- useEffect(() => {
- let targetDate: string | null = null;
-
- if (hackathonStatus.current === 'ongoing' && deadline) {
- targetDate = deadline;
- } else if (hackathonStatus.current === 'upcoming' && startDate) {
- targetDate = startDate;
- }
-
- if (!targetDate) return;
-
- setTimeRemaining(calculateTimeRemaining(targetDate));
-
- const interval = setInterval(() => {
- setTimeRemaining(calculateTimeRemaining(targetDate!));
- }, 1000);
-
- return () => clearInterval(interval);
- }, [deadline, startDate]);
-
- const renderDateSection = () => {
- const countdownInfo = getCountdownInfo();
-
- if (hackathonStatus.current === 'ongoing' && deadline) {
- return (
-
- Deadline:
-
- {formatDate(new Date(deadline))}
-
- {timeRemaining.total > 0 && (
-
- ({countdownInfo.text})
-
- )}
-
- );
- }
-
- if (hackathonStatus.current === 'upcoming' && startDate) {
- return (
-
- Starts:
-
- {formatDate(new Date(startDate))}
-
- {timeRemaining.total > 0 && (
-
- ({countdownInfo.text})
-
- )}
-
- );
- }
-
- if (hackathonStatus.current === 'ended' && deadline) {
- return (
-
- Ended:
-
- {formatDate(new Date(deadline))}
-
-
- );
- }
-
- return null;
- };
-
- // Render registration button based on policy and status
+ // Format date with fallback
+ // const formatDateWithFallback = (dateString?: string) => {
+ // if (!dateString) return "N/A"
+ // try {
+ // return formatDate(new Date(dateString))
+ // } catch {
+ // return new Date(dateString).toLocaleDateString("en-US", {
+ // month: "short",
+ // day: "numeric",
+ // year: "numeric",
+ // })
+ // }
+ // }
+
+ // Render registration button based on various conditions
const renderRegistrationButton = () => {
- // If hackathon has ended, don't show registration button
- if (hackathonStatus.current === 'ended') {
+ // Hackathon has ended
+ if (status === 'ended') {
return (
);
}
- // If user is already registered, show leave button
+ // User is already registered
if (isRegistered) {
return (
);
}
- // If registration is not allowed, show disabled button
+ // Registration is closed
if (!canRegister) {
return (