...
@@ -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[];