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 ( - )} + {/* Prize Pool */} + {totalPrizePool && ( +
+
+ + + Total Prize Pool + +
+
+ ${totalPrizePool} +
+
+ )} +
- {/* View Submission */} - {hackathonStatus.current === 'ongoing' && - isRegistered && - hasSubmitted && - onViewSubmissionClick && ( - - )} + {/* Action Buttons - Mobile Only */} +
+ {renderRegistrationButton()} + + {status === 'ongoing' && + isRegistered && + !hasSubmitted && + onSubmitClick && ( + + )} - {/* Find Team */} - {hackathonStatus.current === 'ongoing' && - isRegistered && - isTeamFormationEnabled && - onFindTeamClick && ( - - )} -
+ {status === 'ongoing' && + isRegistered && + hasSubmitted && + onViewSubmissionClick && ( + + )} + + {status === 'ongoing' && + isRegistered && + isTeamFormationEnabled && + onFindTeamClick && ( + + )}
- +
); } diff --git a/components/hackathons/hackathonStickyCard.tsx b/components/hackathons/hackathonStickyCard.tsx new file mode 100644 index 000000000..964962d44 --- /dev/null +++ b/components/hackathons/hackathonStickyCard.tsx @@ -0,0 +1,295 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import { Card } from '@/components/ui/card'; +import Image from 'next/image'; +import { useAuthStatus } from '@/hooks/use-auth'; +import { useRouter, usePathname } from 'next/navigation'; +import { useMemo } from 'react'; +import { + Calendar, + ArrowRight, + FileText, + Users, + Trophy, + Plus, +} from 'lucide-react'; +import { useHackathonStatus } from '@/hooks/hackathon/use-hackathon-status'; +import { formatDate } from '@/lib/utils'; + +interface HackathonStickyCardProps { + title: string; + imageUrl?: string; + deadline?: string; + startDate?: string; + totalPrizePool?: string; + isRegistered?: boolean; + hasSubmitted?: boolean; + isTeamFormationEnabled?: boolean; + registrationDeadlinePolicy?: + | 'before_start' + | 'before_submission_deadline' + | 'custom'; + registrationDeadline?: string; + isLeaving?: boolean; + onJoinClick?: () => void; + onSubmitClick?: () => void; + onViewSubmissionClick?: () => void; + onFindTeamClick?: () => void; + onLeaveClick?: () => void; +} + +export function HackathonStickyCard(props: HackathonStickyCardProps) { + const { + title, + imageUrl, + deadline, + startDate, + totalPrizePool, + isRegistered = false, + hasSubmitted = false, + isTeamFormationEnabled = false, + registrationDeadlinePolicy, + registrationDeadline, + onJoinClick, + onSubmitClick, + onViewSubmissionClick, + onFindTeamClick, + onLeaveClick, + isLeaving, + } = props; + + const { status } = useHackathonStatus(startDate, deadline); + const { isAuthenticated } = useAuthStatus(); + const router = useRouter(); + const pathname = usePathname(); + + // Determine if registration is allowed + const canRegister = useMemo(() => { + const now = new Date(); + const policy = registrationDeadlinePolicy || 'before_submission_deadline'; + + switch (policy) { + case 'before_start': + return startDate ? now < new Date(startDate) : false; + case 'before_submission_deadline': + return deadline ? now < new Date(deadline) : false; + case 'custom': + return registrationDeadline + ? now < new Date(registrationDeadline) + : false; + default: + return false; + } + }, [registrationDeadlinePolicy, startDate, deadline, registrationDeadline]); + + // Get appropriate register button text + const getRegisterButtonText = useMemo(() => { + if (!canRegister) return null; + const now = new Date(); + const beforeStart = startDate && now < new Date(startDate); + + switch (registrationDeadlinePolicy || 'before_submission_deadline') { + case 'before_start': + return 'Register'; + case 'before_submission_deadline': + return beforeStart ? 'Register' : 'Join'; + case 'custom': + if (registrationDeadline) { + const custom = new Date(registrationDeadline); + const beforeCustom = now < custom; + if (beforeStart && beforeCustom) return 'Register'; + if (!beforeStart && beforeCustom) return 'Register'; + } + return 'Register'; + default: + return 'Join'; + } + }, [ + registrationDeadlinePolicy, + canRegister, + startDate, + registrationDeadline, + ]); + + // Redirect to auth if user not authenticated + const redirectToAuth = () => { + const callbackUrl = encodeURIComponent(pathname); + router.push(`/auth?mode=signin&callbackUrl=${callbackUrl}`); + }; + + // 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', + }); + } + }; + + return ( +
+ + {/* Image Section - UNCHANGED */} + {imageUrl && ( +
+ {title} +
+ )} + + {/* Wave Background */} +
+ +
+ +
+ {/* Prize Pool Section */} + {totalPrizePool && ( +
+
+ + Prize Pool +
+
+ ${totalPrizePool} +
+
+ )} + + {/* Countdown Timer */} + {/* {timeRemaining.total > 0 && ( +
+
+ {status === "ongoing" ? "Ends In" : "Starts In"} +
+
+ {formatCountdown(timeRemaining)} +
+
+ )} */} + + {/* Date Info Section */} +
+ {status === 'ongoing' && deadline && ( +
+ Deadline + + {formatDateWithFallback(deadline)} + +
+ )} + + {status === 'upcoming' && startDate && ( +
+ Starts + + {formatDateWithFallback(startDate)} + +
+ )} + + {status === 'ended' && deadline && ( +
+ Ended + + {formatDateWithFallback(deadline)} + +
+ )} +
+ + {/* Action Buttons */} +
+ {/* Register / Leave Button */} + {!isRegistered ? ( + canRegister ? ( + + ) : ( + + ) + ) : ( + + )} + + {/* Submit Project Button */} + {status === 'ongoing' && isRegistered && !hasSubmitted && ( + + )} + + {/* View Submission Button */} + {status === 'ongoing' && + isRegistered && + hasSubmitted && + onViewSubmissionClick && ( + + )} + + {/* Find Team Button */} + {status === 'ongoing' && + isRegistered && + isTeamFormationEnabled && + onFindTeamClick && ( + + )} +
+
+
+
+ ); +} diff --git a/components/hackathons/overview/hackathonOverview.tsx b/components/hackathons/overview/hackathonOverview.tsx index df8bb3220..c804f0c5a 100644 --- a/components/hackathons/overview/hackathonOverview.tsx +++ b/components/hackathons/overview/hackathonOverview.tsx @@ -108,7 +108,7 @@ export function HackathonOverview({ }; return ( -
+
{loading &&
Loading content...
} {error && ( diff --git a/components/landing-page/Explore.tsx b/components/landing-page/Explore.tsx index 291f4c0cb..7aa933339 100644 --- a/components/landing-page/Explore.tsx +++ b/components/landing-page/Explore.tsx @@ -253,7 +253,7 @@ export default function Explore() {

Active Opportunities

-

Explore What’s Happening

+

Explore What's Happening

{tabs.map(tab => ( @@ -281,7 +281,7 @@ export default function Explore() {
-
+
{activeTab === 'featured-projects' && ( <> {projectsLoading ? ( diff --git a/hooks/hackathon/use-hackathon-status.ts b/hooks/hackathon/use-hackathon-status.ts new file mode 100644 index 000000000..aee9e136f --- /dev/null +++ b/hooks/hackathon/use-hackathon-status.ts @@ -0,0 +1,81 @@ +'use client'; + +import { useEffect, useState } from 'react'; + +export function useHackathonStatus(startDate?: string, deadline?: string) { + const [status, setStatus] = useState<'upcoming' | 'ongoing' | 'ended'>( + 'upcoming' + ); + const [timeRemaining, setTimeRemaining] = useState({ + days: 0, + hours: 0, + minutes: 0, + seconds: 0, + total: 0, + }); + + useEffect(() => { + const now = Date.now(); + const start = startDate ? new Date(startDate).getTime() : null; + const end = deadline ? new Date(deadline).getTime() : null; + + let newStatus: 'upcoming' | 'ongoing' | 'ended' = 'upcoming'; + + if (end && now > end) { + newStatus = 'ended'; + } else if (start && now >= start) { + newStatus = 'ongoing'; + } else { + newStatus = 'upcoming'; + } + + setStatus(newStatus); + }, [startDate, deadline]); + + useEffect(() => { + const target = + status === 'ongoing' + ? deadline + : status === 'upcoming' + ? startDate + : null; + + if (!target) return; + + const getTime = () => { + const now = Date.now(); + const targetTime = new Date(target).getTime(); + const diff = targetTime - now; + + if (diff <= 0) { + return { days: 0, hours: 0, minutes: 0, seconds: 0, total: 0 }; + } + + return { + days: Math.floor(diff / 86400000), + hours: Math.floor((diff % 86400000) / 3600000), + minutes: Math.floor((diff % 3600000) / 60000), + seconds: Math.floor((diff % 60000) / 1000), + total: diff, + }; + }; + + setTimeRemaining(getTime()); + + const interval = setInterval(() => setTimeRemaining(getTime()), 1000); + return () => clearInterval(interval); + }, [deadline, startDate, status]); + + const formatCountdown = (t: typeof timeRemaining) => { + if (t.total <= 0) return 'Ended'; + if (t.days > 0) return `${t.days}d ${t.hours}h`; + if (t.hours > 0) return `${t.hours}h ${t.minutes}m`; + return `${t.minutes}m ${t.seconds}s`; + }; + + return { + status, + timeRemaining, + formatCountdown, + }; +}