Competition/Round architecture: full platform rewrite (Phases 1-9)
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m45s
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m45s
Replace Pipeline/Stage system with Competition/Round architecture. New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy, ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow. New services: round-engine, round-assignment, deliberation, result-lock, submission-manager, competition-context, ai-prompt-guard. Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with structured prompts, retry logic, and injection detection. All legacy pipeline/stage code removed. 4 new migrations + seed aligned. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { Slider } from '@/components/ui/slider'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { toast } from 'sonner'
|
||||
import { ExpertiseSelect } from '@/components/shared/expertise-select'
|
||||
@@ -40,10 +41,21 @@ import {
|
||||
Camera,
|
||||
Globe,
|
||||
FileText,
|
||||
Scale,
|
||||
} from 'lucide-react'
|
||||
import { AnimatedCard } from '@/components/shared/animated-container'
|
||||
|
||||
type Step = 'name' | 'photo' | 'country' | 'bio' | 'phone' | 'tags' | 'preferences' | 'complete'
|
||||
type Step = 'name' | 'photo' | 'country' | 'bio' | 'phone' | 'tags' | 'jury' | 'preferences' | 'complete'
|
||||
|
||||
type JuryPref = {
|
||||
juryGroupMemberId: string
|
||||
juryGroupName: string
|
||||
currentCap: number
|
||||
allowCapAdjustment: boolean
|
||||
allowRatioAdjustment: boolean
|
||||
selfServiceCap: number | null
|
||||
selfServiceRatio: number | null
|
||||
}
|
||||
|
||||
export default function OnboardingPage() {
|
||||
const router = useRouter()
|
||||
@@ -62,6 +74,7 @@ export default function OnboardingPage() {
|
||||
const [notificationPreference, setNotificationPreference] = useState<
|
||||
'EMAIL' | 'WHATSAPP' | 'BOTH' | 'NONE'
|
||||
>('EMAIL')
|
||||
const [juryPrefs, setJuryPrefs] = useState<Map<string, { cap?: number; ratio?: number }>>(new Map())
|
||||
|
||||
// Fetch current user data only after session is hydrated
|
||||
const { data: userData, isLoading: userLoading, refetch: refetchUser } = trpc.user.me.useQuery(
|
||||
@@ -105,6 +118,14 @@ export default function OnboardingPage() {
|
||||
}
|
||||
}, [userData, initialized])
|
||||
|
||||
// Fetch jury onboarding context
|
||||
const { data: onboardingCtx } = trpc.user.getOnboardingContext.useQuery(
|
||||
undefined,
|
||||
{ enabled: isAuthenticated }
|
||||
)
|
||||
const juryMemberships: JuryPref[] = onboardingCtx?.memberships ?? []
|
||||
const hasJuryStep = onboardingCtx?.hasSelfServiceOptions ?? false
|
||||
|
||||
// Fetch feature flags only after session is hydrated
|
||||
const { data: featureFlags } = trpc.settings.getFeatureFlags.useQuery(
|
||||
undefined,
|
||||
@@ -117,14 +138,15 @@ export default function OnboardingPage() {
|
||||
onSuccess: () => utils.user.me.invalidate(),
|
||||
})
|
||||
|
||||
// Dynamic steps based on WhatsApp availability
|
||||
// Dynamic steps based on WhatsApp availability and jury self-service
|
||||
const steps: Step[] = useMemo(() => {
|
||||
if (whatsappEnabled) {
|
||||
return ['name', 'photo', 'country', 'bio', 'phone', 'tags', 'preferences', 'complete']
|
||||
}
|
||||
// Skip phone step if WhatsApp is disabled
|
||||
return ['name', 'photo', 'country', 'bio', 'tags', 'preferences', 'complete']
|
||||
}, [whatsappEnabled])
|
||||
const base: Step[] = ['name', 'photo', 'country', 'bio']
|
||||
if (whatsappEnabled) base.push('phone')
|
||||
base.push('tags')
|
||||
if (hasJuryStep) base.push('jury')
|
||||
base.push('preferences', 'complete')
|
||||
return base
|
||||
}, [whatsappEnabled, hasJuryStep])
|
||||
|
||||
const currentIndex = steps.indexOf(step)
|
||||
const totalVisibleSteps = steps.length - 1 // Exclude 'complete' from count
|
||||
@@ -149,6 +171,23 @@ export default function OnboardingPage() {
|
||||
|
||||
const handleComplete = async () => {
|
||||
try {
|
||||
// Build jury preferences from state
|
||||
const juryPreferences = juryMemberships
|
||||
.map((m) => {
|
||||
const pref = juryPrefs.get(m.juryGroupMemberId)
|
||||
if (!pref) return null
|
||||
return {
|
||||
juryGroupMemberId: m.juryGroupMemberId,
|
||||
selfServiceCap: pref.cap,
|
||||
selfServiceRatio: pref.ratio,
|
||||
}
|
||||
})
|
||||
.filter(Boolean) as Array<{
|
||||
juryGroupMemberId: string
|
||||
selfServiceCap?: number
|
||||
selfServiceRatio?: number
|
||||
}>
|
||||
|
||||
await completeOnboarding.mutateAsync({
|
||||
name,
|
||||
country: country || undefined,
|
||||
@@ -156,6 +195,7 @@ export default function OnboardingPage() {
|
||||
phoneNumber: phoneNumber || undefined,
|
||||
expertiseTags,
|
||||
notificationPreference,
|
||||
juryPreferences: juryPreferences.length > 0 ? juryPreferences : undefined,
|
||||
})
|
||||
setStep('complete')
|
||||
toast.success('Welcome to MOPC!')
|
||||
@@ -227,6 +267,7 @@ export default function OnboardingPage() {
|
||||
bio: 'About',
|
||||
phone: 'Phone',
|
||||
tags: 'Expertise',
|
||||
jury: 'Jury',
|
||||
preferences: 'Settings',
|
||||
}
|
||||
return (
|
||||
@@ -473,7 +514,95 @@ export default function OnboardingPage() {
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Step 7: Preferences */}
|
||||
{/* Jury Preferences Step (conditional) */}
|
||||
{step === 'jury' && (
|
||||
<>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Scale className="h-5 w-5 text-primary" />
|
||||
Jury Preferences
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Customize your assignment preferences for each jury panel you belong to.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{juryMemberships.map((m) => {
|
||||
const pref = juryPrefs.get(m.juryGroupMemberId) ?? {}
|
||||
const capValue = pref.cap ?? m.selfServiceCap ?? m.currentCap
|
||||
const ratioValue = pref.ratio ?? m.selfServiceRatio ?? 0.5
|
||||
|
||||
return (
|
||||
<div key={m.juryGroupMemberId} className="rounded-lg border p-4 space-y-4">
|
||||
<h4 className="font-medium text-sm">{m.juryGroupName}</h4>
|
||||
|
||||
{m.allowCapAdjustment && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Maximum assignments: {capValue}
|
||||
</Label>
|
||||
<Slider
|
||||
value={[capValue]}
|
||||
onValueChange={([v]) =>
|
||||
setJuryPrefs((prev) => {
|
||||
const next = new Map(prev)
|
||||
next.set(m.juryGroupMemberId, { ...pref, cap: v })
|
||||
return next
|
||||
})
|
||||
}
|
||||
min={1}
|
||||
max={m.currentCap}
|
||||
step={1}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Admin default: {m.currentCap}. You may reduce this to match your availability.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{m.allowRatioAdjustment && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Startup vs Business Concept ratio: {Math.round(ratioValue * 100)}% / {Math.round((1 - ratioValue) * 100)}%
|
||||
</Label>
|
||||
<Slider
|
||||
value={[ratioValue * 100]}
|
||||
onValueChange={([v]) =>
|
||||
setJuryPrefs((prev) => {
|
||||
const next = new Map(prev)
|
||||
next.set(m.juryGroupMemberId, { ...pref, ratio: v / 100 })
|
||||
return next
|
||||
})
|
||||
}
|
||||
min={0}
|
||||
max={100}
|
||||
step={5}
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-muted-foreground">
|
||||
<span>More Business Concepts</span>
|
||||
<span>More Startups</span>
|
||||
</div>
|
||||
</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>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Notification Preferences */}
|
||||
{step === 'preferences' && (
|
||||
<>
|
||||
<CardHeader>
|
||||
|
||||
Reference in New Issue
Block a user