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:
@@ -12,6 +12,7 @@ import {
|
||||
CardTitle,
|
||||
} from '@/components/ui/card'
|
||||
import { Loader2, CheckCircle2, AlertCircle, XCircle, Clock } from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
import { trpc } from '@/lib/trpc/client'
|
||||
import { AnimatedCard } from '@/components/shared/animated-container'
|
||||
|
||||
@@ -179,13 +180,14 @@ function AcceptInviteContent() {
|
||||
|
||||
// Valid invitation - show welcome
|
||||
const user = data?.user
|
||||
const team = data?.team
|
||||
return (
|
||||
<AnimatedCard>
|
||||
<Card className="w-full max-w-md overflow-hidden">
|
||||
<div className="h-1 w-full bg-gradient-to-r from-brand-blue via-brand-teal to-brand-blue" />
|
||||
<CardHeader className="text-center">
|
||||
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-2xl bg-emerald-50">
|
||||
<CheckCircle2 className="h-6 w-6 text-green-600" />
|
||||
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-xl bg-white shadow-sm border">
|
||||
<Image src="/images/MOPC-blue-small.png" alt="MOPC" width={32} height={32} className="object-contain" />
|
||||
</div>
|
||||
<CardTitle className="text-xl">
|
||||
{user?.name ? `Welcome, ${user.name}!` : 'Welcome!'}
|
||||
@@ -196,6 +198,14 @@ function AcceptInviteContent() {
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{team?.projectTitle && (
|
||||
<div className="rounded-md border border-blue-200 bg-blue-50 p-3 text-center">
|
||||
<p className="text-sm text-blue-700">
|
||||
You've been invited to join the team for
|
||||
</p>
|
||||
<p className="font-semibold text-blue-900">“{team.projectTitle}”</p>
|
||||
</div>
|
||||
)}
|
||||
{user?.email && (
|
||||
<div className="rounded-md bg-muted/50 p-3 text-center">
|
||||
<p className="text-sm text-muted-foreground">Signing in as</p>
|
||||
|
||||
@@ -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}!
|
||||
|
||||
@@ -43,7 +43,7 @@ export default function SetPasswordPage() {
|
||||
} else if (session?.user?.role === 'SUPER_ADMIN' || session?.user?.role === 'PROGRAM_ADMIN') {
|
||||
router.push('/admin')
|
||||
} else if (session?.user?.role === 'APPLICANT') {
|
||||
router.push('/applicant')
|
||||
router.push('/onboarding')
|
||||
} else {
|
||||
router.push('/')
|
||||
}
|
||||
@@ -148,7 +148,7 @@ export default function SetPasswordPage() {
|
||||
</CardHeader>
|
||||
<CardContent className="text-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Redirecting you to the dashboard...
|
||||
Redirecting you to onboarding...
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user