feat: round finalization with ranking-based outcomes + award pool notifications
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m0s
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m0s
- processRoundClose EVALUATION uses ranking scores + advanceMode config (threshold vs count) to auto-set proposedOutcome instead of defaulting all to PASSED - Advancement emails generate invite tokens for passwordless users with "Create Your Account" CTA; rejection emails have no link - Finalization UI shows account stats (invite vs dashboard link counts) - Fixed getFinalizationSummary ranking query (was using non-existent rankingsJson) - New award pool notification system: getAwardSelectionNotificationTemplate email, notifyEligibleProjects mutation with invite token generation, "Notify Pool" button on award detail page with custom message dialog Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useMemo, useEffect } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import Image from 'next/image'
|
||||
import { trpc } from '@/lib/trpc/client'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
@@ -39,8 +40,17 @@ import {
|
||||
Building2,
|
||||
Flag,
|
||||
ImageIcon,
|
||||
Compass,
|
||||
LayoutDashboard,
|
||||
Upload,
|
||||
ClipboardList,
|
||||
Users,
|
||||
Trophy,
|
||||
BookOpen,
|
||||
GraduationCap,
|
||||
} from 'lucide-react'
|
||||
import { AnimatedCard } from '@/components/shared/animated-container'
|
||||
import { UserAvatar } from '@/components/shared/user-avatar'
|
||||
|
||||
type Step =
|
||||
| 'name'
|
||||
@@ -51,6 +61,7 @@ type Step =
|
||||
| 'bio'
|
||||
| 'logo'
|
||||
| 'preferences'
|
||||
| 'guide'
|
||||
| 'complete'
|
||||
|
||||
type ApplicantWizardProps = {
|
||||
@@ -136,7 +147,7 @@ export function ApplicantOnboardingWizard({
|
||||
if (onboardingCtx?.projectId) {
|
||||
base.push('logo')
|
||||
}
|
||||
base.push('preferences', 'complete')
|
||||
base.push('preferences', 'guide', 'complete')
|
||||
return base
|
||||
}, [onboardingCtx?.projectId])
|
||||
|
||||
@@ -191,6 +202,7 @@ export function ApplicantOnboardingWizard({
|
||||
bio: 'About',
|
||||
logo: 'Logo',
|
||||
preferences: 'Settings',
|
||||
guide: 'Guide',
|
||||
complete: 'Done',
|
||||
}
|
||||
|
||||
@@ -203,11 +215,11 @@ export function ApplicantOnboardingWizard({
|
||||
{/* Progress indicator */}
|
||||
{step !== 'complete' && (
|
||||
<div className="px-6 pt-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1">
|
||||
{steps.slice(0, -1).map((s, i) => (
|
||||
<div key={s} className="flex items-center flex-1">
|
||||
<div key={s} className="flex-1 flex flex-col items-center gap-1">
|
||||
<div
|
||||
className={`h-2 flex-1 rounded-full transition-colors ${
|
||||
className={`h-2 w-full rounded-full transition-colors ${
|
||||
i < currentIndex
|
||||
? 'bg-primary'
|
||||
: i === currentIndex
|
||||
@@ -215,15 +227,9 @@ export function ApplicantOnboardingWizard({
|
||||
: '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]',
|
||||
'text-[10px] leading-none',
|
||||
i <= currentIndex ? 'text-primary font-medium' : 'text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
@@ -291,7 +297,16 @@ export function ApplicantOnboardingWizard({
|
||||
}}
|
||||
currentAvatarUrl={avatarUrl}
|
||||
onUploadComplete={() => refetchUser()}
|
||||
/>
|
||||
>
|
||||
<div className="cursor-pointer">
|
||||
<UserAvatar
|
||||
user={{ name: userData?.name, email: userData?.email }}
|
||||
avatarUrl={avatarUrl}
|
||||
size="2xl"
|
||||
showEditOverlay
|
||||
/>
|
||||
</div>
|
||||
</AvatarUpload>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground text-center">
|
||||
Click the avatar to upload a new photo.
|
||||
@@ -555,6 +570,83 @@ export function ApplicantOnboardingWizard({
|
||||
</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={goNext} className="flex-1">
|
||||
Continue
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Step: Portal Guide */}
|
||||
{step === 'guide' && (
|
||||
<>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Compass className="h-5 w-5 text-primary" />
|
||||
Your Applicant Portal
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Here's what you can do through the MOPC Applicant Portal.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{
|
||||
icon: LayoutDashboard,
|
||||
title: 'Dashboard',
|
||||
desc: 'Overview of your project status, team, and upcoming deadlines.',
|
||||
},
|
||||
{
|
||||
icon: Upload,
|
||||
title: 'Documents',
|
||||
desc: 'Upload required files for each round and track submission progress.',
|
||||
},
|
||||
{
|
||||
icon: ClipboardList,
|
||||
title: 'Evaluations',
|
||||
desc: 'View anonymized jury feedback and scores for your project.',
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
title: 'Team',
|
||||
desc: 'Manage your team members, invite collaborators, and update your project logo.',
|
||||
},
|
||||
{
|
||||
icon: Trophy,
|
||||
title: 'Competition',
|
||||
desc: 'Track your progress through competition rounds and milestones.',
|
||||
},
|
||||
{
|
||||
icon: GraduationCap,
|
||||
title: 'Mentorship',
|
||||
desc: 'Connect with your assigned mentor for guidance and support.',
|
||||
},
|
||||
{
|
||||
icon: BookOpen,
|
||||
title: 'Resources',
|
||||
desc: 'Access helpful materials, guides, and competition resources.',
|
||||
},
|
||||
].map(({ icon: Icon, title, desc }) => (
|
||||
<div key={title} className="flex items-start gap-3 rounded-lg border p-3">
|
||||
<div className="rounded-md bg-primary/10 p-2 shrink-0">
|
||||
<Icon className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-sm">{title}</p>
|
||||
<p className="text-xs text-muted-foreground">{desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={goBack} className="flex-1">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
@@ -580,8 +672,8 @@ export function ApplicantOnboardingWizard({
|
||||
{/* 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 className="mb-4 animate-in zoom-in-50 duration-500">
|
||||
<Image src="/images/MOPC-blue-small.png" alt="MOPC Logo" width={64} height={64} className="h-16 w-16" />
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold mb-2 animate-in fade-in slide-in-from-bottom-2 duration-500 delay-200">
|
||||
Welcome, {name}!
|
||||
|
||||
Reference in New Issue
Block a user