'use client' import { useState, useEffect, Suspense } from 'react' import { useSearchParams, useRouter } from 'next/navigation' import { signIn } from 'next-auth/react' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card' import { Loader2, CheckCircle2, AlertCircle, XCircle, Clock } from 'lucide-react' import { trpc } from '@/lib/trpc/client' import { AnimatedCard } from '@/components/shared/animated-container' type InviteState = 'loading' | 'valid' | 'accepting' | 'error' function AcceptInviteContent() { const [state, setState] = useState('loading') const [errorType, setErrorType] = useState(null) const searchParams = useSearchParams() const router = useRouter() const token = searchParams.get('token') || '' const { data, isLoading, error } = trpc.user.validateInviteToken.useQuery( { token }, { enabled: !!token, retry: false } ) useEffect(() => { if (!token) { setState('error') setErrorType('MISSING_TOKEN') return } if (isLoading) { setState('loading') return } if (error) { setState('error') setErrorType('NETWORK_ERROR') return } if (data) { if (data.valid) { setState('valid') } else { setState('error') setErrorType(data.error || 'UNKNOWN') } } }, [token, data, isLoading, error]) const handleAccept = async () => { setState('accepting') try { const result = await signIn('credentials', { inviteToken: token, redirect: false, }) if (result?.error) { setState('error') setErrorType('AUTH_FAILED') } else if (result?.ok) { // Redirect to set-password (middleware will enforce this since mustSetPassword=true) window.location.href = '/set-password' } } catch { setState('error') setErrorType('AUTH_FAILED') } } const getRoleLabel = (role: string): string => { switch (role) { case 'JURY_MEMBER': return 'Jury Member' case 'PROGRAM_ADMIN': return 'Program Admin' case 'MENTOR': return 'Mentor' case 'OBSERVER': return 'Observer' case 'APPLICANT': return 'Applicant' default: return role } } const getErrorContent = () => { switch (errorType) { case 'MISSING_TOKEN': return { icon: , title: 'Invalid Link', description: 'This invitation link is incomplete. Please check your email for the correct link.', } case 'INVALID_TOKEN': return { icon: , title: 'Invalid Invitation', description: 'This invitation link is not valid. It may have already been used or the link is incorrect.', } case 'EXPIRED_TOKEN': return { icon: , title: 'Invitation Expired', description: 'This invitation has expired. Please contact your administrator to receive a new invitation.', } case 'ALREADY_ACCEPTED': return { icon: , title: 'Already Accepted', description: 'This invitation has already been accepted. You can sign in with your credentials.', } case 'AUTH_FAILED': return { icon: , title: 'Something Went Wrong', description: 'We couldn\'t complete your account setup. The invitation may have expired. Please try again or contact your administrator.', } default: return { icon: , title: 'Something Went Wrong', description: 'An unexpected error occurred. Please try again or contact your administrator.', } } } // Loading state if (state === 'loading') { return (

Verifying your invitation...

) } // Error state if (state === 'error') { const errorContent = getErrorContent() return (
{errorContent.icon}
{errorContent.title} {errorContent.description}
) } // Valid invitation - show welcome const user = data?.user return (
{user?.name ? `Welcome, ${user.name}!` : 'Welcome!'} You've been invited to join the Monaco Ocean Protection Challenge platform {user?.role ? ` as ${/^[aeiou]/i.test(getRoleLabel(user.role)) ? 'an' : 'a'} ${getRoleLabel(user.role)}.` : '.'}
{user?.email && (

Signing in as

{user.email}

)}

You'll be asked to set a password after accepting.

) } // Loading fallback for Suspense function LoadingCard() { return (

Loading...

) } // Export with Suspense boundary for useSearchParams export default function AcceptInvitePage() { return ( }> ) }