diff --git a/app/(landing)/hackathons/[slug]/page.tsx b/app/(landing)/hackathons/[slug]/page.tsx index 60c8b8872..c16328f9e 100644 --- a/app/(landing)/hackathons/[slug]/page.tsx +++ b/app/(landing)/hackathons/[slug]/page.tsx @@ -57,6 +57,10 @@ export default function HackathonPage() { }, // { id: 'discussions', label: 'Discussions' }, ]; + console.log( + currentHackathon?.registrationDeadlinePolicy, + currentHackathon?.registrationDeadline + ); const participantType = currentHackathon?.participation?.participantType; const isTeamHackathon = @@ -183,6 +187,8 @@ export default function HackathonPage() { isRegistered={isRegistered} hasSubmitted={hasSubmitted} isEnded={isEnded} + registrationDeadlinePolicy={currentHackathon.registrationDeadlinePolicy} + registrationDeadline={currentHackathon.registrationDeadline} isTeamFormationEnabled={isTeamFormationEnabled} onJoinClick={handleJoinClick} onSubmitClick={handleSubmitClick} diff --git a/app/(landing)/hackathons/preview/[orgId]/[draftId]/page.tsx b/app/(landing)/hackathons/preview/[orgId]/[draftId]/page.tsx index 9e14c6a50..3b3f185af 100644 --- a/app/(landing)/hackathons/preview/[orgId]/[draftId]/page.tsx +++ b/app/(landing)/hackathons/preview/[orgId]/[draftId]/page.tsx @@ -117,6 +117,11 @@ export default function DraftPreviewPage({ params }: PreviewPageProps) { ? 'ended' : 'upcoming', participants: 0, // Always 0 for drafts + registrationDeadlinePolicy: + transformed.participation.registrationDeadlinePolicy || + 'before_submission_deadline', + registrationDeadline: + transformed.participation.registrationDeadline, totalPrizePool: response.data.totalPrizePool, deadline: transformed.timeline.submissionDeadline, categories: transformed.information.categories.map(cat => diff --git a/components/hackathons/hackathonBanner.tsx b/components/hackathons/hackathonBanner.tsx index 2ae7873ae..5dfc44744 100644 --- a/components/hackathons/hackathonBanner.tsx +++ b/components/hackathons/hackathonBanner.tsx @@ -4,11 +4,10 @@ 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 } from 'react'; -import { FileText, Users, ArrowRight } from 'lucide-react'; +import { useEffect, useState, useRef, useMemo } from 'react'; +import { FileText, Users, ArrowRight, Calendar } from 'lucide-react'; import { useAuthStatus } from '@/hooks/use-auth'; import { useRouter } from 'next/navigation'; -// import { sanitizeHtml } from '@/lib/utils/renderHtml'; interface HackathonBannerProps { title: string; @@ -21,11 +20,15 @@ interface HackathonBannerProps { status?: string; participants?: number; totalPrizePool?: string; - // Action buttons isRegistered?: boolean; hasSubmitted?: boolean; isEnded?: boolean; isTeamFormationEnabled?: boolean; + registrationDeadlinePolicy?: + | 'before_start' + | 'before_submission_deadline' + | 'custom'; + registrationDeadline?: string; onJoinClick?: () => void; onSubmitClick?: () => void; onViewSubmissionClick?: () => void; @@ -60,16 +63,13 @@ function calculateTimeRemaining(targetDate: string): TimeRemaining { function formatCountdown(time: TimeRemaining): string { if (time.total <= 0) return 'Ended'; - - if (time.days > 0) { + if (time.days > 0) return `${time.days} day${time.days !== 1 ? 's' : ''} left`; - } else if (time.hours > 0) { + if (time.hours > 0) return `${time.hours} hour${time.hours !== 1 ? 's' : ''} left`; - } else if (time.minutes > 0) { + if (time.minutes > 0) return `${time.minutes} minute${time.minutes !== 1 ? 's' : ''} left`; - } else { - return `${time.seconds} second${time.seconds !== 1 ? 's' : ''} left`; - } + return `${time.seconds} second${time.seconds !== 1 ? 's' : ''} left`; } const CategoriesDisplay = ({ @@ -83,17 +83,14 @@ const CategoriesDisplay = ({ 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) => ( ))}
+ {showEllipsis && (
... @@ -119,15 +117,14 @@ export function HackathonBanner({ imageUrl, deadline, startDate, - endDate, categories, - status, participants, totalPrizePool, isRegistered = false, hasSubmitted = false, - isEnded = false, isTeamFormationEnabled = false, + registrationDeadlinePolicy, + registrationDeadline, onJoinClick, onSubmitClick, onViewSubmissionClick, @@ -144,43 +141,194 @@ export function HackathonBanner({ const { isAuthenticated } = useAuthStatus(); const router = useRouter(); + 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 + const canRegister = useMemo(() => { + const now = new Date(); + + // If no policy is specified, default to 'before_submission_deadline' + const policy = registrationDeadlinePolicy || 'before_submission_deadline'; + + console.log('🔍 Registration Check:', { + policy, + now: now.toISOString(), + startDate, + deadline, + registrationDeadline, + }); + + switch (policy) { + case 'before_start': + // Can register only before hackathon starts + if (startDate) { + const startDateObj = new Date(startDate); + const canReg = now < startDateObj; + console.log('before_start check:', { + canReg, + startDateObj: startDateObj.toISOString(), + }); + return canReg; + } + return false; + + case 'before_submission_deadline': + // Can register before submission deadline + if (deadline) { + const deadlineObj = new Date(deadline); + const canReg = now < deadlineObj; + console.log('before_submission_deadline check:', { + canReg, + deadlineObj: deadlineObj.toISOString(), + }); + return canReg; + } + return false; + + case 'custom': + // Can register before custom registration deadline + if (registrationDeadline) { + const registrationDeadlineObj = new Date(registrationDeadline); + const canReg = now < registrationDeadlineObj; + console.log('custom check:', { + canReg, + registrationDeadlineObj: registrationDeadlineObj.toISOString(), + }); + return canReg; + } + return false; + + default: + return false; + } + }, [registrationDeadlinePolicy, startDate, deadline, registrationDeadline]); + + // Determine button text based on registration policy and hackathon status + const getRegisterButtonText = useMemo(() => { + if (!canRegister) return null; + + const now = new Date(); + const isBeforeStart = startDate && now < new Date(startDate); + + 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'; + } + + case 'custom': + if (registrationDeadline) { + const customDeadline = new Date(registrationDeadline); + const isBeforeCustomDeadline = now < customDeadline; + + if (isBeforeStart && isBeforeCustomDeadline) { + return 'Register Interest'; + } else if (!isBeforeStart && isBeforeCustomDeadline) { + return 'Late Register'; + } + } + return 'Register Now'; + + default: + return 'Join Hackathon'; + } + }, [ + registrationDeadlinePolicy, + canRegister, + startDate, + registrationDeadline, + ]); + + // Debug useEffect to track registration logic + useEffect(() => { + console.log('🔍 Debug Registration Logic:', { + hackathonStatus: hackathonStatus.current, + registrationDeadlinePolicy: + registrationDeadlinePolicy || 'before_submission_deadline (default)', + startDate, + deadline, + registrationDeadline, + canRegister, + isBeforeStart: startDate && new Date() < new Date(startDate), + buttonText: getRegisterButtonText, + isRegistered, + }); + }, [ + registrationDeadlinePolicy, + startDate, + deadline, + registrationDeadline, + canRegister, + getRegisterButtonText, + isRegistered, + ]); + + const handleRedirectToAuthScreen = () => { + router.push('/auth?mode=signin'); + }; + const getStatusColor = () => { - switch (status) { - case 'Ongoing': + switch (hackathonStatus.current) { case 'ongoing': - return 'text-blue-400 bg-blue-400/10'; - case 'Published': + return 'text-green-400 bg-green-400/10'; case 'upcoming': - return 'text-primary bg-primary/10'; - case 'Completed': + return 'text-blue-400 bg-blue-400/10'; case 'ended': - return 'text-green-400 bg-green-400/10'; - case 'Cancelled': - return 'text-gray-500 bg-gray-700/20'; - default: return 'text-gray-400 bg-gray-800/20'; + default: + return ''; } }; - const handleRedirectToAuthScreen = () => { - router.push('/auth?mode=signin'); + const getStatusText = () => { + switch (hackathonStatus.current) { + case 'ongoing': + return 'Live'; + case 'upcoming': + return 'Upcoming'; + case 'ended': + return 'Ended'; + default: + return 'Upcoming'; + } }; - const getDeadlineInfo = () => { - const days = timeRemaining.days; - + const getCountdownInfo = () => { if (timeRemaining.total <= 0) return { text: 'Ended', className: 'text-gray-500' }; - if (days <= 3) + + if (timeRemaining.days <= 3) return { text: formatCountdown(timeRemaining), className: 'text-red-400', }; - if (days <= 15) + + if (timeRemaining.days <= 15) return { text: formatCountdown(timeRemaining), className: 'text-yellow-400', }; + return { text: formatCountdown(timeRemaining), className: 'text-green-400', @@ -189,86 +337,127 @@ export function HackathonBanner({ useEffect(() => { let targetDate: string | null = null; - const statusLower = status?.toLowerCase(); - if (statusLower === 'ongoing' && deadline) { + + if (hackathonStatus.current === 'ongoing' && deadline) { targetDate = deadline; - } else if (statusLower === 'upcoming' && startDate) { + } else if (hackathonStatus.current === 'upcoming' && startDate) { targetDate = startDate; - } else if (statusLower === 'ended' && endDate) { - targetDate = endDate; - } else if (deadline) { - targetDate = deadline; } if (!targetDate) return; setTimeRemaining(calculateTimeRemaining(targetDate)); - // Update every second const interval = setInterval(() => { setTimeRemaining(calculateTimeRemaining(targetDate!)); }, 1000); return () => clearInterval(interval); - }, [status, deadline, startDate, endDate]); + }, [deadline, startDate]); const renderDateSection = () => { - const statusLower = status?.toLowerCase(); - const deadlineInfo = getDeadlineInfo(); + const countdownInfo = getCountdownInfo(); - if (statusLower === 'ongoing' && deadline) { + if (hackathonStatus.current === 'ongoing' && deadline) { return (
- Deadline: - - {deadlineInfo.text} + Deadline: + + {formatDate(new Date(deadline))} + {timeRemaining.total > 0 && ( + + ({countdownInfo.text}) + + )}
); - } else if (statusLower === 'upcoming' && startDate) { + } + + if (hackathonStatus.current === 'upcoming' && startDate) { return (
- Start: + Starts: {formatDate(new Date(startDate))} {timeRemaining.total > 0 && ( - - ({formatCountdown(timeRemaining)}) + + ({countdownInfo.text}) )}
); - } else if (statusLower === 'ended' && endDate) { + } + + if (hackathonStatus.current === 'ended' && deadline) { return (
Ended: - {formatDate(new Date(endDate))} + {formatDate(new Date(deadline))}
); } - if (deadline) { + return null; + }; + + // Render registration button based on policy and status + const renderRegistrationButton = () => { + // If hackathon has ended, don't show registration button + if (hackathonStatus.current === 'ended') { return ( -
- - Deadline: - - - {formatDate(new Date(deadline))} - - {timeRemaining.total > 0 && ( - - ({deadlineInfo.text}) - - )} -
+ ); } - return null; + // If user is already registered, show leave button + if (isRegistered) { + return ( + + ); + } + + // If registration is not allowed, show disabled button + if (!canRegister) { + return ( + + ); + } + + // Show active registration button with appropriate text + const buttonText = getRegisterButtonText || 'Join Hackathon'; + + return ( + + ); }; return ( @@ -276,28 +465,28 @@ export function HackathonBanner({
- {/* Text content */}
+ {/* Top section */}
- {status && ( - - {status} - - )} + + {getStatusText()} + + {participants !== undefined && ( {participants.toLocaleString()} participants )}
+ {totalPrizePool && (
@@ -310,18 +499,16 @@ export function HackathonBanner({ )}
+ {/* Title & tagline */}

{title}

- {tagline && typeof tagline === 'string' ? ( + + {tagline && ( {tagline} - ) : ( -
- {tagline} -
)} {tagline && ( @@ -329,9 +516,10 @@ export function HackathonBanner({ )}
- {/* Bottom section: Deadline/Start/Ended, categories, and action buttons */} + {/* Bottom section */}
{renderDateSection()} + {categories && categories.length > 0 && (
@@ -341,57 +529,28 @@ export function HackathonBanner({
)} - {/* Action Buttons */} {/* Action Buttons */}
- {/* Hackathon Ended → Disabled button */} - {isEnded && ( - - )} + {/* Registration Button */} + {renderRegistrationButton()} - {/* Hackathon Ongoing → Not registered → Join */} - {!isEnded && !isRegistered && onJoinClick && ( - - )} - - {/* Hackathon Ongoing → Registered → Leave */} - {!isEnded && isRegistered && ( - - )} - - {/* Submit Project */} - {!isEnded && isRegistered && !hasSubmitted && onSubmitClick && ( - - )} + {/* Submit */} + {hackathonStatus.current === 'ongoing' && + isRegistered && + !hasSubmitted && + onSubmitClick && ( + + )} {/* View Submission */} - {!isEnded && + {hackathonStatus.current === 'ongoing' && isRegistered && hasSubmitted && onViewSubmissionClick && ( @@ -405,7 +564,7 @@ export function HackathonBanner({ )} {/* Find Team */} - {!isEnded && + {hackathonStatus.current === 'ongoing' && isRegistered && isTeamFormationEnabled && onFindTeamClick && ( diff --git a/components/organization/OrganizationAnalytics.tsx b/components/organization/OrganizationAnalytics.tsx index 4c7444466..88ae30a06 100644 --- a/components/organization/OrganizationAnalytics.tsx +++ b/components/organization/OrganizationAnalytics.tsx @@ -123,7 +123,7 @@ const OrganizationAnalytics = () => { return (
-
+
{/* Header */}

Analytics

diff --git a/components/organization/hackathons/new/tabs/ParticipantTab.tsx b/components/organization/hackathons/new/tabs/ParticipantTab.tsx index a4f6707a2..a24008578 100644 --- a/components/organization/hackathons/new/tabs/ParticipantTab.tsx +++ b/components/organization/hackathons/new/tabs/ParticipantTab.tsx @@ -18,12 +18,21 @@ import { UserCheck, UsersRound, Loader2, + CalendarIcon, } from 'lucide-react'; import { Switch } from '@/components/ui/switch'; import { participantSchema, ParticipantFormData, } from './schemas/participantSchema'; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover'; +import { Button } from '@/components/ui/button'; +import { Calendar } from '@/components/ui/calendar'; +import { format } from 'date-fns'; interface ParticipantTabProps { onContinue?: () => void; @@ -60,6 +69,25 @@ const submissionRequirements = [ }, ]; +// Add this array after the submissionRequirements array +const registrationPolicies = [ + { + value: 'before_start' as const, + label: 'Before Hackathon Starts', + description: 'Registration closes when the hackathon begins', + }, + { + value: 'before_submission_deadline' as const, + label: 'Before Submission Deadline', + description: 'Registration stays open until submission deadline', + }, + { + value: 'custom' as const, + label: 'Custom Deadline', + description: 'Set your own registration deadline', + }, +]; + const tabVisibility = [ { name: 'detailsTab' as const, label: 'Details' }, { name: 'participantsTab' as const, label: 'Participants' }, @@ -84,6 +112,8 @@ export default function ParticipantTab({ participantType: 'individual', teamMin: 2, teamMax: 5, + registrationDeadlinePolicy: 'before_submission_deadline', + registrationDeadline: undefined, require_github: true, require_demo_video: true, require_other_links: true, @@ -210,6 +240,122 @@ export default function ParticipantTab({ )} /> + {/* Registration Deadline Policy */} +
+
+

+ Registration Deadline * +

+

+ Choose when registration should close +

+
+ + ( + + +
+ {registrationPolicies.map( + ({ value, label, description }) => { + const isSelected = field.value === value; + return ( + + ); + } + )} +
+
+ +
+ )} + /> + + {/* Custom Deadline Calendar */} + {form.watch('registrationDeadlinePolicy') === 'custom' && ( + ( + + + + + + + + { + if (date) { + field.onChange(date.toISOString()); + } + }} + disabled={date => date < new Date()} + initialFocus + /> + + + + + + )} + /> + )} +
+ {/* Team Size Settings */} {participantType === 'team' && (
diff --git a/components/organization/hackathons/new/tabs/schemas/participantSchema.ts b/components/organization/hackathons/new/tabs/schemas/participantSchema.ts index 54e18f2d9..7fe56741b 100644 --- a/components/organization/hackathons/new/tabs/schemas/participantSchema.ts +++ b/components/organization/hackathons/new/tabs/schemas/participantSchema.ts @@ -7,6 +7,10 @@ export const participantSchema = z .default('individual'), teamMin: z.number().min(1).max(20).optional(), teamMax: z.number().min(1).max(20).optional(), + registrationDeadlinePolicy: z + .enum(['before_start', 'before_submission_deadline', 'custom']) + .default('before_submission_deadline'), + registrationDeadline: z.string().optional(), require_github: z.boolean().optional(), require_demo_video: z.boolean().optional(), require_other_links: z.boolean().optional(), @@ -22,6 +26,7 @@ export const participantSchema = z rulesTab: z.boolean().optional(), }) .superRefine((data, ctx) => { + // Team size validation if ( data.participantType === 'team' || data.participantType === 'team_or_individual' @@ -48,6 +53,17 @@ export const participantSchema = z }); } } + + // Custom registration deadline validation + if (data.registrationDeadlinePolicy === 'custom') { + if (!data.registrationDeadline) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Registration deadline is required for custom policy', + path: ['registrationDeadline'], + }); + } + } }); export type ParticipantFormData = z.input; diff --git a/lib/api/hackathons.ts b/lib/api/hackathons.ts index 76f134f87..950090136 100644 --- a/lib/api/hackathons.ts +++ b/lib/api/hackathons.ts @@ -2,6 +2,11 @@ import api, { type RequestConfig } from './api'; import { ApiResponse, ErrorResponse, PaginatedResponse } from './types'; import { Discussion } from '@/types/hackathon'; +export type RegistrationDeadlinePolicy = + | 'custom' + | 'before_start' + | 'before_submission_deadline'; + // Enums matching backend models export enum HackathonCategory { DEFI = 'DeFi', @@ -91,6 +96,8 @@ export interface HackathonParticipation { teamMin?: number; teamMax?: number; about?: string; + registrationDeadlinePolicy?: RegistrationDeadlinePolicy; + registrationDeadline?: string; submissionRequirements?: SubmissionRequirements; tabVisibility?: TabVisibility; } diff --git a/lib/utils/hackathon-form-transforms.ts b/lib/utils/hackathon-form-transforms.ts index 9575b002f..a9c5281d6 100644 --- a/lib/utils/hackathon-form-transforms.ts +++ b/lib/utils/hackathon-form-transforms.ts @@ -76,6 +76,8 @@ export const transformToApiFormat = (stepData: { ParticipantType.INDIVIDUAL, teamMin: participation?.teamMin, teamMax: participation?.teamMax, + registrationDeadlinePolicy: participation?.registrationDeadlinePolicy, + registrationDeadline: participation?.registrationDeadline, submissionRequirements: { requireGithub: participation?.require_github, requireDemoVideo: participation?.require_demo_video, @@ -181,6 +183,10 @@ export const transformFromApiFormat = (draft: HackathonDraft) => { participantType: participation?.participantType || 'individual', teamMin: participation?.teamMin, teamMax: participation?.teamMax, + registrationDeadlinePolicy: + participation?.registrationDeadlinePolicy || + 'before_submission_deadline', + registrationDeadline: participation?.registrationDeadline, require_github: participation?.submissionRequirements?.requireGithub || false, require_demo_video: diff --git a/types/hackathon.ts b/types/hackathon.ts index b3b2b3a9b..3e756c935 100644 --- a/types/hackathon.ts +++ b/types/hackathon.ts @@ -1,6 +1,7 @@ import { HackathonPhase, PrizeTier, + RegistrationDeadlinePolicy, SponsorPartner, } from '@/lib/api/hackathons'; @@ -128,6 +129,8 @@ export interface HackathonParticipationSettings { teamMin?: number; teamMax?: number; about?: string; + registrationDeadlinePolicy?: RegistrationDeadlinePolicy; + registrationDeadline?: string; submissionRequirements?: HackathonSubmissionRequirements; tabVisibility?: HackathonTabVisibility; } @@ -178,6 +181,8 @@ export interface Hackathon { prizeTiers?: PrizeTier[]; teamMin?: number; teamMax?: number; + registrationDeadlinePolicy: RegistrationDeadlinePolicy; + registrationDeadline: string | undefined; venue?: Venue; sponsors?: SponsorPartner[]; socialLinks?: string[];