diff --git a/app/(landing)/hackathons/[slug]/page.tsx b/app/(landing)/hackathons/[slug]/page.tsx new file mode 100644 index 000000000..35fbb8dba --- /dev/null +++ b/app/(landing)/hackathons/[slug]/page.tsx @@ -0,0 +1,133 @@ +'use client'; + +import { useState, useEffect, useMemo } from 'react'; +import { useRouter, useSearchParams, useParams } from 'next/navigation'; +import { useHackathonData } from '@/lib/providers/hackathonProvider'; + +import { HackathonBanner } from '@/components/hackathons/hackathonBanner'; +import { HackathonNavTabs } from '@/components/hackathons/hackathonNavTabs'; +import { HackathonOverview } from '@/components/hackathons/overview/hackathonOverview'; +import { HackathonParticipants } from '@/components/hackathons/participants/hackathonParticipant'; +import { HackathonResources } from '@/components/hackathons/resources/resources'; +import SubmissionTab from '@/components/hackathons/submissions/submissionTab'; +import { HackathonDiscussions } from '@/components/hackathons/discussion/comment'; +import LoadingScreen from '@/components/landing-page/project/CreateProjectModal/LoadingScreen'; + +export default function HackathonPage() { + const router = useRouter(); + const searchParams = useSearchParams(); + const params = useParams(); + + const { + currentHackathon, + content, + timelineEvents, + participants, + submissions, + prizes, + loading, + setCurrentHackathon, + } = useHackathonData(); + + const hackathonTabs = useMemo( + () => [ + { id: 'overview', label: 'Overview' }, + { id: 'participants', label: 'Participants', badge: participants.length }, + { id: 'resources', label: 'Resources' }, + { + id: 'submission', + label: 'Submissions', + badge: submissions.filter(p => p.status === 'Approved').length, + }, + { id: 'discussions', label: 'Discussions' }, + ], + [participants.length, submissions] + ); + + const hackathonId = params.slug as string; + const [activeTab, setActiveTab] = useState('overview'); + + useEffect(() => { + if (hackathonId) { + setCurrentHackathon(hackathonId); + } + }, [hackathonId, setCurrentHackathon]); + + useEffect(() => { + const tabFromUrl = searchParams.get('tab'); + if (tabFromUrl && hackathonTabs.some(tab => tab.id === tabFromUrl)) { + setActiveTab(tabFromUrl); + } + }, [searchParams, hackathonTabs]); + + const handleTabChange = (tabId: string) => { + setActiveTab(tabId); + const params = new URLSearchParams(searchParams.toString()); + params.set('tab', tabId); + router.push(`?${params.toString()}`, { scroll: false }); + }; + + if (loading) { + return ; + } + + if (!currentHackathon) { + return ( +
+
+

+ Hackathon not found +

+

+ The hackathon you're looking for doesn't exist. +

+
+
+ ); + } + + return ( +
+ {/* Banner */} + + + {/* Tabs */} + + + {/* Content */} +
+ {activeTab === 'overview' && ( + + )} + + {activeTab === 'participants' && } + + {activeTab === 'resources' && } + + {activeTab === 'submission' && } + + {activeTab === 'discussions' && ( + + )} +
+
+ ); +} diff --git a/app/(landing)/hackathons/layout.tsx b/app/(landing)/hackathons/layout.tsx new file mode 100644 index 000000000..9bc3e940e --- /dev/null +++ b/app/(landing)/hackathons/layout.tsx @@ -0,0 +1,22 @@ +import { HackathonDataProvider } from '@/lib/providers/hackathonProvider'; +import { use } from 'react'; + +interface HackathonLayoutProps { + children: React.ReactNode; + params: Promise<{ + slug?: string; + }>; +} + +export default function HackathonLayout({ + children, + params, +}: HackathonLayoutProps) { + const resolvedParams = use(params); + + return ( + + {children} + + ); +} diff --git a/components/hackathons/discussion/comment.tsx b/components/hackathons/discussion/comment.tsx new file mode 100644 index 000000000..c3d6455d4 --- /dev/null +++ b/components/hackathons/discussion/comment.tsx @@ -0,0 +1,390 @@ +'use client'; + +import { useState } from 'react'; +import { CommentsSortDropdown } from '@/components/project-details/comment-section/comments-sort-dropdown'; +import { CommentItem } from '@/components/project-details/comment-section/comment-item'; +import { CommentInput } from '@/components/project-details/comment-section/comment-input'; +import { CommentsEmptyState } from '@/components/project-details/comment-section/comments-empty-state'; + +interface CommentUser { + _id: string; + profile: { + firstName: string; + lastName: string; + username: string; + avatar?: string; + }; +} + +interface CommentReactionCounts { + LIKE: number; + DISLIKE: number; + HELPFUL: number; +} + +interface CommentEditHistory { + content: string; + editedAt: string; +} + +interface CommentReport { + userId: string; + reason: 'spam' | 'inappropriate' | 'harassment' | 'misinformation' | 'other'; + description?: string; + createdAt: string; +} + +interface Discussion { + _id: string; + userId: CommentUser; + projectId: string; + content: string; + parentCommentId?: string; + status: 'active' | 'deleted' | 'flagged' | 'hidden'; + editHistory: CommentEditHistory[]; + reactionCounts: CommentReactionCounts; + totalReactions: number; + replyCount: number; + replies: Discussion[]; + createdAt: string; + updatedAt: string; + isSpam: boolean; + reports: CommentReport[]; +} + +interface HackathonDiscussionsProps { + hackathonId: string; +} + +// Mock data +const mockDiscussions: Discussion[] = [ + { + _id: '1', + userId: { + _id: 'user1', + profile: { + firstName: 'Sarah', + lastName: 'Chen', + username: 'sarahc', + avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Sarah', + }, + }, + projectId: '', + content: + 'Excited to participate in this hackathon! Are there any team formation channels?', + status: 'active', + createdAt: '2025-01-10T10:30:00Z', + updatedAt: '2025-01-10T10:30:00Z', + totalReactions: 12, + reactionCounts: { LIKE: 12, DISLIKE: 0, HELPFUL: 0 }, + editHistory: [], + replyCount: 1, + isSpam: false, + reports: [], + replies: [ + { + _id: '1-1', + userId: { + _id: 'user2', + profile: { + firstName: 'Alex', + lastName: 'Rodriguez', + username: 'alexr', + avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Alex', + }, + }, + projectId: '', + content: + "Yes! Check out the Discord server, there's a #team-formation channel.", + status: 'active', + createdAt: '2025-01-10T11:00:00Z', + updatedAt: '2025-01-10T11:00:00Z', + totalReactions: 5, + reactionCounts: { LIKE: 5, DISLIKE: 0, HELPFUL: 0 }, + editHistory: [], + replyCount: 0, + isSpam: false, + reports: [], + replies: [], + parentCommentId: '1', + }, + ], + }, + { + _id: '2', + userId: { + _id: 'user3', + profile: { + firstName: 'Michael', + lastName: 'Chen', + username: 'michaelc', + avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Michael', + }, + }, + projectId: '', + content: + 'What technologies are allowed for this hackathon? Can we use any framework?', + status: 'active', + createdAt: '2025-01-11T14:20:00Z', + updatedAt: '2025-01-11T14:20:00Z', + totalReactions: 8, + reactionCounts: { LIKE: 8, DISLIKE: 0, HELPFUL: 0 }, + editHistory: [], + replyCount: 0, + isSpam: false, + reports: [], + replies: [], + }, + { + _id: '3', + userId: { + _id: 'user4', + profile: { + firstName: 'Emily', + lastName: 'Zhang', + username: 'emilyz', + avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Emily', + }, + }, + projectId: '', + content: + "Looking forward to the workshops! Will they be recorded for those who can't attend live?", + status: 'active', + createdAt: '2025-01-12T09:15:00Z', + updatedAt: '2025-01-12T09:15:00Z', + totalReactions: 15, + reactionCounts: { LIKE: 15, DISLIKE: 0, HELPFUL: 0 }, + editHistory: [], + replyCount: 0, + isSpam: false, + reports: [], + replies: [], + }, +]; + +export function HackathonDiscussions({ + hackathonId, +}: HackathonDiscussionsProps) { + const [sortBy, setSortBy] = useState< + 'createdAt' | 'updatedAt' | 'totalReactions' + >('createdAt'); + const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); + const [discussions, setDiscussions] = useState(mockDiscussions); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + // Sort discussions + const sortedDiscussions = [...discussions].sort((a, b) => { + let comparison = 0; + if (sortBy === 'createdAt') + comparison = + new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); + else if (sortBy === 'updatedAt') + comparison = + new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime(); + else comparison = a.totalReactions - b.totalReactions; + return sortOrder === 'desc' ? -comparison : comparison; + }); + + const handleAddDiscussion = async (content: string) => { + try { + setLoading(true); + const newDiscussion: Discussion = { + _id: Date.now().toString(), + userId: { + _id: 'current-user', + profile: { + firstName: 'Current', + lastName: 'User', + username: 'currentuser', + avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Current', + }, + }, + projectId: hackathonId, + content, + status: 'active', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + totalReactions: 0, + reactionCounts: { LIKE: 0, DISLIKE: 0, HELPFUL: 0 }, + editHistory: [], + replyCount: 0, + isSpam: false, + reports: [], + replies: [], + }; + setDiscussions([newDiscussion, ...discussions]); + } catch { + setError('Failed to post discussion'); + } finally { + setLoading(false); + } + }; + + const handleAddReply = async (parentCommentId: string, content: string) => { + try { + setLoading(true); + const newReply: Discussion = { + _id: `${Date.now()}`, + userId: { + _id: 'current-user', + profile: { + firstName: 'Current', + lastName: 'User', + username: 'currentuser', + avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Current', + }, + }, + projectId: hackathonId, + content, + status: 'active', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + totalReactions: 0, + reactionCounts: { LIKE: 0, DISLIKE: 0, HELPFUL: 0 }, + editHistory: [], + replyCount: 0, + isSpam: false, + reports: [], + replies: [], + parentCommentId, + }; + + const updateReplies = (items: Discussion[]): Discussion[] => + items.map(item => + item._id === parentCommentId + ? { + ...item, + replies: [...item.replies, newReply], + replyCount: item.replyCount + 1, + } + : { ...item, replies: updateReplies(item.replies) } + ); + + setDiscussions(updateReplies(discussions)); + } catch { + setError('Failed to post reply'); + } finally { + setLoading(false); + } + }; + + const handleUpdateDiscussion = async (commentId: string, content: string) => { + try { + setLoading(true); + const updateContent = (items: Discussion[]): Discussion[] => + items.map(item => + item._id === commentId + ? { + ...item, + content, + updatedAt: new Date().toISOString(), + editHistory: [ + ...item.editHistory, + { content: item.content, editedAt: new Date().toISOString() }, + ], + } + : { ...item, replies: updateContent(item.replies) } + ); + + setDiscussions(updateContent(discussions)); + } catch { + setError('Failed to update discussion'); + } finally { + setLoading(false); + } + }; + + const handleDeleteDiscussion = async (commentId: string) => { + try { + setLoading(true); + const deleteItem = (items: Discussion[]): Discussion[] => + items + .filter(item => item._id !== commentId) + .map(item => ({ ...item, replies: deleteItem(item.replies) })); + + setDiscussions(deleteItem(discussions)); + } catch { + setError('Failed to delete discussion'); + } finally { + setLoading(false); + } + }; + + const handleReportDiscussion = async ( + commentId: string, + reason: string, + description?: string + ) => { + try { + setLoading(true); + // Example: replace this console.log with actual API call + void { commentId, reason, description }; // ✅ avoids eslint 'no-unused-vars' + } catch { + setError('Failed to report discussion'); + } finally { + setLoading(false); + } + }; + + if (loading && discussions.length === 0) + return ( +
+ Loading discussions... +
+ ); + + if (error && discussions.length === 0) + return ( +
+ Error loading discussions: {error} + +
+ ); + + if (discussions.length === 0) + return ; + + return ( +
+
+ { + setSortBy(newSortBy); + setSortOrder(newSortOrder); + }} + /> +
+ +
+ {sortedDiscussions.map(discussion => ( + + ))} +
+ +
+ +
+ + {error && ( +
+

{error}

+
+ )} +
+ ); +} diff --git a/components/hackathons/hackathonBanner.tsx b/components/hackathons/hackathonBanner.tsx new file mode 100644 index 000000000..b12b4e377 --- /dev/null +++ b/components/hackathons/hackathonBanner.tsx @@ -0,0 +1,281 @@ +'use client'; + +import { Card } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { formatDate } from '@/lib/utils'; +import { useEffect, useState } from 'react'; + +interface HackathonBannerProps { + title: string; + subtitle?: string; + imageUrl?: string; + deadline?: string; + startDate?: string; + endDate?: string; + categories?: string[]; + status?: string; + participants?: number; + totalPrizePool?: string; +} + +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`; + } 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`; + } +} + +export function HackathonBanner({ + title, + subtitle, + imageUrl, + deadline, + startDate, + endDate, + categories, + status, + participants, + totalPrizePool, +}: HackathonBannerProps) { + const [timeRemaining, setTimeRemaining] = useState({ + days: 0, + hours: 0, + minutes: 0, + seconds: 0, + total: 0, + }); + + const getStatusColor = () => { + switch (status) { + case 'Ongoing': + case 'ongoing': + return 'text-blue-400 bg-blue-400/10'; + case 'Published': + case 'upcoming': + return 'text-primary bg-primary/10'; + case 'Completed': + 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'; + } + }; + + const getDeadlineInfo = () => { + const days = timeRemaining.days; + + if (timeRemaining.total <= 0) + return { text: 'Ended', className: 'text-gray-500' }; + if (days <= 3) + return { + text: formatCountdown(timeRemaining), + className: 'text-red-400', + }; + if (days <= 15) + return { + text: formatCountdown(timeRemaining), + className: 'text-yellow-400', + }; + return { + text: formatCountdown(timeRemaining), + className: 'text-green-400', + }; + }; + + useEffect(() => { + let targetDate: string | null = null; + const statusLower = status?.toLowerCase(); + if (statusLower === 'ongoing' && deadline) { + targetDate = deadline; + } else if (statusLower === '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]); + + const renderDateSection = () => { + const statusLower = status?.toLowerCase(); + const deadlineInfo = getDeadlineInfo(); + + if (statusLower === 'ongoing' && deadline) { + return ( +
+ Deadline: + + {deadlineInfo.text} + +
+ ); + } else if (statusLower === 'upcoming' && startDate) { + return ( +
+ Start: + + {formatDate(new Date(startDate))} + + {timeRemaining.total > 0 && ( + + ({formatCountdown(timeRemaining)}) + + )} +
+ ); + } else if (statusLower === 'ended' && endDate) { + return ( +
+ Ended: + + {formatDate(new Date(endDate))} + +
+ ); + } + + if (deadline) { + return ( +
+ + Deadline: + + + {formatDate(new Date(deadline))} + + {timeRemaining.total > 0 && ( + + ({deadlineInfo.text}) + + )} +
+ ); + } + + return null; + }; + + return ( + +
+
+ + {/* Text content */} +
+
+
+ {status && ( + + {status} + + )} + {participants !== undefined && ( + + {participants.toLocaleString()} participants + + )} +
+ {totalPrizePool && ( +
+ + Prize Pool + + + ${totalPrizePool} + +
+ )} +
+ +
+

+ {title} +

+ {subtitle && ( +

+ {subtitle.slice(3)} +

+ )} + {subtitle && ( +
+ )} +
+ + {/* Bottom section: Deadline/Start/Ended and categories */} +
+ {renderDateSection()} + {categories && categories.length > 0 && ( +
+ + Categories: + + {categories.map(category => ( + + {category} + + ))} +
+ )} +
+
+
+ + ); +} diff --git a/components/hackathons/hackathonNavTabs.tsx b/components/hackathons/hackathonNavTabs.tsx new file mode 100644 index 000000000..40dd2a3c8 --- /dev/null +++ b/components/hackathons/hackathonNavTabs.tsx @@ -0,0 +1,54 @@ +'use client'; + +interface HackathonNavTab { + id: string; + label: string; + badge?: number; +} + +interface HackathonNavTabsProps { + tabs: HackathonNavTab[]; + activeTab?: string; + onTabChange?: (tabId: string) => void; +} + +export function HackathonNavTabs({ + tabs, + activeTab, + onTabChange, +}: HackathonNavTabsProps) { + const handleTabChange = (tabId: string) => { + onTabChange?.(tabId); + }; + + return ( +
+
+
+ {tabs.map(tab => { + const isActive = activeTab === tab.id; + return ( + + ); + })} +
+
+
+ ); +} diff --git a/components/hackathons/overview/hackathonOverview.tsx b/components/hackathons/overview/hackathonOverview.tsx new file mode 100644 index 000000000..c43088c6e --- /dev/null +++ b/components/hackathons/overview/hackathonOverview.tsx @@ -0,0 +1,105 @@ +'use client'; +import ReactMarkdown from 'react-markdown'; +import { HackathonTimeline } from './hackathonTimeline'; +import { HackathonPrizes } from './hackathonPrizes'; + +interface TimelineEvent { + event: string; + date: string; +} + +interface Prize { + title: string; + rank: string; + prize: string; + details: string[]; + icon?: string; +} + +interface HackathonOverviewProps { + content: string; + timelineEvents?: TimelineEvent[]; + prizes?: Prize[]; + className?: string; +} + +export function HackathonOverview({ + content, + timelineEvents, + prizes, + className = '', +}: HackathonOverviewProps) { + return ( +
+
+ ( +

+ ), + h2: ({ ...props }) => ( +

+ ), + h3: ({ ...props }) => ( +

+ ), + p: ({ ...props }) => ( +

+ ), + ul: ({ ...props }) => ( +

    + ), + ol: ({ ...props }) => ( +
      + ), + li: ({ ...props }) =>
    1. , + blockquote: ({ ...props }) => ( +
      + ), + code: ({ ...props }) => ( + + ), + pre: ({ ...props }) => ( +
      +            ),
      +            table: ({ ...props }) => (
      +              
      +            ),
      +            th: ({ ...props }) => (
      +              
      + ), + td: ({ ...props }) => ( + + ), + }} + > + {content} + + + + {timelineEvents && } + {prizes && } + + ); +} diff --git a/components/hackathons/overview/hackathonPrizes.tsx b/components/hackathons/overview/hackathonPrizes.tsx new file mode 100644 index 000000000..b56670829 --- /dev/null +++ b/components/hackathons/overview/hackathonPrizes.tsx @@ -0,0 +1,74 @@ +'use client'; + +// interface Prize { +// title: string +// rank: string +// prize: string +// details: string[] +// icon?: string +// } + +interface HackathonPrizesProps { + title?: string; + totalPrizes?: string; + otherPrizes?: string; + prizes: Array<{ + title: string; + rank: string; + prize: string; + details: string[]; + icon?: string; + }>; +} + +export function HackathonPrizes({ + title = 'PRIZES', + totalPrizes = '$1,000+ in prizes', + otherPrizes, + prizes, +}: HackathonPrizesProps) { + return ( +
      +
      +

      {title}

      +
      + {totalPrizes} + {otherPrizes && ( + + {otherPrizes} + )} +
      +
      + +
      + {prizes.map((prize, index) => ( +
      +
      + {prize.icon || '⭐'} +
      +

      {prize.title}

      +

      {prize.rank}

      +
      +
      + +
      +
      + {prize.prize} +
      +
        + {prize.details.map((detail, i) => ( +
      • + + {detail} +
      • + ))} +
      +
      +
      + ))} +
      +
      + ); +} diff --git a/components/hackathons/overview/hackathonTimeline.tsx b/components/hackathons/overview/hackathonTimeline.tsx new file mode 100644 index 000000000..2a9ed046f --- /dev/null +++ b/components/hackathons/overview/hackathonTimeline.tsx @@ -0,0 +1,64 @@ +'use client'; + +interface TimelineEvent { + event: string; + date: string; +} + +interface HackathonTimelineProps { + events?: TimelineEvent[]; +} + +export function HackathonTimeline({ + events = [ + { event: 'Hackathon Launch', date: 'September 9, 2025' }, + { event: 'First Workshop: Getting Started', date: 'September 10' }, + { + event: 'Weekly Web Design & Deployment Sessions', + date: 'Sept 11 – Sep 28', + }, + { event: 'Submission Deadline', date: 'October 28, 2025 @ 11:59 PM UTC' }, + { event: 'Judging Period', date: 'November 9' }, + { event: 'Winners Announced', date: 'December 15, 2025' }, + ], +}: HackathonTimelineProps) { + return ( +
      +
      +

      + TIMELINE & KEY DATES +

      +
      + +
      + + + + + + + + + {events.map((event, index) => ( + + + + + ))} + +
      + EVENT + + DATE +
      + {event.event} + + {event.date} +
      +
      +
      + ); +} diff --git a/components/hackathons/overview/joinHackathon.tsx b/components/hackathons/overview/joinHackathon.tsx new file mode 100644 index 000000000..bcb2c3446 --- /dev/null +++ b/components/hackathons/overview/joinHackathon.tsx @@ -0,0 +1,82 @@ +'use client'; + +import { useEffect, useState, useMemo } from 'react'; +import { Button } from '@/components/ui/button'; +import { ArrowRight } from 'lucide-react'; + +interface JoinHackathonBannerProps { + onJoinClick?: () => void; + participants?: number; + prizePool?: string; + isEnded?: boolean; +} + +export function JoinHackathonBanner({ + onJoinClick, + participants = 342, + prizePool = '1,000', + isEnded = false, +}: JoinHackathonBannerProps) { + const titles = useMemo( + () => [ + 'Ready to Build Something Amazing?', + 'Show Your Coding Superpowers!', + 'Build. Compete. Win Big!', + 'Hack the Future with Us!', + 'Your Next Great Project Starts Here!', + 'Push the Limits of Innovation!', + 'Create. Collaborate. Conquer!', + 'Turn Your Ideas into Impact!', + 'Join the Ultimate Dev Challenge!', + 'Code Today, Lead Tomorrow!', + ], + [] + ); + + const [currentTitle, setCurrentTitle] = useState( + titles[Math.floor(Math.random() * titles.length)] + ); + + useEffect(() => { + const interval = setInterval(() => { + const randomIndex = Math.floor(Math.random() * titles.length); + setCurrentTitle(titles[randomIndex]); + }, 3000 * 5); // every 15 seconds + + return () => clearInterval(interval); + }, [titles]); + + return ( +
      +
      + {/* Animated gradient background */} +
      + +
      + {/* Text content */} +
      +

      + {currentTitle} +

      +

      + Join {participants}+ developers competing for ${prizePool}+ in + prizes +

      +
      + + {/* Join Button */} + +
      +
      +
      + ); +} diff --git a/components/hackathons/participants/hackathonParticipant.tsx b/components/hackathons/participants/hackathonParticipant.tsx new file mode 100644 index 000000000..169b17d67 --- /dev/null +++ b/components/hackathons/participants/hackathonParticipant.tsx @@ -0,0 +1,78 @@ +'use client'; + +import { useParticipants } from '@/hooks/hackathon/use-participants'; +import ParticipantsFilter from './participantFilter'; +import { ParticipantAvatar } from './participantAvatar'; + +export const HackathonParticipants = () => { + const { + participants, + totalParticipants, + submittedCount, + setSearchTerm, + setSortBy, + setSubmissionFilter, + setSkillFilter, + } = useParticipants(); + + return ( +
      + {/* Explanation Section */} +
      +

      + Understanding Participant Status +

      +
      +
      +
      +
      +
      +
      +
      +
      +

      + Participants with a{' '} + + green indicator dot + {' '} + on their avatar have successfully submitted their hackathon + project. This visual badge helps you quickly identify active + contributors who have completed their submissions. Participants + without the green dot are still working on their projects or + haven't submitted yet. +

      +
      +
      +
      + + {/* Filters */} + + + {/* Participants Grid */} +
      + {participants.map(participant => ( + + ))} +
      + + {/* Empty State */} + {participants.length === 0 && ( +
      +

      + No participants found matching your filters. +

      +

      + Try adjusting your search or filter criteria. +

      +
      + )} +
      + ); +}; diff --git a/components/hackathons/participants/participantAvatar.tsx b/components/hackathons/participants/participantAvatar.tsx new file mode 100644 index 000000000..eb53b6ba9 --- /dev/null +++ b/components/hackathons/participants/participantAvatar.tsx @@ -0,0 +1,71 @@ +'use client'; + +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { ProfileCard } from './profileCard'; +import type { Participant } from '@/types/hackathon'; +import Image from 'next/image'; + +interface ParticipantAvatarProps { + participant: Participant; +} + +export function ParticipantAvatar({ participant }: ParticipantAvatarProps) { + return ( + + + +
      +
      + + + {/* */} + {/* {participant.username.slice(0, 2).toUpperCase()} */} + + avatar + + {/* */} + + + {/* Green status dot if submitted */} + {participant.hasSubmitted && ( +
      + )} +
      + + + {participant.username} + +
      + + + + + + + + ); +} diff --git a/components/hackathons/participants/participantFilter.tsx b/components/hackathons/participants/participantFilter.tsx new file mode 100644 index 000000000..b4b84412f --- /dev/null +++ b/components/hackathons/participants/participantFilter.tsx @@ -0,0 +1,215 @@ +'use client'; + +import React, { useState } from 'react'; +import { Search, ChevronDown } from 'lucide-react'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { cn } from '@/lib/utils'; +import Image from 'next/image'; + +interface ParticipantsFilterProps { + onSearch?: (searchTerm: string) => void; + onSortChange?: (sortType: string) => void; + onSubmissionStatusChange?: (status: string) => void; + onSkillChange?: (skill: string) => void; + className?: string; + searchPlaceholder?: string; + totalParticipants?: number; + submittedCount?: number; +} + +const ParticipantsFilter = ({ + onSearch, + onSortChange, + onSubmissionStatusChange, + onSkillChange, + className, + searchPlaceholder = 'Search by name, role, or skills...', + totalParticipants = 0, + // submittedCount = 0, +}: ParticipantsFilterProps) => { + const [searchTerm, setSearchTerm] = useState(''); + const [selectedSort, setSelectedSort] = useState('Newest First'); + const [selectedStatus, setSelectedStatus] = useState('All Participants'); + const [selectedSkill, setSelectedSkill] = useState('All Skills'); + + const handleSearch = (value: string) => { + setSearchTerm(value); + if (onSearch) onSearch(value); + }; + + const handleSort = (sortValue: string) => { + const option = sortOptions.find(opt => opt.value === sortValue); + setSelectedSort(option?.label || 'Newest First'); + if (onSortChange) onSortChange(sortValue); + }; + + const handleStatus = (statusValue: string) => { + const option = statusOptions.find(opt => opt.value === statusValue); + setSelectedStatus(option?.label || 'All Participants'); + if (onSubmissionStatusChange) onSubmissionStatusChange(statusValue); + }; + + const handleSkill = (skillValue: string) => { + const option = skillOptions.find(opt => opt.value === skillValue); + setSelectedSkill(option?.label || 'All Skills'); + if (onSkillChange) onSkillChange(skillValue); + }; + + const sortOptions = [ + { label: 'Newest First', value: 'newest' }, + { label: 'Oldest First', value: 'oldest' }, + { label: 'Most Followers', value: 'followers_high' }, + { label: 'Least Followers', value: 'followers_low' }, + { label: 'Most Projects', value: 'projects_high' }, + { label: 'Least Projects', value: 'projects_low' }, + ]; + + const statusOptions = [ + { label: 'All Participants', value: 'all' }, + { label: 'Submitted', value: 'submitted' }, + { label: 'Not Submitted', value: 'not_submitted' }, + ]; + + const skillOptions = [ + { label: 'All Skills', value: 'all' }, + { label: 'Full Stack Developer', value: 'fullstack' }, + { label: 'Frontend Developer', value: 'frontend' }, + { label: 'Backend Developer', value: 'backend' }, + { label: 'Mobile Developer', value: 'mobile' }, + { label: 'UI/UX Designer', value: 'uiux' }, + { label: 'Product Designer', value: 'product_designer' }, + { label: 'Data Scientist', value: 'data_scientist' }, + { label: 'DevOps Engineer', value: 'devops' }, + { label: 'React', value: 'react' }, + { label: 'Node.js', value: 'nodejs' }, + { label: 'Python', value: 'python' }, + { label: 'TypeScript', value: 'typescript' }, + { label: 'Web3', value: 'web3' }, + ]; + + return ( +
      + {/* Stats Section */} +
      + + + {totalParticipants} + {' '} + total participants + +
      + + {/* Filters */} +
      +
      + + + + + + {sortOptions.map(option => ( + handleSort(option.value)} + className='cursor-pointer hover:bg-gray-800' + > + {option.label} + + ))} + + + + + + + + + {statusOptions.map(option => ( + handleStatus(option.value)} + className='cursor-pointer hover:bg-gray-800' + > + {option.label} + + ))} + + + + + + + + + {skillOptions.map(option => ( + handleSkill(option.value)} + className='cursor-pointer hover:bg-gray-800' + > + {option.label} + + ))} + + +
      + +
      + + handleSearch(e.target.value)} + className='bg-background w-full rounded-lg border-gray-900 py-3 pr-4 pl-10 text-base text-white placeholder-gray-400 focus:border-gray-400 focus:ring-1 focus:ring-gray-400' + /> +
      +
      +
      + ); +}; + +export default ParticipantsFilter; diff --git a/components/hackathons/participants/profileCard.tsx b/components/hackathons/participants/profileCard.tsx new file mode 100644 index 000000000..7bada8419 --- /dev/null +++ b/components/hackathons/participants/profileCard.tsx @@ -0,0 +1,130 @@ +'use client'; + +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import type { Participant } from '@/types/hackathon'; +import Image from 'next/image'; +import { MessageCircle } from 'lucide-react'; +import { format } from 'date-fns'; + +interface ProfileCardProps { + participant: Participant; +} + +export function ProfileCard({ participant }: ProfileCardProps) { + const [isFollowing, setIsFollowing] = useState(false); + + return ( +
      + {/* Header with avatar */} +
      +
      +
      + {participant.username} +
      +
      +

      + {participant.name} + {participant.verified && ( + + + + )} +

      +

      @{participant.username}

      +

      + Joined {format(new Date(participant.joinedDate!), 'MMM, yyyy')} +

      +
      +
      +
      + + {/* Action Buttons */} +
      + + +
      + + {/* Role */} + {participant.role && ( +
      + + {participant.role} + +
      + )} + + {/* Description */} + {participant.description && ( +

      + {participant.description} +

      + )} + + {/* Categories */} + {participant.categories && participant.categories.length > 0 && ( +
      +

      Interests

      +
      + {participant.categories.map((category, index) => ( + + {category} + + ))} +
      +
      + )} + + {/* Stats */} +
      +
      +

      + {participant.projects || 0} +

      +

      Projects

      +
      +
      +

      + {participant.followers || 0} +

      +

      Followers

      +
      +
      +

      + {participant.following || 0} +

      +

      Following

      +
      +
      +
      + ); +} diff --git a/components/hackathons/resources/resources.tsx b/components/hackathons/resources/resources.tsx new file mode 100644 index 000000000..1a392bf95 --- /dev/null +++ b/components/hackathons/resources/resources.tsx @@ -0,0 +1,192 @@ +'use client'; +import { + BarChart3, + FileEdit, + FileText, + Library, + Link2, + Presentation, + VideoIcon, +} from 'lucide-react'; +import Link from 'next/link'; + +interface ResourceItem { + id: number; + title: string; + type: 'pdf' | 'doc' | 'sheet' | 'slide' | 'link'; + size?: string; + url: string; + uploadDate: string; + description?: string; +} + +export function HackathonResources() { + const resources: ResourceItem[] = [ + // Documents + { + id: 1, + title: 'Hackathon Rulebook', + type: 'pdf', + size: '2.4 MB', + url: '#', + uploadDate: 'Oct 15, 2024', + description: 'Complete rules and guidelines for participation', + }, + { + id: 2, + title: 'Submission Template', + type: 'doc', + size: '1.1 MB', + url: '#', + uploadDate: 'Oct 15, 2024', + description: 'Template for project submission documentation', + }, + { + id: 3, + title: 'Judging Criteria', + type: 'sheet', + size: '0.8 MB', + url: '#', + uploadDate: 'Oct 14, 2024', + description: 'Detailed breakdown of judging criteria and scoring', + }, + { + id: 5, + title: 'Design Resources', + type: 'slide', + size: '5.7 MB', + url: '#', + uploadDate: 'Oct 12, 2024', + description: 'Design assets and presentation templates', + }, + { + id: 7, + title: 'GitHub Repository', + type: 'link', + url: 'https://github.com/techvision/hackathon-2024', + uploadDate: 'Oct 14, 2024', + description: 'Starter code and project templates', + }, + ]; + + const getFileIcon = (type: string) => { + const iconClass = 'w-5 h-5'; + + switch (type) { + case 'pdf': + return ; + case 'doc': + return ; + case 'sheet': + return ; + case 'slide': + return ; + case 'link': + return ; + default: + return ; + } + }; + + const getTypeLabel = (type: string) => { + switch (type) { + case 'pdf': + return 'PDF'; + case 'doc': + return 'DOC'; + case 'sheet': + return 'SHEET'; + case 'slide': + return 'SLIDES'; + case 'link': + return 'LINK'; + default: + return type.toUpperCase(); + } + }; + + return ( +
      + {/* Header */} +
      +

      + Hackathon Resources +

      +
      + + {/* Video Guides Section */} +
      +

      +
      + +
      + Video Guides +

      + +
      +
      +