feat: applicant onboarding, bulk invite, team management enhancements
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m50s
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m50s
- Add nationality/institution fields to User model with migration - Applicant onboarding wizard (name, photo, nationality, country, institution, bio, project logo, preferences) - Project logo upload from applicant context with team membership verification - APPLICANT redirects in set-password, onboarding, and auth layout - Mask evaluation round names as "Evaluation Round 1/2/..." for applicants - Extend inviteTeamMember with nationality/country/institution/sendInvite fields - Admin getApplicants query with search/filter/pagination - Admin bulkInviteApplicants mutation with token generation and emails - Applicants tab on Members page with bulk select and floating invite bar Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -46,6 +46,8 @@ import {
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { CountrySelect } from '@/components/ui/country-select'
|
||||
import { Checkbox as CheckboxPrimitive } from '@/components/ui/checkbox'
|
||||
import {
|
||||
Users,
|
||||
UserPlus,
|
||||
@@ -64,6 +66,10 @@ const inviteSchema = z.object({
|
||||
email: z.string().email('Invalid email address'),
|
||||
role: z.enum(['MEMBER', 'ADVISOR']),
|
||||
title: z.string().optional(),
|
||||
nationality: z.string().optional(),
|
||||
country: z.string().optional(),
|
||||
institution: z.string().optional(),
|
||||
sendInvite: z.boolean().default(true),
|
||||
})
|
||||
|
||||
type InviteFormData = z.infer<typeof inviteSchema>
|
||||
@@ -129,6 +135,10 @@ export default function ApplicantTeamPage() {
|
||||
email: '',
|
||||
role: 'MEMBER',
|
||||
title: '',
|
||||
nationality: '',
|
||||
country: '',
|
||||
institution: '',
|
||||
sendInvite: true,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -280,6 +290,42 @@ export default function ApplicantTeamPage() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Nationality</Label>
|
||||
<CountrySelect
|
||||
value={form.watch('nationality') || ''}
|
||||
onChange={(v) => form.setValue('nationality', v)}
|
||||
placeholder="Select nationality"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Country of Residence</Label>
|
||||
<CountrySelect
|
||||
value={form.watch('country') || ''}
|
||||
onChange={(v) => form.setValue('country', v)}
|
||||
placeholder="Select country"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="institution">Institution (optional)</Label>
|
||||
<Input
|
||||
id="institution"
|
||||
placeholder="e.g., Ocean Research Institute"
|
||||
{...form.register('institution')}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckboxPrimitive
|
||||
id="sendInvite"
|
||||
checked={form.watch('sendInvite')}
|
||||
onCheckedChange={(checked) => form.setValue('sendInvite', !!checked)}
|
||||
/>
|
||||
<Label htmlFor="sendInvite" className="text-sm font-normal cursor-pointer">
|
||||
Send platform invite email
|
||||
</Label>
|
||||
</div>
|
||||
<div className="rounded-lg bg-muted/50 border p-3 text-sm">
|
||||
<p className="font-medium mb-1">What invited members can do:</p>
|
||||
<ul className="list-disc list-inside space-y-1 text-muted-foreground">
|
||||
|
||||
@@ -35,6 +35,8 @@ export default async function AuthLayout({
|
||||
redirect('/observer')
|
||||
} else if (role === 'MENTOR') {
|
||||
redirect('/mentor')
|
||||
} else if (role === 'APPLICANT') {
|
||||
redirect('/applicant')
|
||||
}
|
||||
}
|
||||
// If user doesn't exist in DB, fall through and show auth page
|
||||
|
||||
599
src/app/(auth)/onboarding/applicant-wizard.tsx
Normal file
599
src/app/(auth)/onboarding/applicant-wizard.tsx
Normal file
@@ -0,0 +1,599 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useMemo, useEffect } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { trpc } from '@/lib/trpc/client'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { CountrySelect } from '@/components/ui/country-select'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { toast } from 'sonner'
|
||||
import { AvatarUpload } from '@/components/shared/avatar-upload'
|
||||
import { ProjectLogoUpload } from '@/components/shared/project-logo-upload'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import {
|
||||
User,
|
||||
Bell,
|
||||
CheckCircle,
|
||||
Loader2,
|
||||
ArrowRight,
|
||||
ArrowLeft,
|
||||
Camera,
|
||||
Globe,
|
||||
FileText,
|
||||
Building2,
|
||||
Flag,
|
||||
ImageIcon,
|
||||
} from 'lucide-react'
|
||||
import { AnimatedCard } from '@/components/shared/animated-container'
|
||||
|
||||
type Step =
|
||||
| 'name'
|
||||
| 'photo'
|
||||
| 'nationality'
|
||||
| 'country'
|
||||
| 'institution'
|
||||
| 'bio'
|
||||
| 'logo'
|
||||
| 'preferences'
|
||||
| 'complete'
|
||||
|
||||
type ApplicantWizardProps = {
|
||||
userData: {
|
||||
id: string
|
||||
name: string | null
|
||||
email: string
|
||||
role: string
|
||||
country: string | null
|
||||
nationality: string | null
|
||||
institution: string | null
|
||||
bio: string | null
|
||||
profileImageKey: string | null
|
||||
notificationPreference: string
|
||||
}
|
||||
avatarUrl: string | null | undefined
|
||||
refetchUser: () => void
|
||||
}
|
||||
|
||||
export function ApplicantOnboardingWizard({
|
||||
userData,
|
||||
avatarUrl,
|
||||
refetchUser,
|
||||
}: ApplicantWizardProps) {
|
||||
const router = useRouter()
|
||||
const [step, setStep] = useState<Step>('name')
|
||||
const [initialized, setInitialized] = useState(false)
|
||||
|
||||
// Form state
|
||||
const [name, setName] = useState('')
|
||||
const [nationality, setNationality] = useState('')
|
||||
const [country, setCountry] = useState('')
|
||||
const [institution, setInstitution] = useState('')
|
||||
const [bio, setBio] = useState('')
|
||||
const [notificationPreference, setNotificationPreference] = useState<
|
||||
'EMAIL' | 'WHATSAPP' | 'BOTH' | 'NONE'
|
||||
>('EMAIL')
|
||||
|
||||
// Fetch onboarding context (project info)
|
||||
const { data: onboardingCtx } = trpc.applicant.getOnboardingContext.useQuery()
|
||||
const { data: logoUrl, refetch: refetchLogo } = trpc.applicant.getProjectLogoUrl.useQuery(
|
||||
{ projectId: onboardingCtx?.projectId ?? '' },
|
||||
{ enabled: !!onboardingCtx?.projectId }
|
||||
)
|
||||
|
||||
// Initialize form with user data
|
||||
useEffect(() => {
|
||||
if (userData && !initialized) {
|
||||
if (userData.name) setName(userData.name)
|
||||
if (userData.country) setCountry(userData.country)
|
||||
if (userData.nationality) setNationality(userData.nationality)
|
||||
if (userData.institution) setInstitution(userData.institution)
|
||||
if (userData.bio) setBio(userData.bio)
|
||||
if (userData.notificationPreference) {
|
||||
setNotificationPreference(userData.notificationPreference as typeof notificationPreference)
|
||||
}
|
||||
setInitialized(true)
|
||||
}
|
||||
}, [userData, initialized])
|
||||
|
||||
// Prefill institution from project if user hasn't set one
|
||||
useEffect(() => {
|
||||
if (onboardingCtx?.institution && !institution && initialized) {
|
||||
setInstitution(onboardingCtx.institution)
|
||||
}
|
||||
}, [onboardingCtx, institution, initialized])
|
||||
|
||||
const utils = trpc.useUtils()
|
||||
const completeOnboarding = trpc.user.completeOnboarding.useMutation({
|
||||
onSuccess: () => utils.user.me.invalidate(),
|
||||
})
|
||||
|
||||
const steps: Step[] = useMemo(() => {
|
||||
const base: Step[] = [
|
||||
'name',
|
||||
'photo',
|
||||
'nationality',
|
||||
'country',
|
||||
'institution',
|
||||
'bio',
|
||||
]
|
||||
// Only show logo step if applicant has a project
|
||||
if (onboardingCtx?.projectId) {
|
||||
base.push('logo')
|
||||
}
|
||||
base.push('preferences', 'complete')
|
||||
return base
|
||||
}, [onboardingCtx?.projectId])
|
||||
|
||||
const currentIndex = steps.indexOf(step)
|
||||
const totalVisibleSteps = steps.length - 1
|
||||
|
||||
const goNext = () => {
|
||||
if (step === 'name' && !name.trim()) {
|
||||
toast.error('Please enter your name')
|
||||
return
|
||||
}
|
||||
const nextIndex = currentIndex + 1
|
||||
if (nextIndex < steps.length) {
|
||||
setStep(steps[nextIndex])
|
||||
}
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
const prevIndex = currentIndex - 1
|
||||
if (prevIndex >= 0) {
|
||||
setStep(steps[prevIndex])
|
||||
}
|
||||
}
|
||||
|
||||
const handleComplete = async () => {
|
||||
try {
|
||||
await completeOnboarding.mutateAsync({
|
||||
name,
|
||||
country: country || undefined,
|
||||
nationality: nationality || undefined,
|
||||
institution: institution || undefined,
|
||||
bio: bio || undefined,
|
||||
notificationPreference,
|
||||
})
|
||||
setStep('complete')
|
||||
toast.success('Welcome to MOPC!')
|
||||
|
||||
setTimeout(() => {
|
||||
router.push('/applicant')
|
||||
}, 2000)
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to complete onboarding')
|
||||
}
|
||||
}
|
||||
|
||||
const stepLabels: Record<Step, string> = {
|
||||
name: 'Name',
|
||||
photo: 'Photo',
|
||||
nationality: 'Nationality',
|
||||
country: 'Residence',
|
||||
institution: 'Institution',
|
||||
bio: 'About',
|
||||
logo: 'Logo',
|
||||
preferences: 'Settings',
|
||||
complete: 'Done',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 -m-4 flex items-center justify-center p-4 md:p-8 bg-[#053d57] bg-[url('https://s3.monaco-opc.com/public/ocean.png')] bg-cover bg-center bg-no-repeat">
|
||||
<AnimatedCard>
|
||||
<Card className="w-full max-w-lg max-h-[85vh] overflow-y-auto overflow-x-hidden shadow-2xl">
|
||||
<div className="h-1 w-full bg-gradient-to-r from-brand-blue via-brand-teal to-brand-blue" />
|
||||
|
||||
{/* Progress indicator */}
|
||||
{step !== 'complete' && (
|
||||
<div className="px-6 pt-6">
|
||||
<div className="flex items-center gap-2">
|
||||
{steps.slice(0, -1).map((s, i) => (
|
||||
<div key={s} className="flex items-center flex-1">
|
||||
<div
|
||||
className={`h-2 flex-1 rounded-full transition-colors ${
|
||||
i < currentIndex
|
||||
? 'bg-primary'
|
||||
: i === currentIndex
|
||||
? 'bg-primary/60'
|
||||
: 'bg-muted'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
{steps.slice(0, -1).map((s, i) => (
|
||||
<div key={s} className="flex-1 text-center">
|
||||
<span
|
||||
className={cn(
|
||||
'text-[10px]',
|
||||
i <= currentIndex ? 'text-primary font-medium' : 'text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
{stepLabels[s]}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-2">
|
||||
Step {currentIndex + 1} of {totalVisibleSteps}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step: Name */}
|
||||
{step === 'name' && (
|
||||
<>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<User className="h-5 w-5 text-primary" />
|
||||
Welcome to MOPC
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Let's get your profile set up. What should we call you?
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">Full Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Enter your full name"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={goNext} className="w-full" disabled={!name.trim()}>
|
||||
Continue
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</CardContent>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Step: Profile Photo */}
|
||||
{step === 'photo' && (
|
||||
<>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Camera className="h-5 w-5 text-primary" />
|
||||
Profile Photo
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Add a profile photo so others can recognize you. This step is optional.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex justify-center">
|
||||
<AvatarUpload
|
||||
user={{
|
||||
name: userData?.name,
|
||||
email: userData?.email,
|
||||
profileImageKey: userData?.profileImageKey,
|
||||
}}
|
||||
currentAvatarUrl={avatarUrl}
|
||||
onUploadComplete={() => refetchUser()}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground text-center">
|
||||
Click the avatar to upload a new photo.
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={goBack} className="flex-1">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back
|
||||
</Button>
|
||||
<Button onClick={goNext} className="flex-1">
|
||||
{avatarUrl ? 'Continue' : 'Skip for now'}
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Step: Nationality */}
|
||||
{step === 'nationality' && (
|
||||
<>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Flag className="h-5 w-5 text-primary" />
|
||||
Nationality
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Select your nationality.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="nationality">Nationality</Label>
|
||||
<CountrySelect
|
||||
value={nationality}
|
||||
onChange={setNationality}
|
||||
placeholder="Select your nationality"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={goBack} className="flex-1">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back
|
||||
</Button>
|
||||
<Button onClick={goNext} className="flex-1">
|
||||
{nationality ? 'Continue' : 'Skip for now'}
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Step: Country of Residence */}
|
||||
{step === 'country' && (
|
||||
<>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Globe className="h-5 w-5 text-primary" />
|
||||
Country of Residence
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Where are you currently based?
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="country">Country</Label>
|
||||
<CountrySelect
|
||||
value={country}
|
||||
onChange={setCountry}
|
||||
placeholder="Select your country"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={goBack} className="flex-1">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back
|
||||
</Button>
|
||||
<Button onClick={goNext} className="flex-1">
|
||||
{country ? 'Continue' : 'Skip for now'}
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Step: Institution */}
|
||||
{step === 'institution' && (
|
||||
<>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Building2 className="h-5 w-5 text-primary" />
|
||||
Institution
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Your organization or institution name.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="institution">Institution / Organization</Label>
|
||||
<Input
|
||||
id="institution"
|
||||
value={institution}
|
||||
onChange={(e) => setInstitution(e.target.value)}
|
||||
placeholder="e.g., Ocean Research Institute"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={goBack} className="flex-1">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back
|
||||
</Button>
|
||||
<Button onClick={goNext} className="flex-1">
|
||||
{institution ? 'Continue' : 'Skip for now'}
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Step: Bio */}
|
||||
{step === 'bio' && (
|
||||
<>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FileText className="h-5 w-5 text-primary" />
|
||||
About You
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Tell us a bit about yourself and your work. (Optional)
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="bio">Bio</Label>
|
||||
<Textarea
|
||||
id="bio"
|
||||
value={bio}
|
||||
onChange={(e) => setBio(e.target.value)}
|
||||
placeholder="e.g., Marine biologist working on coral reef conservation..."
|
||||
rows={4}
|
||||
maxLength={500}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground text-right">
|
||||
{bio.length}/500 characters
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={goBack} className="flex-1">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back
|
||||
</Button>
|
||||
<Button onClick={goNext} className="flex-1">
|
||||
{bio ? 'Continue' : 'Skip for now'}
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Step: Project Logo */}
|
||||
{step === 'logo' && onboardingCtx?.projectId && (
|
||||
<>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<ImageIcon className="h-5 w-5 text-primary" />
|
||||
Project Logo
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Upload a logo for "{onboardingCtx.projectTitle}". This step is optional.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex justify-center">
|
||||
<ProjectLogoUpload
|
||||
projectId={onboardingCtx.projectId}
|
||||
currentLogoUrl={logoUrl}
|
||||
onUploadComplete={() => refetchLogo()}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground text-center">
|
||||
Click the image area to upload a logo.
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={goBack} className="flex-1">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back
|
||||
</Button>
|
||||
<Button onClick={goNext} className="flex-1">
|
||||
{logoUrl ? 'Continue' : 'Skip for now'}
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Step: Notification Preferences */}
|
||||
{step === 'preferences' && (
|
||||
<>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Bell className="h-5 w-5 text-primary" />
|
||||
Notification Preferences
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
How would you like to receive notifications?
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="notifications">Notification Channel</Label>
|
||||
<Select
|
||||
value={notificationPreference}
|
||||
onValueChange={(v) =>
|
||||
setNotificationPreference(v as typeof notificationPreference)
|
||||
}
|
||||
>
|
||||
<SelectTrigger id="notifications">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="EMAIL">Email only</SelectItem>
|
||||
<SelectItem value="NONE">No notifications</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border p-4 bg-muted/50">
|
||||
<h4 className="font-medium mb-2">Summary</h4>
|
||||
<div className="space-y-1 text-sm">
|
||||
<p>
|
||||
<span className="text-muted-foreground">Name:</span> {name}
|
||||
</p>
|
||||
{nationality && (
|
||||
<p>
|
||||
<span className="text-muted-foreground">Nationality:</span> {nationality}
|
||||
</p>
|
||||
)}
|
||||
{country && (
|
||||
<p>
|
||||
<span className="text-muted-foreground">Country:</span> {country}
|
||||
</p>
|
||||
)}
|
||||
{institution && (
|
||||
<p>
|
||||
<span className="text-muted-foreground">Institution:</span> {institution}
|
||||
</p>
|
||||
)}
|
||||
{bio && (
|
||||
<p>
|
||||
<span className="text-muted-foreground">Bio:</span>{' '}
|
||||
{bio.length > 50 ? `${bio.substring(0, 50)}...` : bio}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={goBack} className="flex-1">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleComplete}
|
||||
className="flex-1"
|
||||
disabled={completeOnboarding.isPending}
|
||||
>
|
||||
{completeOnboarding.isPending ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<CheckCircle className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
Complete Setup
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Step: Complete */}
|
||||
{step === 'complete' && (
|
||||
<CardContent className="flex flex-col items-center justify-center py-12">
|
||||
<div className="rounded-2xl bg-emerald-50 p-4 mb-4 animate-in zoom-in-50 duration-500">
|
||||
<CheckCircle className="h-12 w-12 text-green-600" />
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold mb-2 animate-in fade-in slide-in-from-bottom-2 duration-500 delay-200">
|
||||
Welcome, {name}!
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-center mb-4">
|
||||
Your profile is all set up. You'll be redirected to your dashboard shortly.
|
||||
</p>
|
||||
<Loader2 className="h-6 w-6 animate-spin text-primary" />
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
Scale,
|
||||
} from 'lucide-react'
|
||||
import { AnimatedCard } from '@/components/shared/animated-container'
|
||||
import { ApplicantOnboardingWizard } from './applicant-wizard'
|
||||
|
||||
type Step = 'name' | 'photo' | 'country' | 'bio' | 'phone' | 'tags' | 'jury' | 'preferences' | 'complete'
|
||||
|
||||
@@ -208,6 +209,8 @@ export default function OnboardingPage() {
|
||||
router.push('/mentor')
|
||||
} else if (role === 'OBSERVER') {
|
||||
router.push('/observer')
|
||||
} else if (role === 'APPLICANT') {
|
||||
router.push('/applicant')
|
||||
} else {
|
||||
router.push('/jury')
|
||||
}
|
||||
@@ -234,6 +237,17 @@ export default function OnboardingPage() {
|
||||
)
|
||||
}
|
||||
|
||||
// Applicant users get a dedicated onboarding wizard
|
||||
if (userData?.role === 'APPLICANT') {
|
||||
return (
|
||||
<ApplicantOnboardingWizard
|
||||
userData={userData as unknown as Parameters<typeof ApplicantOnboardingWizard>[0]['userData']}
|
||||
avatarUrl={avatarUrl}
|
||||
refetchUser={refetchUser}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 -m-4 flex items-center justify-center p-4 md:p-8 bg-[#053d57] bg-[url('https://s3.monaco-opc.com/public/ocean.png')] bg-cover bg-center bg-no-repeat">
|
||||
<AnimatedCard>
|
||||
|
||||
@@ -42,6 +42,8 @@ export default function SetPasswordPage() {
|
||||
router.push('/jury')
|
||||
} else if (session?.user?.role === 'SUPER_ADMIN' || session?.user?.role === 'PROGRAM_ADMIN') {
|
||||
router.push('/admin')
|
||||
} else if (session?.user?.role === 'APPLICANT') {
|
||||
router.push('/applicant')
|
||||
} else {
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user