Skip to content
100 changes: 87 additions & 13 deletions components/landing-page/hackathon/HackathonCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type HackathonCardProps = {
categories: string[];
location?: string;
venueType?: 'virtual' | 'physical';
participantType?: 'individual' | 'team' | 'team_or_individual';
participants?: {
current: number;
goal?: number;
Expand Down Expand Up @@ -76,26 +77,97 @@ function HackathonCard({
};

const getDeadlineInfo = () => {
if (deadlineInDays <= 0)
// Handle completed or cancelled status
if (status === 'Completed') {
return { text: 'Ended', className: 'text-gray-500' };
if (deadlineInDays <= 3)
return { text: `${deadlineInDays} days left`, className: 'text-red-400' };
if (deadlineInDays <= 15)
}
if (status === 'Cancelled') {
return { text: 'Cancelled', className: 'text-gray-500' };
}

// Handle cases where deadline has passed
if (deadlineInDays <= 0) {
if (status === 'Ongoing') {
return { text: 'Ending soon', className: 'text-red-400' };
}
return { text: 'Started', className: 'text-gray-500' };
}

// Handle different statuses with contextual messages
if (status === 'Published') {
// Upcoming hackathons - show "starting in X days"
if (deadlineInDays <= 3) {
return {
text: `starting in ${deadlineInDays} day${deadlineInDays !== 1 ? 's' : ''}`,
className: 'text-red-400',
};
}
if (deadlineInDays <= 15) {
return {
text: `starting in ${deadlineInDays} days`,
className: 'text-yellow-400',
};
}
return {
text: `starting in ${deadlineInDays} days`,
className: 'text-green-400',
};
}

if (status === 'Ongoing') {
// Ongoing hackathons - show "ending in X days"
if (deadlineInDays <= 3) {
return {
text: `ending in ${deadlineInDays} day${deadlineInDays !== 1 ? 's' : ''}`,
className: 'text-red-400',
};
}
if (deadlineInDays <= 15) {
return {
text: `ending in ${deadlineInDays} days`,
className: 'text-yellow-400',
};
}
return {
text: `ending in ${deadlineInDays} days`,
className: 'text-green-400',
};
}

// Fallback for any other status
if (deadlineInDays <= 3) {
return {
text: `${deadlineInDays} day${deadlineInDays !== 1 ? 's' : ''} left`,
className: 'text-red-400',
};
}
if (deadlineInDays <= 15) {
return {
text: `${deadlineInDays} days left`,
className: 'text-yellow-400',
};
return { text: `${deadlineInDays} days left`, className: 'text-green-400' };
}
return {
text: `${deadlineInDays} days left`,
className: 'text-green-400',
};
};

const deadlineInfo = getDeadlineInfo();
const locationText =
location ||
(venueType === 'virtual'
? 'Virtual'
: venueType === 'physical'
? 'Physical'
: 'TBD');
const locationText = (() => {
// If location is provided (from transform hook), use it
if (location) {
return location;
}
// Otherwise, fall back to venueType
if (venueType === 'virtual') {
return 'Virtual';
}
if (venueType === 'physical') {
return 'Physical';
}
return undefined;
})();

const CategoriesDisplay = ({
categoriesList,
Expand Down Expand Up @@ -211,7 +283,9 @@ function HackathonCard({
</div>

<div className='flex items-center justify-between border-t border-neutral-800 px-4 py-3 sm:px-5'>
<span className={`text-xs font-medium ${deadlineInfo.className}`}>
<span
className={`text-xs font-medium capitalize ${deadlineInfo.className}`}
>
{deadlineInfo.text}
</span>
{participants?.goal && (
Expand Down
31 changes: 26 additions & 5 deletions hooks/hackathon/use-hackathon-transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ interface TransformedHackathon {
categories: string[];
location?: string;
venueType?: 'virtual' | 'physical';
participantType?: 'individual' | 'team' | 'team_or_individual';
participants?: {
current: number;
goal?: number;
Expand Down Expand Up @@ -76,10 +77,20 @@ export function useHackathonTransform() {
const venue = hackathon.information?.venue;
let locationText: string | undefined;
if (venue) {
if (venue.type === 'physical' && venue.city && venue.country) {
locationText = `${venue.city}, ${venue.country}`;
} else if (venue.type === 'physical' && venue.country) {
locationText = venue.country;
if (venue.type === 'physical') {
// For physical venues, prioritize city + country, then country, then state, then venue name
if (venue.city && venue.country) {
locationText = `${venue.city}, ${venue.country}`;
} else if (venue.country) {
locationText = venue.country;
} else if (venue.state) {
locationText = venue.state;
} else if (venue.venueName) {
locationText = venue.venueName;
} else if (venue.venueAddress) {
locationText = venue.venueAddress;
}
// If still no location, we'll rely on venueType in the card component
} else if (venue.type === 'virtual') {
locationText = 'Virtual';
}
Expand Down Expand Up @@ -136,6 +147,9 @@ export function useHackathonTransform() {
categories.push('Other');
}

// Extract participantType
const participantType = hackathon.participation?.participantType;

return {
hackathonId: hackathon._id,
organizationName: orgName,
Expand All @@ -154,7 +168,14 @@ export function useHackathonTransform() {
deadlineInDays: Math.max(0, deadlineInDays),
categories: categories,
location: locationText,
venueType: venue?.type,
venueType: venue?.type
? venue.type === 'virtual'
? 'virtual'
: 'physical'
: undefined,
participantType: participantType
? (participantType as 'individual' | 'team' | 'team_or_individual')
: undefined,
participants: {
current: extendedHackathon.participants || 0, // Use participants from public API if available
// goal could be calculated from participation settings
Expand Down
47 changes: 34 additions & 13 deletions lib/api/hackathons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export interface HackathonInformation {
description: string;
category?: HackathonCategory; // Legacy format (single category)
categories?: HackathonCategory[]; // New format (array of categories)
venue: HackathonVenue;
venue?: HackathonVenue;
}

// Timeline Tab Types
Expand Down Expand Up @@ -84,7 +84,7 @@ export interface TabVisibility {
}

export interface HackathonParticipation {
participantType: ParticipantType;
participantType?: ParticipantType;
teamMin?: number;
teamMax?: number;
about?: string;
Expand Down Expand Up @@ -654,6 +654,15 @@ export interface PublicHackathon {
organizer: string;
featured: boolean;
resources: string[];
venue?: {
type: 'virtual' | 'physical';
country?: string;
state?: string;
city?: string;
venueName?: string;
venueAddress?: string;
};
participantType?: 'individual' | 'team' | 'team_or_individual';
}

export interface PublicHackathonsListData {
Expand Down Expand Up @@ -778,9 +787,7 @@ const transformHackathonResponse = (
? [flat.category as HackathonCategory]
: [HackathonCategory.OTHER],
category: (flat.category as HackathonCategory) || HackathonCategory.OTHER,
venue: flat.venue || {
type: VenueType.VIRTUAL,
},
venue: flat.venue,
},
timeline: {
startDate: flat.startDate || '',
Expand All @@ -791,8 +798,7 @@ const transformHackathonResponse = (
phases: flat.phases || [],
},
participation: {
participantType:
(flat.participantType as ParticipantType) || ParticipantType.INDIVIDUAL,
participantType: flat.participantType as ParticipantType | undefined,
teamMin: flat.teamMin,
teamMax: flat.teamMax,
about: flat.about,
Expand Down Expand Up @@ -1348,11 +1354,20 @@ export const transformPublicHackathonToHackathon = (
const prizePoolAmount =
parseFloat(publicHackathon.totalPrizePool.replace(/,/g, '')) || 0;

// Determine venue type from location or default to virtual
// Since API doesn't provide venue details, we'll default to virtual
const venue: HackathonVenue = {
type: VenueType.VIRTUAL,
};
// Extract venue from API response or default to virtual
const venue: HackathonVenue | undefined = publicHackathon.venue
? {
type:
publicHackathon.venue.type === 'physical'
? VenueType.PHYSICAL
: VenueType.VIRTUAL,
country: publicHackathon.venue.country,
state: publicHackathon.venue.state,
city: publicHackathon.venue.city,
venueName: publicHackathon.venue.venueName,
venueAddress: publicHackathon.venue.venueAddress,
}
: undefined;

// Map API status to internal status
// API uses: upcoming, ongoing, ended
Expand Down Expand Up @@ -1406,7 +1421,13 @@ export const transformPublicHackathonToHackathon = (
phases: [],
},
participation: {
participantType: ParticipantType.TEAM_OR_INDIVIDUAL,
participantType: publicHackathon.participantType
? publicHackathon.participantType === 'individual'
? ParticipantType.INDIVIDUAL
: publicHackathon.participantType === 'team'
? ParticipantType.TEAM
: ParticipantType.TEAM_OR_INDIVIDUAL
: undefined,
teamMin: undefined,
teamMax: undefined,
about: publicHackathon.subtitle,
Expand Down
Loading