diff --git a/app/(landing)/hackathons/[slug]/page.tsx b/app/(landing)/hackathons/[slug]/page.tsx index 18ca6f903..8429926da 100644 --- a/app/(landing)/hackathons/[slug]/page.tsx +++ b/app/(landing)/hackathons/[slug]/page.tsx @@ -42,6 +42,7 @@ export default function HackathonPage() { const hackathonTabs = useMemo(() => { const hasParticipants = participants.length > 0; + const hasResources = currentHackathon?.resources?.resources?.[0]; const tabs = [ { id: 'overview', label: 'Overview' }, @@ -54,7 +55,15 @@ export default function HackathonPage() { }, ] : []), - { id: 'resources', label: 'Resources' }, + ...(hasResources + ? [ + { + id: 'resources', + label: 'Resources', + badge: currentHackathon?.resources?.resources?.length, + }, + ] + : []), { id: 'submission', @@ -302,9 +311,10 @@ export default function HackathonPage() { {activeTab === 'team-formation' && ( )} - {activeTab === 'resources' && ( - - )} + {activeTab === 'resources' && + currentHackathon?.resources?.resources?.[0] && ( + + )} ); diff --git a/app/(landing)/hackathons/[slug]/team-invitations/[token]/accept/page.tsx b/app/(landing)/hackathons/[slug]/team-invitations/[token]/accept/page.tsx new file mode 100644 index 000000000..b5b6fa706 --- /dev/null +++ b/app/(landing)/hackathons/[slug]/team-invitations/[token]/accept/page.tsx @@ -0,0 +1,354 @@ +'use client'; +import { useParams, useRouter, useSearchParams } from 'next/navigation'; +import React, { useEffect, useState } from 'react'; +import { + Mail, + Users, + Shield, + AlertCircle, + Loader2, + CheckCircle2, + ArrowRight, +} from 'lucide-react'; +import { useAuthStatus } from '@/hooks/use-auth'; +import { acceptTeamInvitation } from '@/lib/api/hackathons'; +import { toast } from 'sonner'; + +const AcceptTeamInvitationPage = () => { + const params = useParams(); + const searchParams = useSearchParams(); + const router = useRouter(); + const { isAuthenticated, isLoading: authLoading } = useAuthStatus(); + + const [isProcessing, setIsProcessing] = useState(false); + const [error, setError] = useState(null); + const [successTeamName, setSuccessTeamName] = useState(''); + const [showAcceptButton, setShowAcceptButton] = useState(false); + + const hackathonSlug = params.slug as string; + const token = params.token as string; + const redirectToken = searchParams.get('token'); + const invitationToken = token || redirectToken; + + useEffect(() => { + if (!invitationToken) { + router.push(`/hackathons/${hackathonSlug}`); + return; + } + + // If user is authenticated, show accept button + if (isAuthenticated && !authLoading) { + setShowAcceptButton(true); + } + + // If user is not authenticated and not loading, redirect to auth + if (!isAuthenticated && !authLoading) { + redirectToAuth(); + } + }, [isAuthenticated, authLoading, invitationToken, hackathonSlug]); + + const redirectToAuth = () => { + const redirectUrl = `/hackathons/${hackathonSlug}/team-invitations/${invitationToken}/accept`; + const authUrl = `/auth?mode=signin&redirect=${encodeURIComponent(redirectUrl)}`; + router.push(authUrl); + }; + + const handleAcceptInvitation = async () => { + if (!invitationToken || isProcessing) return; + + setIsProcessing(true); + setError(null); + + try { + const response = await acceptTeamInvitation(hackathonSlug, { + token: invitationToken, + }); + + if (response.success) { + setSuccessTeamName(response.data.teamName); + toast.success(`Successfully joined ${response.data.teamName}!`); + setTimeout(() => { + router.push(`/hackathons/${hackathonSlug}`); + }, 2000); + } + } catch (err: any) { + console.error('Failed to accept invitation:', err); + + const errorMessage = err?.message || 'Failed to accept invitation'; + setError(errorMessage); + + // Handle specific error cases + if (err?.status === 403) { + if (errorMessage.includes('different email address')) { + toast.error('This invitation was sent to a different email address'); + } else { + toast.error('Authentication required'); + redirectToAuth(); + } + } else if (err?.status === 404) { + toast.error('Invitation not found or has expired'); + } else if (err?.status === 409) { + toast.error('You are already a member of this team'); + } else { + toast.error(errorMessage); + } + } finally { + setIsProcessing(false); + } + }; + + // Loading authentication state + if (authLoading) { + return ( +
+
+
+
+
+
+ +
+
+ +
+
+
+ +

+ Verifying Invitation +

+ +

+ Please wait while we verify your invitation... +

+ +
+
+
+ Checking authentication +
+
+
+
+
+ ); + } + + // Show accept button (user is authenticated and ready to accept) + if (showAcceptButton && !error && !successTeamName) { + return ( +
+
+
+ {/* Icon */} +
+
+ +
+
+ + {/* Title */} +

+ Team Invitation +

+ + {/* Description */} +

+ You've been invited to join a team for this hackathon. Click below + to accept the invitation and become a team member. +

+ + {/* Info box */} +
+
+ +
+

+ Ready to join? +

+

+ By accepting this invitation, you'll be added to the team + and can start collaborating immediately. +

+
+
+
+ + {/* Action buttons */} +
+ + +
+
+
+
+ ); + } + + // Error state + if (error && !isProcessing) { + const isWrongEmail = error.includes('different email address'); + const isExpired = error.includes('expired') || error.includes('not found'); + const isAlreadyMember = error.includes('already a member'); + + return ( +
+
+
+ {/* Icon */} +
+
+ {isAlreadyMember ? ( + + ) : ( + + )} +
+
+ + {/* Title */} +

+ {isAlreadyMember + ? 'Already a Team Member' + : isWrongEmail + ? 'Wrong Account' + : isExpired + ? 'Invitation Expired' + : 'Invitation Error'} +

+ + {/* Description */} +

{error}

+ + {/* Additional info for wrong email */} + {isWrongEmail && ( +
+
+ +
+

+ Sign up with the correct email +

+

+ Create an account with the email this invitation was sent + to. +

+
+
+
+ )} + + {/* Additional info for already member */} + {isAlreadyMember && ( +
+
+ +
+

+ You're all set! +

+

+ You're already part of this team. Head back to the + hackathon page. +

+
+
+
+ )} + + {/* Action buttons */} +
+ {isWrongEmail ? ( + <> + + + + ) : ( + + )} +
+
+
+
+ ); + } + + // Success state (brief moment before redirect) + if (successTeamName) { + return ( +
+
+
+ {/* Icon */} +
+
+ +
+
+ + {/* Title */} +

+ Successfully Joined! +

+ + {/* Description */} +

+ Welcome to {successTeamName}! Redirecting to hackathon page... +

+ + {/* Progress bar */} +
+
+
+
+
+
+ ); + } + + return null; +}; + +export default AcceptTeamInvitationPage; diff --git a/app/(landing)/organizations/[id]/hackathons/page.tsx b/app/(landing)/organizations/[id]/hackathons/page.tsx index 74d54738d..edaedbbf0 100644 --- a/app/(landing)/organizations/[id]/hackathons/page.tsx +++ b/app/(landing)/organizations/[id]/hackathons/page.tsx @@ -11,6 +11,7 @@ import { ExternalLink, Settings, Eye, + Trash2, } from 'lucide-react'; import { Input } from '@/components/ui/input'; import { @@ -24,7 +25,10 @@ import Link from 'next/link'; import Image from 'next/image'; import { BoundlessButton } from '@/components/buttons'; import { useHackathons } from '@/hooks/use-hackathons'; +import { useDeleteHackathon } from '@/hooks/hackathon/use-delete-hackathon'; import type { Hackathon, HackathonDraft } from '@/lib/api/hackathons'; +import { toast } from 'sonner'; +import DeleteHackathonDialog from '@/components/organization/DeleteHackathonDialog'; const calculateDraftCompletion = (draft: HackathonDraft): number => { const fields = [ @@ -77,13 +81,36 @@ export default function HackathonsPage() { 'all' ); const [categoryFilter, setCategoryFilter] = useState('all'); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [hackathonToDelete, setHackathonToDelete] = useState<{ + id: string; + title: string; + } | null>(null); - const { hackathons, hackathonsLoading, drafts, draftsLoading } = + const { hackathons, hackathonsLoading, drafts, draftsLoading, refetchAll } = useHackathons({ organizationId, autoFetch: true, }); + // Use the separate delete hook + const { isDeleting, deleteHackathon } = useDeleteHackathon({ + organizationId, + hackathonId: hackathonToDelete?.id || '', // This will be set when we have a hackathon to delete + onSuccess: () => { + // Refresh the hackathons list after successful deletion + refetchAll(); + toast.success('Hackathon deleted successfully', { + description: `"${hackathonToDelete?.title}" has been permanently deleted.`, + }); + }, + onError: error => { + toast.error('Failed to delete hackathon', { + description: error, + }); + }, + }); + const allHackathons = useMemo(() => { const items: Array<{ type: 'draft' | 'hackathon'; @@ -140,6 +167,32 @@ export default function HackathonsPage() { return { published, drafts: drafts.length, total }; }, [hackathons, drafts]); + const handleDeleteClick = (hackathonId: string) => { + const hackathon = allHackathons.find(item => item.data._id === hackathonId); + if (hackathon) { + const title = + hackathon.data.information?.title || + hackathon.data.title || + 'Untitled Hackathon'; + setHackathonToDelete({ id: hackathonId, title }); + setDeleteDialogOpen(true); + } + }; + + const handleDeleteConfirm = async () => { + if (!hackathonToDelete) return; + + setDeleteDialogOpen(false); + + try { + await deleteHackathon(); + } catch (error) { + console.error(error); + } finally { + setHackathonToDelete(null); + } + }; + return (
{/* Header */} @@ -373,6 +426,17 @@ export default function HackathonsPage() { > + +
@@ -496,6 +568,17 @@ export default function HackathonsPage() { )} + + {/* Delete Hackathon Dialog */} + {hackathonToDelete && ( + + )} ); } diff --git a/app/(landing)/profile/[username]/profile-data.tsx b/app/(landing)/profile/[username]/profile-data.tsx index 4deb79319..b9f013bac 100644 --- a/app/(landing)/profile/[username]/profile-data.tsx +++ b/app/(landing)/profile/[username]/profile-data.tsx @@ -1,68 +1,186 @@ -import { - getUserProfileByUsernameServer, - getMeServer, -} from '@/lib/api/auth-server'; +'use client'; + +import { useEffect, useState } from 'react'; +import { getUserProfileByUsername } from '@/lib/api/auth'; import { GetMeResponse } from '@/lib/api/types'; -import { getServerUser } from '@/lib/auth/server-auth'; +import { useAuthStore } from '@/lib/stores/auth-store'; import ProfileOverview from '@/components/profile/ProfileOverview'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import ActivityTab from '@/components/profile/ActivityTab'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { Button } from '@/components/ui/button'; +import ActivityFeed from '@/components/profile/ActivityFeed'; +import ProjectsTab from '@/components/profile/ProjectsTab'; +import OrganizationsTab from '@/components/profile/OrganizationsTab'; +import { Filter } from 'lucide-react'; -interface ProfileDataProps { +interface PublicProfileDataProps { username: string; } -export async function ProfileData({ username }: ProfileDataProps) { - const user = await getServerUser(); +const FILTER_OPTIONS = [ + 'All', + 'Today', + 'Yesterday', + 'This Week', + 'This Month', + 'This Year', + 'All Time', +]; + +export function ProfileData({ username }: PublicProfileDataProps) { + const [userData, setUserData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [selectedFilter, setSelectedFilter] = useState('All'); + + const { user: authUser, isAuthenticated } = useAuthStore(); + + useEffect(() => { + async function loadProfile() { + try { + setLoading(true); + const data = await getUserProfileByUsername(username); + console.log('userData:', data); // This will log the actual data + setUserData(data); + } catch (err) { + setError(`Failed to load user profile: ${err}`); + } finally { + setLoading(false); + } + } + + loadProfile(); + }, [username]); - // Check if user is authenticated - if (!user) { + // This logs on every render - will show null initially, then the data after fetch + useEffect(() => { + if (userData) { + console.log('Current userData state:', userData); + } + }, [userData]); + + if (loading) { return (
-
Please sign in to view profiles
+
Loading profile...
); } - try { - // Check if viewing own profile by comparing with user data - // We need to fetch user data first to get the username - const currentUserData = await getMeServer(); - const isOwnProfile = - currentUserData.profile?.username === username || - currentUserData._id === username; - - let userData: GetMeResponse; - - // Use server-side versions that forward cookies from request headers - if (isOwnProfile) { - // For own profile, use the already fetched user data - userData = currentUserData; - } else { - // For other profiles, fetch by username - userData = await getUserProfileByUsernameServer(username); - } - - return ; - } catch (error) { - // Check if it's an authentication error - if ( - error && - typeof error === 'object' && - 'status' in error && - error.status === 401 - ) { - return ( -
-
- Session expired. Please sign in again. -
-
- ); - } + if (error) { + return ( +
+
{error}
+
+ ); + } + if (!userData) { return (
-
Failed to load user profile
+
User not found
); } + + const organizationsData = + userData.organizations?.map(org => ({ + name: org.name, + avatarUrl: org.logo || '/blog1.jpg', + })) || []; + // Determine if it's the user's own profile + const isOwnProfile = + isAuthenticated && authUser?.profile?.username === username; + + return ( +
+ + +
+ +
+ + + Activity + + + Projects + + + Organizations + + +
+ +
+ + + + + + + + + {FILTER_OPTIONS.map(filter => ( + setSelectedFilter(filter)} + className={ + selectedFilter === filter + ? 'bg-zinc-800' + : 'hover:!bg-zinc-600/50 hover:!text-white' + } + > + {filter} + + ))} + + + + + + + + + + + {isAuthenticated && isOwnProfile && ( + + + + )} +
+
+
+
+ ); } diff --git a/components/hackathons/overview/hackathonPrizes.tsx b/components/hackathons/overview/hackathonPrizes.tsx index 3a7a6720a..ef51897fd 100644 --- a/components/hackathons/overview/hackathonPrizes.tsx +++ b/components/hackathons/overview/hackathonPrizes.tsx @@ -1,6 +1,7 @@ 'use client'; import { PrizeTier } from '@/lib/api/hackathons'; +import { Trophy } from 'lucide-react'; import Image from 'next/image'; interface HackathonPrizesProps { @@ -11,7 +12,7 @@ interface HackathonPrizesProps { } export function HackathonPrizes({ - title = 'PRIZES', + title = 'Prize Tiers', totalPrizePool, otherPrizes, prizes, @@ -22,7 +23,15 @@ export function HackathonPrizes({ return (
-

{title}

+
+
+ + Prizes +
+
+

+ {title} +

{/* Wave background */}
diff --git a/components/hackathons/participants/hackathonParticipant.tsx b/components/hackathons/participants/hackathonParticipant.tsx index 169b17d67..2f7a166d4 100644 --- a/components/hackathons/participants/hackathonParticipant.tsx +++ b/components/hackathons/participants/hackathonParticipant.tsx @@ -3,6 +3,7 @@ import { useParticipants } from '@/hooks/hackathon/use-participants'; import ParticipantsFilter from './participantFilter'; import { ParticipantAvatar } from './participantAvatar'; +import Link from 'next/link'; export const HackathonParticipants = () => { const { @@ -58,7 +59,13 @@ export const HackathonParticipants = () => { {/* Participants Grid */}
{participants.map(participant => ( - + + + ))}
diff --git a/components/hackathons/participants/profileCard.tsx b/components/hackathons/participants/profileCard.tsx index 7bada8419..b187c88ec 100644 --- a/components/hackathons/participants/profileCard.tsx +++ b/components/hackathons/participants/profileCard.tsx @@ -1,12 +1,13 @@ 'use client'; - -import { useState } from 'react'; +import { useState, useMemo } 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 { MessageCircle, Users } from 'lucide-react'; import { format } from 'date-fns'; +import { useHackathonData } from '@/lib/providers/hackathonProvider'; +import Link from 'next/link'; interface ProfileCardProps { participant: Participant; @@ -14,6 +15,24 @@ interface ProfileCardProps { export function ProfileCard({ participant }: ProfileCardProps) { const [isFollowing, setIsFollowing] = useState(false); + const { participants } = useHackathonData(); + const teamMembers = useMemo(() => { + if (participant.role === 'leader' && participant.teamId) { + return participants.filter( + p => p.teamId === participant.teamId && p.id !== participant.id // Exclude the current participant (leader) + ); + } + return []; + }, [participant, participants]); + + // Check if this participant is a team leader + const isTeamLeader = participant.role === 'leader' && participant.teamId; + + // Check if this participant is a team member (but not leader) + // const isTeamMember = participant.role === 'member' && participant.teamId; + + // Check if this is an individual participant + // const isIndividual = !participant.teamId || participant.isIndividual; return (
@@ -40,7 +59,7 @@ export function ProfileCard({ participant }: ProfileCardProps) { > @@ -54,7 +73,23 @@ export function ProfileCard({ participant }: ProfileCardProps) {
- {/* Action Buttons */} + {/* Team Info */} + {participant.teamName && ( +
+ +
+

+ {participant.teamName} +

+

+ {isTeamLeader + ? `${teamMembers.length + 1} member${teamMembers.length + 1 !== 1 ? 's' : ''} • Team Leader` + : 'Team Member'} +

+
+
+ )} +
- {/* Role */} - {participant.role && ( -
- - {participant.role} - + {isTeamLeader && teamMembers.length > 0 && ( +
+
+ {teamMembers.map(member => ( + +
+ {member.username} +
+ + ))} +
)} diff --git a/components/landing-page/navbar.tsx b/components/landing-page/navbar.tsx index a4c5a5899..9c45ea9e0 100644 --- a/components/landing-page/navbar.tsx +++ b/components/landing-page/navbar.tsx @@ -8,6 +8,8 @@ import { User, LogOut, Settings, + Sparkles, + Wand2, } from 'lucide-react'; import Image from 'next/image'; import { useState, useMemo } from 'react'; @@ -28,6 +30,7 @@ import CreateProjectModal from './project/CreateProjectModal'; import { useProtectedAction } from '@/hooks/use-protected-action'; import WalletRequiredModal from '@/components/wallet/WalletRequiredModal'; import { WalletButton } from '../wallet/WalletButton'; +import { NotificationBell } from '../notifications/NotificationBell'; // Constants const BRAND_COLOR = '#a7f950'; @@ -78,7 +81,8 @@ export function Navbar() { {/* Desktop Actions - Updated with better alignment */} -
+ {/*
*/} +
{isLoading ? ( ) : isAuthenticated ? ( @@ -94,6 +98,7 @@ export function Navbar() { isLoading={isLoading} user={user} /> + {/*
*/}
@@ -134,7 +139,7 @@ function DesktopMenu({ }) { return (