diff --git a/app/(landing)/hackathons/[slug]/page.tsx b/app/(landing)/hackathons/[slug]/page.tsx index 959d07a6..43e983bb 100644 --- a/app/(landing)/hackathons/[slug]/page.tsx +++ b/app/(landing)/hackathons/[slug]/page.tsx @@ -19,6 +19,9 @@ import { toast } from 'sonner'; import type { Participant } from '@/lib/api/hackathons'; import { HackathonStickyCard } from '@/components/hackathons/hackathonStickyCard'; import { HackathonParticipants } from '@/components/hackathons/participants/hackathonParticipant'; +import { useCommentSystem } from '@/hooks/use-comment-system'; +import { CommentEntityType } from '@/types/comment'; +import { useTeamPosts } from '@/hooks/hackathon/use-team-posts'; export default function HackathonPage() { const router = useRouter(); @@ -38,6 +41,26 @@ export default function HackathonPage() { dateFormat: { month: 'short', day: 'numeric', year: 'numeric' }, }); + const hackathonId = params.slug as string; + const [activeTab, setActiveTab] = useState('overview'); + const [showRegisterModal, setShowRegisterModal] = useState(false); + + // Fetch discussion comments for count + const { comments: discussionComments } = useCommentSystem({ + entityType: CommentEntityType.HACKATHON, + entityId: hackathonId, + page: 1, + limit: 1000, + enabled: !!hackathonId, + }); + + // Fetch team posts for count + const { posts: teamPosts } = useTeamPosts({ + hackathonSlugOrId: hackathonId, + organizationId: currentHackathon?.organizationId, + autoFetch: !!hackathonId, + }); + const hackathonTabs = useMemo(() => { const hasParticipants = Array.isArray(currentHackathon?.participants) && @@ -77,11 +100,19 @@ export default function HackathonPage() { label: 'Submissions', badge: submissions.filter(p => p.status === 'Approved').length, }, - { id: 'discussions', label: 'Discussions' }, + { + id: 'discussions', + label: 'Discussions', + badge: discussionComments.comments.length, + }, ]; if (isTeamHackathon && isTabEnabled) { - tabs.push({ id: 'team-formation', label: 'Find Team' }); + tabs.push({ + id: 'team-formation', + label: 'Find Team', + badge: teamPosts.length, + }); } return tabs; @@ -90,13 +121,13 @@ export default function HackathonPage() { currentHackathon?.resources, currentHackathon?.participantType, currentHackathon?.enabledTabs, + currentHackathon?.organizationId, submissions, + discussionComments.comments.length, + teamPosts.length, + hackathonId, ]); - const hackathonId = params.slug as string; - const [activeTab, setActiveTab] = useState('overview'); - const [showRegisterModal, setShowRegisterModal] = useState(false); - // Refresh hackathon data const refreshHackathonData = useCallback(async () => { if (hackathonId && refreshCurrentHackathon) { diff --git a/app/(landing)/hackathons/[slug]/team-invitations/[token]/accept/page.tsx b/app/(landing)/hackathons/[slug]/team-invitations/[token]/accept/page.tsx index 07ee8742..86a2d3d0 100644 --- a/app/(landing)/hackathons/[slug]/team-invitations/[token]/accept/page.tsx +++ b/app/(landing)/hackathons/[slug]/team-invitations/[token]/accept/page.tsx @@ -2,17 +2,27 @@ import { useParams, useRouter, useSearchParams } from 'next/navigation'; import React, { useEffect, useState } from 'react'; import { - Mail, Users, Shield, AlertCircle, Loader2, CheckCircle2, ArrowRight, + Mail, } from 'lucide-react'; import { useAuthStatus } from '@/hooks/use-auth'; import { acceptTeamInvitation } from '@/lib/api/hackathons'; import { toast } from 'sonner'; +import { Button } from '@/components/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; const AcceptTeamInvitationPage = () => { const params = useParams(); @@ -20,19 +30,19 @@ const AcceptTeamInvitationPage = () => { 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; + const [isProcessing, setIsProcessing] = useState(false); + const [error, setError] = useState(null); + const [successTeamName, setSuccessTeamName] = useState(''); + const [showAcceptButton, setShowAcceptButton] = useState(false); + useEffect(() => { if (!invitationToken) { - router.push(`/hackathons/${hackathonSlug}`); + router.push('/hackathons'); return; } @@ -45,7 +55,7 @@ const AcceptTeamInvitationPage = () => { if (!isAuthenticated && !authLoading) { redirectToAuth(); } - }, [isAuthenticated, authLoading, invitationToken, hackathonSlug]); + }, [isAuthenticated, authLoading, invitationToken]); const redirectToAuth = () => { const redirectUrl = `/hackathons/${hackathonSlug}/team-invitations/${invitationToken}/accept`; @@ -67,16 +77,21 @@ const AcceptTeamInvitationPage = () => { if (response.success) { setSuccessTeamName(response.data?.teamId || 'the team'); + // Use the slug from the response if available, otherwise go to hackathons list + const finalSlug = response.data?.invitation?.hackathon?.slug; toast.success('Successfully joined the team!'); setTimeout(() => { - router.push(`/hackathons/${hackathonSlug}`); + if (finalSlug) { + router.push(`/hackathons/${finalSlug}?tab=team-formation`); + } else { + router.push('/hackathons'); + } }, 2000); } } catch (err: any) { 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'); @@ -96,253 +111,167 @@ const AcceptTeamInvitationPage = () => { } }; - // Loading authentication state if (authLoading) { return ( -
-
-
-
-
-
- -
-
- -
-
+
+ + +
+
- -

- Verifying Invitation -

- -

+ 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 */} -
- - +
+ + +
+
-
-
+ Team Invitation + + You've been invited to join a team for this hackathon. + + + + + + Ready to join? + + By accepting this invitation, you'll be added to the team and + can start collaborating immediately. + + + + + + + +
); } - // 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 ? ( - - ) : ( - - )} -
+
+ + +
+ {isAlreadyMember ? ( + + ) : ( + + )}
- - {/* Title */} -

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

- - {/* Description */} -

{error}

- - {/* Additional info for wrong email */} + + {isAlreadyMember ? 'Already a Member' : 'Unable to Join'} + + {error} +
+ {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. -

-
-
-
+ + + Wrong Account + + Please sign in with the email address this invitation was sent + to. + + )} - - {/* Action buttons */} -
- {isWrongEmail ? ( - <> - - - - ) : ( - - )} -
-
-
+ Switch Account + + + + ) : ( + + )} + +
); } - // Success state (brief moment before redirect) if (successTeamName) { return ( -
-
-
- {/* Icon */} -
-
- -
+
+ + +
+
- - {/* Title */} -

- Successfully Joined! -

- - {/* Description */} -

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

- - {/* Progress bar */} -
-
+ Welcome! + + You've successfully joined {successTeamName}. Redirecting... + + + +
+
-
-
+ +
); } diff --git a/app/(landing)/hackathons/[slug]/team-invitations/[token]/reject/page.tsx b/app/(landing)/hackathons/[slug]/team-invitations/[token]/reject/page.tsx index 581e3474..7cbc7fcc 100644 --- a/app/(landing)/hackathons/[slug]/team-invitations/[token]/reject/page.tsx +++ b/app/(landing)/hackathons/[slug]/team-invitations/[token]/reject/page.tsx @@ -5,6 +5,15 @@ import { Users, AlertCircle, Loader2, XCircle } from 'lucide-react'; import { useAuthStatus } from '@/hooks/use-auth'; import { rejectTeamInvitation } from '@/lib/api/hackathons'; import { toast } from 'sonner'; +import { Button } from '@/components/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card'; const RejectTeamInvitationPage = () => { const params = useParams(); @@ -12,25 +21,25 @@ const RejectTeamInvitationPage = () => { const router = useRouter(); const { isAuthenticated, isLoading: authLoading } = useAuthStatus(); - const [isProcessing, setIsProcessing] = useState(false); - const [error, setError] = useState(null); - const [success, setSuccess] = useState(false); - const hackathonSlug = params.slug as string; const token = params.token as string; const redirectToken = searchParams.get('token'); const invitationToken = token || redirectToken; + const [isProcessing, setIsProcessing] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + useEffect(() => { if (!invitationToken) { - router.push(`/hackathons/${hackathonSlug}`); + router.push('/hackathons'); return; } if (!isAuthenticated && !authLoading) { redirectToAuth(); } - }, [isAuthenticated, authLoading, invitationToken, hackathonSlug]); + }, [isAuthenticated, authLoading, invitationToken]); const redirectToAuth = () => { const currentUrl = window.location.href; @@ -52,9 +61,15 @@ const RejectTeamInvitationPage = () => { if (response.success) { setSuccess(true); + // Use the slug from the response if available, otherwise go to hackathons list + const finalSlug = response.data?.invitation?.hackathon?.slug; toast.success('Invitation declined'); setTimeout(() => { - router.push(`/hackathons/${hackathonSlug}`); + if (finalSlug) { + router.push(`/hackathons/${finalSlug}`); + } else { + router.push('/hackathons'); + } }, 2000); } } catch (err: any) { @@ -74,147 +89,115 @@ const RejectTeamInvitationPage = () => { } }; - // Loading authentication state if (authLoading) { return ( -
-
-
-
-
-
- -
-
- -
-
+
+ + +
+
- -

- Verifying Invitation -

- -

+ Verifying Invitation + Please wait while we verify your invitation... -

- -
- -
-
-
+ + +
); } - // Error state if (error && !success) { return ( -
-
-
-
-
- -
+
+ + +
+
- -

- Unable to Process Invitation -

- -

{error}

- - -
-
+ Return to Hackathons + + +
); } - // Success state if (success) { return ( -
-
-
-
-
- -
+
+ + +
+
- -

- Invitation Declined -

- -

+ Invitation Declined + You've declined this team invitation. Redirecting... -

- + +
+
- +
-
-
+ +
); } - // Main decline confirmation return ( -
-
-
-
-
- -
+
+ + +
+
- -

- Decline Team Invitation -

- -

+ Decline Team Invitation + Are you sure you want to decline this team invitation? This action cannot be undone. -

- -
- - - -
-
-
+ + + + + + +
); }; diff --git a/app/(landing)/organizations/[id]/hackathons/[hackathonId]/submissions/page.tsx b/app/(landing)/organizations/[id]/hackathons/[hackathonId]/submissions/page.tsx index 9846a9de..7e02bb40 100644 --- a/app/(landing)/organizations/[id]/hackathons/[hackathonId]/submissions/page.tsx +++ b/app/(landing)/organizations/[id]/hackathons/[hackathonId]/submissions/page.tsx @@ -12,6 +12,7 @@ import { SubmissionsManagement } from '@/components/organization/hackathons/subm export default function SubmissionsPage() { const params = useParams(); const hackathonId = params.hackathonId as string; + const organizationId = params.id as string; const { submissions, @@ -81,6 +82,8 @@ export default function SubmissionsPage() { onFilterChange={updateFilters} onPageChange={goToPage} onRefresh={refresh} + organizationId={organizationId} + hackathonId={hackathonId} /> )}
diff --git a/components/hackathons/submissions/submissionTab.tsx b/components/hackathons/submissions/submissionTab.tsx index 2784c649..7460dbaf 100644 --- a/components/hackathons/submissions/submissionTab.tsx +++ b/components/hackathons/submissions/submissionTab.tsx @@ -123,7 +123,13 @@ const SubmissionTab: React.FC = ({
- {submissions.filter(p => p.status === 'Approved').length} + { + submissions.filter( + p => + p.status?.toLowerCase() === 'shortlisted' || + p.status === 'Approved' + ).length + } {' '} total approved submissions @@ -215,7 +221,7 @@ const SubmissionTab: React.FC = ({ placeholder='Search by project name or participant...' value={searchTerm} onChange={e => setSearchTerm(e.target.value)} - className='w-full rounded-lg border-gray-900 bg-[#030303] py-3 pr-4 pl-10 text-base text-white placeholder-gray-400 focus:border-gray-400 focus:ring-1 focus:ring-gray-400' + className='bg-backgound-main-bg 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' />
@@ -251,7 +257,11 @@ const SubmissionTab: React.FC = ({ submitterName='You' category={mySubmission.category} status={ - mySubmission.status === 'submitted' ? 'Pending' : 'Approved' + mySubmission.status?.toLowerCase() === 'shortlisted' + ? 'Approved' + : mySubmission.status?.toLowerCase() === 'disqualified' + ? 'Rejected' + : 'Pending' } upvotes={ typeof mySubmission.votes === 'number' ? mySubmission.votes : 0 @@ -274,36 +284,46 @@ const SubmissionTab: React.FC = ({ )} {submissions - .filter( - submission => - // Filter out approved submissions, and optionally filter out my own submission if it's already shown as pinned - submission.status === 'Approved' && - (mySubmission ? submission._id !== mySubmission.id : true) + .filter(submission => + // Filter out my own submission if it's already shown as pinned + mySubmission ? submission._id !== mySubmission.id : true ) - .map((submission, index) => ( - - handleViewSubmission((submission as { _id?: string })?._id) - } - onUpvoteClick={() => { - if (!isAuthenticated) { - return; - } - handleUpvoteSubmission((submission as { _id?: string })?._id); - }} - onCommentClick={() => { - if (!isAuthenticated) { - return; + .map((submission, index) => { + const status = + submission.status?.toLowerCase() === 'shortlisted' + ? 'Approved' + : submission.status?.toLowerCase() === 'disqualified' + ? 'Rejected' + : 'Pending'; + + return ( + + handleViewSubmission((submission as { _id?: string })?._id) } - handleCommentSubmission( - (submission as { _id?: string })?._id - ); - }} - /> - ))} + onUpvoteClick={() => { + if (!isAuthenticated) { + return; + } + handleUpvoteSubmission( + (submission as { _id?: string })?._id + ); + }} + onCommentClick={() => { + if (!isAuthenticated) { + return; + } + handleCommentSubmission( + (submission as { _id?: string })?._id + ); + }} + /> + ); + })}
) : (
@@ -366,7 +386,7 @@ const SubmissionTab: React.FC = ({ } }} > - + Delete Submission diff --git a/components/hackathons/team-formation/MyInvitationsList.tsx b/components/hackathons/team-formation/MyInvitationsList.tsx index 0abc31da..4ccb0bc9 100644 --- a/components/hackathons/team-formation/MyInvitationsList.tsx +++ b/components/hackathons/team-formation/MyInvitationsList.tsx @@ -57,6 +57,8 @@ export function MyInvitationsList({ hackathonId }: MyInvitationsListProps) { isProcessing: isProcessingReceived, acceptInvite, rejectInvite, + error: receivedError, + fetchMyInvitations, } = useMyTeamInvitations(hackathonId); // Sent Invitations (only if leader) @@ -65,6 +67,8 @@ export function MyInvitationsList({ hackathonId }: MyInvitationsListProps) { isLoading: isLoadingSent, cancelInvite, isInviting: isProcessingSent, + error: sentError, + fetchInvitations, } = useTeamInvitations({ hackathonId, teamId: teamId || '', @@ -76,6 +80,7 @@ export function MyInvitationsList({ hackathonId }: MyInvitationsListProps) { activeTab === 'received' ? isLoadingReceived : isLoadingSent; const invitations = activeTab === 'received' ? receivedInvitations : sentInvitations; + const error = activeTab === 'received' ? receivedError : sentError; const getStatusVariant = (status: string) => { switch (status.toLowerCase()) { @@ -155,6 +160,26 @@ export function MyInvitationsList({ hackathonId }: MyInvitationsListProps) {
+ ) : error ? ( + + + +

Failed to load invitations

+

{error}

+ +
+
) : invitations.length === 0 ? ( diff --git a/components/hackathons/team-formation/TeamDetailsSheet.tsx b/components/hackathons/team-formation/TeamDetailsSheet.tsx index fe2f21bb..0604ab3c 100644 --- a/components/hackathons/team-formation/TeamDetailsSheet.tsx +++ b/components/hackathons/team-formation/TeamDetailsSheet.tsx @@ -10,15 +10,18 @@ import { ExternalLink, Edit, Pin, + Check, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { ScrollArea } from '@/components/ui/scroll-area'; +import { Switch } from '@/components/ui/switch'; import BoundlessSheet from '@/components/sheet/boundless-sheet'; import { type TeamRecruitmentPost, getTeamPostDetails, + toggleRoleHired, } from '@/lib/api/hackathons'; import { formatDistanceToNow } from 'date-fns'; import { useAuthStatus } from '@/hooks/use-auth'; @@ -132,6 +135,8 @@ export function TeamDetailsSheet({ const [isLoading, setIsLoading] = useState(false); // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars const [error, setError] = useState(null); + const [hiredRoles, setHiredRoles] = useState>(new Set()); + const [togglingRole, setTogglingRole] = useState(null); const isLeader = post.leaderId === user?.id; @@ -139,6 +144,14 @@ export function TeamDetailsSheet({ if (open && initialPost) { setPost(initialPost); + // Initialize hired roles from rolesStatus + if (initialPost.rolesStatus) { + const hired = new Set( + initialPost.rolesStatus.filter(r => r.hired).map(r => r.skill) + ); + setHiredRoles(hired); + } + // Fetch fresh details const loadDetails = async () => { setIsLoading(true); @@ -151,6 +164,13 @@ export function TeamDetailsSheet({ ); if (response.success && response.data) { setPost(response.data); + // Update hired roles from fresh data + if (response.data.rolesStatus) { + const hired = new Set( + response.data.rolesStatus.filter(r => r.hired).map(r => r.skill) + ); + setHiredRoles(hired); + } } } catch { setError('Failed to load latest team details'); @@ -192,6 +212,57 @@ export function TeamDetailsSheet({ } }; + const handleToggleRoleHired = async (skill: string) => { + if (!isLeader || togglingRole) return; + + setTogglingRole(skill); + const wasHired = hiredRoles.has(skill); + + // Optimistic update + setHiredRoles(prev => { + const next = new Set(prev); + if (wasHired) { + next.delete(skill); + } else { + next.add(skill); + } + return next; + }); + + try { + const response = await toggleRoleHired( + hackathonSlugOrId, + post.id, + { skill }, + organizationId + ); + + if (response.success) { + toast.success( + response.data.hired + ? `Marked "${skill}" as filled` + : `Marked "${skill}" as open` + ); + } + } catch (err: any) { + // Rollback on error + setHiredRoles(prev => { + const next = new Set(prev); + if (wasHired) { + next.add(skill); + } else { + next.delete(skill); + } + return next; + }); + + const errorMessage = err?.message || 'Failed to update role status'; + toast.error(errorMessage); + } finally { + setTogglingRole(null); + } + }; + return (
@@ -279,15 +350,50 @@ export function TeamDetailsSheet({ Looking For -
- {post.lookingFor.map((role, index) => ( - - {role} - - ))} +
+ {post.lookingFor.map((role, index) => { + const roleSkill = typeof role === 'string' ? role : role; + const isHired = hiredRoles.has(roleSkill); + const isToggling = togglingRole === roleSkill; + + return ( +
+ + {roleSkill} + + {isHired && ( + + + Filled + + )} + {isLeader && ( +
+ + {isHired ? 'Filled' : 'Open'} + + + handleToggleRoleHired(roleSkill) + } + disabled={isToggling} + className='data-[state=checked]:bg-[#A7F950]' + /> +
+ )} +
+ ); + })}
)} diff --git a/components/hackathons/team-formation/TeamFormationTab.tsx b/components/hackathons/team-formation/TeamFormationTab.tsx index cc3547fc..7cf89e71 100644 --- a/components/hackathons/team-formation/TeamFormationTab.tsx +++ b/components/hackathons/team-formation/TeamFormationTab.tsx @@ -52,6 +52,8 @@ export function TeamFormationTab({ fetchPosts, deletePost, trackContact, + fetchMyTeam, + fetchMyPosts, } = useTeamPosts({ hackathonSlugOrId: hackathonId, organizationId, @@ -316,6 +318,11 @@ export function TeamFormationTab({ onClick={handlePostClick} onTrackContact={trackContact} isPinned={true} + onLeaveSuccess={() => { + fetchMyTeam(); + fetchPosts(); + fetchMyPosts(); // Refresh my posts too + }} /> )} @@ -332,6 +339,11 @@ export function TeamFormationTab({ onDeleteClick={handleDeleteClick} onClick={handlePostClick} onTrackContact={trackContact} + onLeaveSuccess={() => { + fetchMyTeam(); + fetchPosts(); + fetchMyPosts(); + }} /> ))}
diff --git a/components/hackathons/team-formation/TeamRecruitmentPostCard.tsx b/components/hackathons/team-formation/TeamRecruitmentPostCard.tsx index 94e7737c..05979a5f 100644 --- a/components/hackathons/team-formation/TeamRecruitmentPostCard.tsx +++ b/components/hackathons/team-formation/TeamRecruitmentPostCard.tsx @@ -21,6 +21,19 @@ import { import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { type TeamRecruitmentPost } from '@/lib/api/hackathons'; import { toast } from 'sonner'; +import { useAuthStatus } from '@/hooks/use-auth'; +import { useLeaveTeam } from '@/hooks/hackathon/use-leave-team'; +import { LogOut, Loader2 } from 'lucide-react'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog'; interface TeamRecruitmentPostCardProps { post: TeamRecruitmentPost; @@ -31,6 +44,7 @@ interface TeamRecruitmentPostCardProps { isMyPost?: boolean; onTrackContact?: (postId: string) => void; isPinned?: boolean; + onLeaveSuccess?: () => void; } const getContactMethodIcon = ( @@ -185,12 +199,44 @@ export function TeamRecruitmentPostCard({ isMyPost, onTrackContact, isPinned = false, + onLeaveSuccess, }: TeamRecruitmentPostCardProps) { const ContactIcon = getContactMethodIcon( post.contactMethod, post.contactInfo ); + const { user } = useAuthStatus(); + const [showLeaveDialog, setShowLeaveDialog] = React.useState(false); + + const isMember = user ? post.members.some(m => m.userId === user.id) : false; + // Leader is also a member, so isMember is true for leader. Check specifically for leader capability + const isLeader = user ? post.leaderId === user.id : false; + + const { leaveTeam, isLeaving } = useLeaveTeam({ + hackathonSlugOrId: post.hackathonId, + teamId: post.id, + organizationId: post.organizationId, + onSuccess: onLeaveSuccess, + }); + + const handleLeaveClick = (e: React.MouseEvent) => { + e.stopPropagation(); + + // Check leader restrictions + if (isLeader && post.members.length > 1) { + toast.error('You must transfer leadership before leaving the team.'); + return; + } + + setShowLeaveDialog(true); + }; + + const confirmLeave = async () => { + await leaveTeam(); + setShowLeaveDialog(false); + }; + const handleEditClick = (e: React.MouseEvent) => { e.stopPropagation(); onEditClick?.(post); @@ -271,7 +317,8 @@ export function TeamRecruitmentPostCard({ > {post.isOpen ? 'Open' : 'Closed'} - {isMyPost && ( + + {(isMyPost || isMember) && (
+ + + + Leave Team + + Are you sure you want to leave this team? + {isLeader && + post.members.length === 1 && + " Since you're the only member, the team might be dissolved."} + + + + + Cancel + + + {isLeaving ? ( + <> + + Leaving... + + ) : ( + 'Leave Team' + )} + + + + + {/* Project Name */}

{post.teamName} diff --git a/components/organization/hackathons/submissions/DisqualifyDialog.tsx b/components/organization/hackathons/submissions/DisqualifyDialog.tsx new file mode 100644 index 00000000..5d99d0e5 --- /dev/null +++ b/components/organization/hackathons/submissions/DisqualifyDialog.tsx @@ -0,0 +1,111 @@ +'use client'; + +import { useState } from 'react'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Textarea } from '@/components/ui/textarea'; +import { Label } from '@/components/ui/label'; +import { AlertCircle } from 'lucide-react'; + +interface DisqualifyDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onSubmit: (reason: string) => Promise; + isSubmitting: boolean; +} + +export function DisqualifyDialog({ + open, + onOpenChange, + onSubmit, + isSubmitting, +}: DisqualifyDialogProps) { + const [reason, setReason] = useState(''); + const [error, setError] = useState(null); + + const handleSubmit = async () => { + if (reason.length < 10) { + setError('Reason must be at least 10 characters long'); + return; + } + setError(null); + await onSubmit(reason); + setReason(''); + onOpenChange(false); + }; + + const handleOpenChange = (newOpen: boolean) => { + if (!newOpen) { + setReason(''); + setError(null); + } + onOpenChange(newOpen); + }; + + return ( + + + + + Disqualify Submission + + + Please provides a reason for disqualifying this submission. This + will be visible to the participant. + + +
+
+ +