diff --git a/app/(landing)/hackathons/preview/[orgId]/[draftId]/page.tsx b/app/(landing)/hackathons/preview/[orgId]/[draftId]/page.tsx index 2bfa90b08..d5ece3a86 100644 --- a/app/(landing)/hackathons/preview/[orgId]/[draftId]/page.tsx +++ b/app/(landing)/hackathons/preview/[orgId]/[draftId]/page.tsx @@ -95,13 +95,14 @@ export default function DraftPreviewPage({ params }: PreviewPageProps) { resolvedParams.orgId, resolvedParams.draftId ); - let organizationData = { name: '', logo: '' }; + let organizationData = { name: '', logo: '', slug: '' }; try { const orgRes = await getOrganization(resolvedParams.orgId); if (orgRes) { organizationData = { name: orgRes.name, logo: orgRes.logo || '', + slug: orgRes.slug || '', }; } } catch (orgErr) { @@ -130,6 +131,7 @@ export default function DraftPreviewPage({ params }: PreviewPageProps) { id: resolvedParams.orgId, name: organizationData.name, logo: organizationData.logo, + slug: organizationData.slug, }, status: 'DRAFT', diff --git a/components/avatars/GroupAvatar.tsx b/components/avatars/GroupAvatar.tsx index 006d2b2a9..7ba22fd86 100644 --- a/components/avatars/GroupAvatar.tsx +++ b/components/avatars/GroupAvatar.tsx @@ -19,10 +19,7 @@ const GroupAvatar = ({ members }: GroupAvatarProps) => { return ( {visibleMembers.map((member, index) => ( - + {member.slice(0, 2).toUpperCase()} @@ -30,7 +27,7 @@ const GroupAvatar = ({ members }: GroupAvatarProps) => { ))} {remainingCount > 0 && ( - + +{remainingCount} )} diff --git a/components/hackathons/HackathonsPage.tsx b/components/hackathons/HackathonsPage.tsx index 34f6dbffd..9a5c345a6 100644 --- a/components/hackathons/HackathonsPage.tsx +++ b/components/hackathons/HackathonsPage.tsx @@ -1,7 +1,7 @@ 'use client'; import React, { useState } from 'react'; -import HackathonCard from '@/components/landing-page/hackathon/HackathonCard'; +import { HackathonCard } from '@/components/landing-page/hackathon/HackathonCard'; import HackathonsFiltersHeader from '@/components/hackathons/HackathonsFiltersHeader'; import LoadingSpinner from '@/components/LoadingSpinner'; import { useHackathonFilters } from '@/hooks/hackathon/use-hackathon-filters'; diff --git a/components/landing-page/Explore.tsx b/components/landing-page/Explore.tsx index d784ffdae..d7659ef2b 100644 --- a/components/landing-page/Explore.tsx +++ b/components/landing-page/Explore.tsx @@ -4,7 +4,7 @@ import { cn } from '@/lib/utils'; import { ArrowRight } from 'lucide-react'; import Image from 'next/image'; import { useState, useRef, useEffect, useCallback, useMemo } from 'react'; -import HackathonCard from './hackathon/HackathonCard'; +import { HackathonCard } from './hackathon/HackathonCard'; import Link from 'next/link'; import { getPublicHackathonsList } from '@/lib/api/hackathons'; import type { Hackathon } from '@/lib/api/hackathons'; diff --git a/components/landing-page/Hero2.tsx b/components/landing-page/Hero2.tsx index 537b90515..e72a2eba8 100644 --- a/components/landing-page/Hero2.tsx +++ b/components/landing-page/Hero2.tsx @@ -10,7 +10,7 @@ import { } from '../ui/shadcn-io/cursor'; import Image from 'next/image'; import { BoundlessButton } from '../buttons'; -import HackathonCard from '@/components/landing-page/hackathon/HackathonCard'; +import { HackathonCard } from '@/components/landing-page/hackathon/HackathonCard'; import ProjectCard from '@/features/projects/components/ProjectCard'; import { Crowdfunding } from '@/features/projects/types'; diff --git a/components/landing-page/hackathon/AnimatedUsersButton.tsx b/components/landing-page/hackathon/AnimatedUsersButton.tsx new file mode 100644 index 000000000..cb54ea6a6 --- /dev/null +++ b/components/landing-page/hackathon/AnimatedUsersButton.tsx @@ -0,0 +1,62 @@ +'use client'; + +import { useRef } from 'react'; +import { gsap } from 'gsap'; +import { useGSAP } from '@gsap/react'; +import { Users } from 'lucide-react'; + +// Register the hook to prevent React strict-mode double-firing issues +gsap.registerPlugin(useGSAP); + +export default function AnimatedUsersButton() { + const usersIconRef = useRef(null); + + // 1. Mount Animation: Slides the background user in when the page loads + useGSAP( + () => { + if (!usersIconRef.current) return; + + // Selects elements inside this specific SVG + const q = gsap.utils.selector(usersIconRef); + + // Target the elements making up the background person + // (Lucide draws the foreground person first, so the last path/circle belong to the background) + gsap.from(q('path:last-of-type, circle:last-of-type'), { + x: 10, + opacity: 0, + duration: 0.6, + ease: 'back.out(1.5)', + delay: 0.1, // Slight delay so it feels natural after the page loads + }); + }, + { scope: usersIconRef } + ); + + // 2. Hover Animation: Makes both users "bounce" sequentially + const handleMouseEnter = () => { + if (!usersIconRef.current) return; + + // Grab all the individual paths and circles inside the SVG + const svgParts = usersIconRef.current.children; + + gsap.to(svgParts, { + y: -3, + duration: 0.2, + stagger: 0.05, + yoyo: true, + repeat: 1, + ease: 'power2.out', + transformOrigin: 'bottom center', + }); + }; + + return ( + + ); +} diff --git a/components/landing-page/hackathon/HackathonCard.tsx b/components/landing-page/hackathon/HackathonCard.tsx index 305124acd..105792d78 100644 --- a/components/landing-page/hackathon/HackathonCard.tsx +++ b/components/landing-page/hackathon/HackathonCard.tsx @@ -1,124 +1,26 @@ 'use client'; + import Link from 'next/link'; import Image from 'next/image'; -import { MapPinIcon } from 'lucide-react'; -import { useEffect, useState, useCallback } from 'react'; +import { useRouter } from 'next/navigation'; +import { + MapPin, + Users, + Trophy, + Clock, + Lock, + ArrowUpRight, + AlertCircle, +} from 'lucide-react'; +import { useEffect, useState, useCallback, useMemo } from 'react'; import { Hackathon } from '@/lib/api/hackathons'; import { cn } from '@/lib/utils'; - -// type HackathonCardProps = { -// id: string; -// name: string; -// slug: string; -// tagline: string; -// description: string; - -// banner: string; - -// organizationId: string; -// organization: { -// id: string; -// name: string; -// logo: string; -// }; - -// status: "DRAFT" | "PUBLISHED" | "ARCHIVED"; -// isActive: boolean; - -// venueType: "VIRTUAL" | "PHYSICAL" | "HYBRID"; -// venueName: string; -// venueAddress: string; -// city: string; -// state: string; -// country: string; -// timezone: string; - -// startDate: string; // ISO date -// endDate: string; // ISO date -// submissionDeadline: string; // ISO date -// registrationDeadline: string; // ISO date -// customRegistrationDeadline: string | null; - -// registrationOpen: boolean; -// registrationDeadlinePolicy: "BEFORE_SUBMISSION_DEADLINE" | "CUSTOM"; - -// daysUntilStart: number; -// daysUntilEnd: number; - -// participantType: "INDIVIDUAL" | "TEAM"; -// teamMin: number; -// teamMax: number; - -// categories: string[]; - -// enabledTabs: Array< -// | "detailsTab" -// | "participantsTab" -// | "resourcesTab" -// | "submissionTab" -// | "announcementsTab" -// | "discussionTab" -// | "winnersTab" -// | "sponsorsTab" -// | "joinATeamTab" -// | "rulesTab" -// >; - -// judgingCriteria: Array<{ -// id?: string; -// title?: string; -// description?: string; -// weight?: number; -// }>; - -// prizeTiers: Array<{ -// id?: string; -// title?: string; -// prizeAmount?: number; -// description?: string; -// }>; - -// phases: Array<{ -// id?: string; -// name?: string; -// startDate?: string; -// endDate?: string; -// }>; - -// resources: any[]; - -// sponsorsPartners: any[]; - -// submissions: any[]; -// followers: any[]; - -// requireGithub: boolean; -// requireDemoVideo: boolean; -// requireOtherLinks: boolean; - -// contactEmail: string; -// discord: string; -// telegram: string; -// socialLinks: string[]; - -// publishedAt: string; -// createdAt: string; -// updatedAt: string; - -// _count: { -// submissions: number; -// followers: number; -// }; -// isFullWidth?: boolean; -// isListView?: boolean; -// className?: string; - -// }; +import GroupAvatar from '@/components/avatars/GroupAvatar'; const formatFullNumber = (num: number): string => new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }).format(num); -const MAX_VISIBLE_CATEGORIES = 3; +const MAX_VISIBLE_CATEGORIES = 2; interface CategoriesDisplayProps { categoriesList?: string[]; @@ -128,24 +30,23 @@ const CategoriesDisplay = ({ categoriesList = [] }: CategoriesDisplayProps) => { const visible = categoriesList.slice(0, MAX_VISIBLE_CATEGORIES); const remainingCount = categoriesList.length - MAX_VISIBLE_CATEGORIES; + if (!categoriesList.length) return null; + return ( -
-
- {visible.map((cat, i) => ( - - {cat} - - ))} - - {remainingCount > 0 && ( - - +{remainingCount} - - )} -
+
+ {visible.map((cat, i) => ( + + {cat} + + ))} + {remainingCount > 0 && ( + + +{remainingCount} + + )}
); }; @@ -176,24 +77,26 @@ function calculateTimeRemaining(targetDate: string): TimeRemaining { }; } -// function formatCountdown(time: TimeRemaining): string { -// if (time.total <= 0) return 'Ended'; - -// if (time.days > 0) { -// return `${time.days} day${time.days !== 1 ? 's' : ''} left`; -// } else if (time.hours > 0) { -// return `${time.hours} hour${time.hours !== 1 ? 's' : ''} left`; -// } else if (time.minutes > 0) { -// return `${time.minutes} minute${time.minutes !== 1 ? 's' : ''} left`; -// } else { -// return `${time.seconds} second${time.seconds !== 1 ? 's' : ''} left`; -// } -// } - -interface HackathonCardProps extends Hackathon { +interface HackathonCardProps extends Omit< + Hackathon, + 'organization' | '_count' +> { + organization: { + id: string; + name: string; + logo: string; + slug?: string; + }; isFullWidth?: boolean; className?: string; target?: string; + isPrivate?: boolean; + isExtended?: boolean; + _count?: { + participants: number; + submissions: number; + followers: number; + }; } export const HackathonCard = ({ @@ -202,22 +105,26 @@ export const HackathonCard = ({ name, tagline, banner, - organization, status, - venueName, - startDate, - submissionDeadline, - categories, prizeTiers, + participants: participantMembers, isFullWidth = false, className, target, + isPrivate = false, + isExtended = false, + _count: { participants: participantsCount, submissions, followers } = { + participants: 0, + submissions: 0, + followers: 0, + }, }: HackathonCardProps) => { + const router = useRouter(); const [timeRemaining, setTimeRemaining] = useState({ days: 0, hours: 0, @@ -226,12 +133,20 @@ export const HackathonCard = ({ total: 0, }); - // Determine top badge status using raw dates — memoised so it can safely - // appear in the useEffect dependency array without triggering infinite loops. + const participantAvatars = useMemo(() => { + return (participantMembers || []) + .map(p => p.user?.profile?.image || '') + .filter(img => img !== ''); + }, [participantMembers]); + + const totalPrize = + prizeTiers?.reduce((acc, tier) => { + const amount = Number(tier.prizeAmount); + return acc + (Number.isFinite(amount) ? amount : 0); + }, 0) || 0; + const getTopBadgeStatus = useCallback(() => { - if (status === 'ARCHIVED') { - return 'Archived'; - } + if (status === 'ARCHIVED') return 'Archived'; const now = new Date().getTime(); const start = startDate ? new Date(startDate).getTime() : null; @@ -239,255 +154,201 @@ export const HackathonCard = ({ ? new Date(submissionDeadline).getTime() : null; - // Check if ended (submission deadline passed) - if (deadline && now > deadline) { - return 'Ended'; - } - - // Check if ongoing (started but submission deadline not passed) - if (start && now >= start) { - return 'Ongoing'; - } - - // Otherwise it's upcoming + if (deadline && now > deadline) return 'Ended'; + if (start && now >= start) return 'Ongoing'; return 'Upcoming'; }, [status, startDate, submissionDeadline]); const getTopBadgeColor = () => { - const badgeStatus = getTopBadgeStatus(); - switch (badgeStatus) { + switch (getTopBadgeStatus()) { case 'Ongoing': - return 'text-green-400 bg-green-400/10'; + return 'text-success-400 bg-success-400/10 border-success-400/20'; case 'Upcoming': - return 'text-blue-400 bg-blue-400/10'; + return 'text-secondary-400 bg-secondary-400/10 border-secondary-400/20'; case 'Ended': - return 'text-gray-400 bg-gray-800/20'; case 'Archived': - return 'text-red-400 bg-red-400/10'; default: - return 'text-gray-400 bg-gray-800/20'; + return 'text-muted-foreground bg-muted border-border'; } }; - // Determine bottom status text using real-time countdown - const getBottomStatusInfo = () => { - if (status === 'ARCHIVED') { - return { text: 'Archived', className: 'text-red-400' }; - } - - const badgeStatus = getTopBadgeStatus(); - - if (badgeStatus === 'Ended') { - return { text: 'Ended', className: 'text-gray-500' }; - } - - if (badgeStatus === 'Ongoing' && submissionDeadline) { - // Ongoing hackathon - show time until submission deadline - if (timeRemaining.total <= 0) { - return { text: 'Ended', className: 'text-gray-500' }; - } - if (timeRemaining.days === 0) { - return { text: 'Ending today', className: 'text-red-400' }; - } - if (timeRemaining.days === 1) { - return { text: 'Ending tomorrow', className: 'text-red-400' }; - } - if (timeRemaining.days <= 3) { - return { - text: `Ending in ${timeRemaining.days} days`, - className: 'text-red-400', - }; - } - if (timeRemaining.days <= 7) { - return { - text: `Ending in ${timeRemaining.days} days`, - className: 'text-yellow-400', - }; - } - return { - text: `Ending in ${timeRemaining.days} days`, - className: 'text-green-400', - }; - } - - if (badgeStatus === 'Upcoming' && startDate) { - // Upcoming hackathon - show time until start - if (timeRemaining.total <= 0) { - return { text: 'Starting soon', className: 'text-gray-400' }; - } - if (timeRemaining.days === 0) { - return { text: 'Starting today', className: 'text-red-400' }; - } - if (timeRemaining.days === 1) { - return { text: 'Starting tomorrow', className: 'text-red-400' }; - } - if (timeRemaining.days <= 3) { - return { - text: `Starting in ${timeRemaining.days} days`, - className: 'text-red-400', - }; - } - if (timeRemaining.days <= 7) { - return { - text: `Starting in ${timeRemaining.days} days`, - className: 'text-yellow-400', - }; - } - return { - text: `Starting in ${timeRemaining.days} days`, - className: 'text-blue-400', - }; - } - - return { text: 'Starting soon', className: 'text-gray-400' }; - }; - - // Update time remaining based on current status useEffect(() => { - let targetDate: string | null = null; const badgeStatus = getTopBadgeStatus(); + let targetDate: string | null = null; - if (badgeStatus === 'Ongoing' && submissionDeadline) { + if (badgeStatus === 'Ongoing' && submissionDeadline) targetDate = submissionDeadline; - } else if (badgeStatus === 'Upcoming' && startDate) { - targetDate = startDate; - } else if (badgeStatus === 'Ended' && submissionDeadline) { + else if (badgeStatus === 'Upcoming' && startDate) targetDate = startDate; + else if (badgeStatus === 'Ended' && submissionDeadline) targetDate = submissionDeadline; - } if (!targetDate) return; setTimeRemaining(calculateTimeRemaining(targetDate)); - // Update every second for ongoing/upcoming hackathons if (badgeStatus === 'Ongoing' || badgeStatus === 'Upcoming') { const interval = setInterval(() => { setTimeRemaining(calculateTimeRemaining(targetDate!)); }, 1000); - return () => clearInterval(interval); } }, [startDate, submissionDeadline, getTopBadgeStatus]); - const bottomStatusInfo = getBottomStatusInfo(); const topBadgeStatus = getTopBadgeStatus(); - const topBadgeColor = getTopBadgeColor(); - - // const locationText = (() => { - // if (location) { - // return location; - // } - // if (venueType === 'VIRTUAL') { - // return 'Virtual'; - // } - // if (venueType === 'PHYSICAL') { - // return 'Physical'; - // } - // return undefined; - // })(); - const href = `/hackathons/${slug || id || ''}`; + const handleOrganizerClick = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + const orgSlug = organization?.slug; + if (orgSlug) { + router.push(`/org/${orgSlug}`); + } + }; + return ( - {/* Image */} -
+
{name} -
+
-
+
- - {topBadgeStatus} - -
-
- {organization?.logo && ( -
- )} - {organization?.name && ( - - {organization.name} +
+ {isPrivate && ( + + Private + + )} + + {topBadgeStatus} - )} +
- {/* Body */} -
-
-

+
+ {organization && + (organization.slug ? ( + + ) : ( +
+ {organization.logo ? ( +
+ ) : ( +
+ )} + + {organization.name} + +
+ ))} + +
+

{name}

-

{tagline}

+

+ {tagline} +

-
- {prizeTiers && ( -
- - $ - {formatFullNumber( - prizeTiers.reduce((acc, tier) => { - const amount = Number(tier.prizeAmount); - return acc + (Number.isFinite(amount) ? amount : 0); - }, 0) - )} - - Prize Pool -
- )} - {/* {participantsCount && ( -
- - {formatNumber(participantsCount)} - {participantsGoal && `/${formatNumber(participantsGoal)}`} - - Participants + {totalPrize > 0 && ( +
+ + + Prize Pool + + + ${formatFullNumber(totalPrize)} + +
+ )} + +
+
+
+
+ + + {formatFullNumber(participantsCount)} Participants + +
- )} */} +
+ {venueName && venueName !== 'TBD' && ( -
- - {venueName} +
+ + + + {venueName} + +
)}
-
- - {bottomStatusInfo.text} - - {/* {participants?.goal && ( - - )} */} +
+
+ + + {timeRemaining.days > 0 + ? `${topBadgeStatus === 'Upcoming' ? 'Starts' : 'Ends'} in ${timeRemaining.days}d ${timeRemaining.hours}h` + : topBadgeStatus} + +
+ + {isExtended && ( + + + Extended + + )}
diff --git a/components/layout/sidebar.tsx b/components/layout/sidebar.tsx index 5b3b7fe06..8df480eac 100644 --- a/components/layout/sidebar.tsx +++ b/components/layout/sidebar.tsx @@ -187,7 +187,7 @@ const SidebarLayout: React.FC = () => { > { > @@ -301,7 +301,7 @@ const SidebarLayout: React.FC = () => { : 'text-gray-400 hover:bg-[#2A2A2A]/50 hover:text-white' )} > - + {item.label} diff --git a/features/projects/components/CreateProjectModal/SuccessScreen.tsx b/features/projects/components/CreateProjectModal/SuccessScreen.tsx index d50fbac42..43a397c50 100644 --- a/features/projects/components/CreateProjectModal/SuccessScreen.tsx +++ b/features/projects/components/CreateProjectModal/SuccessScreen.tsx @@ -4,7 +4,7 @@ import { motion } from 'motion/react'; const SuccessScreen = ({ onContinue }: { onContinue: () => void }) => { return ( -
+
{/*