Skip to content

Commit b01a431

Browse files
authored
Fix hackathon (#374)
* fix: modify api.ts * fix: remove google auth buttom * fix: fixes responsive fixes on organization * fix: minor fixes * fix: modify create organization * fix: modify create organization * fix: fix organization permission * fix: merge into main * feat: hackathon overview page * feat: hackathon overview page * feat: implement participant overview * feat: implement participant overview * feat: implement resources tab * feat: implement the submission tab * feat: implement comment tab * fix: implement provider for hackathon * fix: implement provider for hackathon * fix: minor fixes * fix: hackathon banner * fix: hackathon banner * fix: fix organization page * fix: fix organization page * fix: use transform * fix: add tagline * fix: add tagline * fix: minor fixes * fix: minor fixes * fix: fix timeline and prizes * fix: correct timeline events * fix: implement registration deadline policy * fix: implement registration deadline policy * feat: implement leave hackathon * feat: implement leave hackathon * fix: delete hackathon * fix: implement invite participants * fix: implement participant profile viewing * feat: fetch participants team * fix: redesign hackathon banner * fix: fix hackthon card * fix: fix search bar in blog page * fix: fix search bar in blog page * fix: fix search bar in blog page * fix: fix error in fetching team posts * feat: implement create team, get my team * feat: implement create team, get my team * feat: implement hackathon project submission flow * feat: implement voting for submission * fix: team formation updates * fix: implement team invitation
1 parent 2da82c3 commit b01a431

16 files changed

Lines changed: 1890 additions & 375 deletions

File tree

app/(landing)/hackathons/[slug]/team-invitations/[token]/accept/page.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,14 @@ const AcceptTeamInvitationPage = () => {
6060
setError(null);
6161

6262
try {
63-
const response = await acceptTeamInvitation(hackathonSlug, {
64-
token: invitationToken,
65-
});
63+
const response = await acceptTeamInvitation(
64+
hackathonSlug,
65+
invitationToken
66+
);
6667

6768
if (response.success) {
68-
setSuccessTeamName(response.data.teamName);
69-
toast.success(`Successfully joined ${response.data.teamName}!`);
69+
setSuccessTeamName(response.data?.teamId || 'the team');
70+
toast.success('Successfully joined the team!');
7071
setTimeout(() => {
7172
router.push(`/hackathons/${hackathonSlug}`);
7273
}, 2000);
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
'use client';
2+
import { useParams, useRouter, useSearchParams } from 'next/navigation';
3+
import React, { useEffect, useState } from 'react';
4+
import { Users, AlertCircle, Loader2, XCircle } from 'lucide-react';
5+
import { useAuthStatus } from '@/hooks/use-auth';
6+
import { rejectTeamInvitation } from '@/lib/api/hackathons';
7+
import { toast } from 'sonner';
8+
9+
const RejectTeamInvitationPage = () => {
10+
const params = useParams();
11+
const searchParams = useSearchParams();
12+
const router = useRouter();
13+
const { isAuthenticated, isLoading: authLoading } = useAuthStatus();
14+
15+
const [isProcessing, setIsProcessing] = useState(false);
16+
const [error, setError] = useState<string | null>(null);
17+
const [success, setSuccess] = useState(false);
18+
19+
const hackathonSlug = params.slug as string;
20+
const token = params.token as string;
21+
const redirectToken = searchParams.get('token');
22+
const invitationToken = token || redirectToken;
23+
24+
useEffect(() => {
25+
if (!invitationToken) {
26+
router.push(`/hackathons/${hackathonSlug}`);
27+
return;
28+
}
29+
30+
if (!isAuthenticated && !authLoading) {
31+
redirectToAuth();
32+
}
33+
}, [isAuthenticated, authLoading, invitationToken, hackathonSlug]);
34+
35+
const redirectToAuth = () => {
36+
const currentUrl = window.location.href;
37+
const encodedRedirect = encodeURIComponent(currentUrl);
38+
router.push(`/auth/login?redirect=${encodedRedirect}`);
39+
};
40+
41+
const handleReject = async () => {
42+
if (!invitationToken || !hackathonSlug) return;
43+
44+
setIsProcessing(true);
45+
setError(null);
46+
47+
try {
48+
const response = await rejectTeamInvitation(
49+
hackathonSlug,
50+
invitationToken
51+
);
52+
53+
if (response.success) {
54+
setSuccess(true);
55+
toast.success('Invitation declined');
56+
setTimeout(() => {
57+
router.push(`/hackathons/${hackathonSlug}`);
58+
}, 2000);
59+
}
60+
} catch (err: any) {
61+
const errorMessage = err?.message || 'Failed to decline invitation';
62+
setError(errorMessage);
63+
64+
if (err?.status === 403) {
65+
toast.error('Authentication required');
66+
redirectToAuth();
67+
} else if (err?.status === 404) {
68+
toast.error('Invitation not found or has expired');
69+
} else {
70+
toast.error(errorMessage);
71+
}
72+
} finally {
73+
setIsProcessing(false);
74+
}
75+
};
76+
77+
// Loading authentication state
78+
if (authLoading) {
79+
return (
80+
<div className='flex min-h-screen items-center justify-center bg-linear-to-br from-gray-900 via-gray-800 to-black p-4'>
81+
<div className='w-full max-w-md'>
82+
<div className='rounded-2xl border border-white/10 bg-gray-800/50 p-8 shadow-2xl backdrop-blur-sm'>
83+
<div className='mb-6 flex justify-center'>
84+
<div className='relative'>
85+
<div className='flex h-20 w-20 items-center justify-center rounded-full border border-[#a7f950]/20 bg-[#a7f950]/10'>
86+
<Users className='h-10 w-10 text-[#a7f950]' />
87+
</div>
88+
<div className='absolute -top-1 -right-1'>
89+
<Loader2 className='h-6 w-6 animate-spin text-[#a7f950]' />
90+
</div>
91+
</div>
92+
</div>
93+
94+
<h1 className='mb-2 text-center text-2xl font-bold text-white'>
95+
Verifying Invitation
96+
</h1>
97+
98+
<p className='mb-6 text-center text-white/70'>
99+
Please wait while we verify your invitation...
100+
</p>
101+
102+
<div className='flex justify-center'>
103+
<Loader2 className='h-6 w-6 animate-spin text-[#a7f950]' />
104+
</div>
105+
</div>
106+
</div>
107+
</div>
108+
);
109+
}
110+
111+
// Error state
112+
if (error && !success) {
113+
return (
114+
<div className='flex min-h-screen items-center justify-center bg-linear-to-br from-gray-900 via-gray-800 to-black p-4'>
115+
<div className='w-full max-w-md'>
116+
<div className='rounded-2xl border border-red-500/20 bg-gray-800/50 p-8 shadow-2xl backdrop-blur-sm'>
117+
<div className='mb-6 flex justify-center'>
118+
<div className='flex h-20 w-20 items-center justify-center rounded-full border border-red-500/20 bg-red-500/10'>
119+
<AlertCircle className='h-10 w-10 text-red-500' />
120+
</div>
121+
</div>
122+
123+
<h1 className='mb-2 text-center text-2xl font-bold text-white'>
124+
Unable to Process Invitation
125+
</h1>
126+
127+
<p className='mb-6 text-center text-white/70'>{error}</p>
128+
129+
<button
130+
onClick={() => router.push(`/hackathons/${hackathonSlug}`)}
131+
className='w-full rounded-lg bg-[#a7f950] px-6 py-3 font-semibold text-black transition-all hover:bg-[#8fd93f]'
132+
>
133+
Return to Hackathon
134+
</button>
135+
</div>
136+
</div>
137+
</div>
138+
);
139+
}
140+
141+
// Success state
142+
if (success) {
143+
return (
144+
<div className='flex min-h-screen items-center justify-center bg-linear-to-br from-gray-900 via-gray-800 to-black p-4'>
145+
<div className='w-full max-w-md'>
146+
<div className='rounded-2xl border border-white/10 bg-gray-800/50 p-8 shadow-2xl backdrop-blur-sm'>
147+
<div className='mb-6 flex justify-center'>
148+
<div className='flex h-20 w-20 items-center justify-center rounded-full border border-gray-500/20 bg-gray-500/10'>
149+
<XCircle className='h-10 w-10 text-gray-400' />
150+
</div>
151+
</div>
152+
153+
<h1 className='mb-2 text-center text-2xl font-bold text-white'>
154+
Invitation Declined
155+
</h1>
156+
157+
<p className='mb-6 text-center text-white/70'>
158+
You've declined this team invitation. Redirecting...
159+
</p>
160+
161+
<div className='flex justify-center'>
162+
<Loader2 className='h-6 w-6 animate-spin text-[#a7f950]' />
163+
</div>
164+
</div>
165+
</div>
166+
</div>
167+
);
168+
}
169+
170+
// Main decline confirmation
171+
return (
172+
<div className='flex min-h-screen items-center justify-center bg-linear-to-br from-gray-900 via-gray-800 to-black p-4'>
173+
<div className='w-full max-w-md'>
174+
<div className='rounded-2xl border border-white/10 bg-gray-800/50 p-8 shadow-2xl backdrop-blur-sm'>
175+
<div className='mb-6 flex justify-center'>
176+
<div className='flex h-20 w-20 items-center justify-center rounded-full border border-red-500/20 bg-red-500/10'>
177+
<XCircle className='h-10 w-10 text-red-500' />
178+
</div>
179+
</div>
180+
181+
<h1 className='mb-2 text-center text-2xl font-bold text-white'>
182+
Decline Team Invitation
183+
</h1>
184+
185+
<p className='mb-8 text-center text-white/70'>
186+
Are you sure you want to decline this team invitation? This action
187+
cannot be undone.
188+
</p>
189+
190+
<div className='space-y-3'>
191+
<button
192+
onClick={handleReject}
193+
disabled={isProcessing}
194+
className='w-full rounded-lg bg-red-500 px-6 py-3 font-semibold text-white transition-all hover:bg-red-600 disabled:opacity-50'
195+
>
196+
{isProcessing ? (
197+
<span className='flex items-center justify-center gap-2'>
198+
<Loader2 className='h-5 w-5 animate-spin' />
199+
Declining...
200+
</span>
201+
) : (
202+
<span className='flex items-center justify-center gap-2'>
203+
<XCircle className='h-5 w-5' />
204+
Decline Invitation
205+
</span>
206+
)}
207+
</button>
208+
209+
<button
210+
onClick={() => router.push(`/hackathons/${hackathonSlug}`)}
211+
className='w-full rounded-lg border border-white/10 bg-transparent px-6 py-3 font-semibold text-white transition-all hover:bg-white/5'
212+
>
213+
Cancel
214+
</button>
215+
</div>
216+
</div>
217+
</div>
218+
</div>
219+
);
220+
};
221+
222+
export default RejectTeamInvitationPage;

app/me/settings/SettingsContent.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const SettingsContent = () => {
3232
);
3333
}
3434
return (
35-
<div className=''>
35+
<div className='p-10'>
3636
<div className=''>
3737
{/* Header */}
3838
<div className='mb-8'>

components/hackathons/participants/participantAvatar.tsx

Lines changed: 83 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -10,63 +10,100 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
1010
import { ProfileCard } from './profileCard';
1111
import type { ParticipantDisplay } from '@/lib/api/hackathons/index';
1212
import Image from 'next/image';
13+
import { useState, useMemo } from 'react';
14+
import { useParticipants } from '@/hooks/hackathon/use-participants';
15+
import { useAuthStatus } from '@/hooks/use-auth';
16+
import { useHackathonData } from '@/lib/providers/hackathonProvider';
17+
import { InviteUserModal } from '../team-formation/InviteUserModal';
1318

1419
interface ParticipantAvatarProps {
1520
participant: ParticipantDisplay;
1621
}
1722

1823
export function ParticipantAvatar({ participant }: ParticipantAvatarProps) {
24+
const [isInviteModalOpen, setIsInviteModalOpen] = useState(false);
25+
const { allParticipants } = useParticipants();
26+
const { user } = useAuthStatus();
27+
const { currentHackathon } = useHackathonData();
28+
29+
const currentUserParticipant = useMemo(() => {
30+
if (!user) return null;
31+
const currentUsername = user.username || (user.profile as any)?.username;
32+
const currentUserId = user.id || (user as any).userId;
33+
34+
return allParticipants.find(
35+
p =>
36+
(currentUsername && p.username === currentUsername) ||
37+
(currentUserId && p.userId === currentUserId)
38+
);
39+
}, [user, allParticipants]);
40+
1941
return (
20-
<TooltipProvider delayDuration={200}>
21-
<Tooltip>
22-
<TooltipTrigger asChild>
23-
<div className='group inline-flex cursor-pointer items-center gap-2'>
24-
<div className='relative flex-shrink-0'>
25-
<Avatar className='h-12 w-12 border-2 border-gray-700 transition-all duration-300 group-hover:scale-110 group-hover:border-[#a7f950]'>
26-
<AvatarImage
27-
src={participant.avatar}
28-
alt={participant.username}
29-
className='object-cover'
30-
/>
31-
{/* <AvatarFallback className="bg-gray-800 text-gray-400 text-xs"> */}
32-
{/* {participant.username.slice(0, 2).toUpperCase()} */}
33-
<AvatarFallback>
34-
<Image
35-
src='https://i.pravatar.cc/150?img=10'
36-
alt='avatar'
37-
width={116}
38-
height={22}
39-
className='h-full w-full object-cover'
42+
<>
43+
<TooltipProvider delayDuration={200}>
44+
<Tooltip>
45+
<TooltipTrigger asChild>
46+
<div className='group inline-flex cursor-pointer items-center gap-2'>
47+
<div className='relative flex-shrink-0'>
48+
<Avatar className='h-12 w-12 border-2 border-gray-700 transition-all duration-300 group-hover:scale-110 group-hover:border-[#a7f950]'>
49+
<AvatarImage
50+
src={participant.avatar}
51+
alt={participant.username}
52+
className='object-cover'
53+
/>
54+
{/* <AvatarFallback className="bg-gray-800 text-gray-400 text-xs"> */}
55+
{/* {participant.username.slice(0, 2).toUpperCase()} */}
56+
<AvatarFallback className='bg-zinc-800 text-xs text-zinc-400'>
57+
{participant.name?.charAt(0) ||
58+
participant.username?.charAt(0) ||
59+
'U'}
60+
</AvatarFallback>
61+
</Avatar>
62+
63+
{/* Green status dot if submitted */}
64+
{participant.hasSubmitted && (
65+
<div
66+
className='bg-primary/45 absolute -right-0.5 -bottom-0.5 h-3 w-3 rounded-full border-2 border-gray-900'
67+
title='Project submitted'
4068
/>
41-
</AvatarFallback>
42-
{/* </AvatarFallback> */}
43-
</Avatar>
69+
)}
70+
</div>
4471

45-
{/* Green status dot if submitted */}
46-
{participant.hasSubmitted && (
47-
<div
48-
className='bg-primary/45 absolute -right-0.5 -bottom-0.5 h-3 w-3 rounded-full border-2 border-gray-900'
49-
title='Project submitted'
50-
/>
51-
)}
72+
<span className='text-sm text-gray-200 transition-colors group-hover:text-[#a7f950]'>
73+
{participant.username
74+
? participant.username.slice(0, 1).toUpperCase() +
75+
participant.username.slice(1)
76+
: participant.name || 'User'}
77+
</span>
5278
</div>
79+
</TooltipTrigger>
5380

54-
<span className='text-sm text-gray-200 transition-colors group-hover:text-[#a7f950]'>
55-
{participant.username.slice(0, 1).toUpperCase() +
56-
participant.username.slice(1)}
57-
</span>
58-
</div>
59-
</TooltipTrigger>
81+
<TooltipContent
82+
side='right'
83+
align='start'
84+
className='border-0 bg-transparent p-0 shadow-none'
85+
sideOffset={10}
86+
>
87+
<ProfileCard
88+
participant={participant}
89+
onInviteClick={() => setIsInviteModalOpen(true)}
90+
/>
91+
</TooltipContent>
92+
</Tooltip>
93+
</TooltipProvider>
6094

61-
<TooltipContent
62-
side='right'
63-
align='start'
64-
className='border-0 bg-transparent p-0 shadow-none'
65-
sideOffset={10}
66-
>
67-
<ProfileCard participant={participant} />
68-
</TooltipContent>
69-
</Tooltip>
70-
</TooltipProvider>
95+
{isInviteModalOpen &&
96+
currentUserParticipant?.teamId &&
97+
currentHackathon?.id && (
98+
<InviteUserModal
99+
open={isInviteModalOpen}
100+
onOpenChange={setIsInviteModalOpen}
101+
hackathonId={currentHackathon.id}
102+
teamId={currentUserParticipant.teamId}
103+
inviteeId={participant.username}
104+
inviteeName={participant.name}
105+
/>
106+
)}
107+
</>
71108
);
72109
}

0 commit comments

Comments
 (0)