Add unified expertise tag system and round entry notifications

- ExpertiseSelect now fetches tags from database with category grouping
- Tags set by admin during invitation are locked and cannot be removed
- Onboarding merges user-selected tags with admin-preset tags
- MENTOR role now goes through onboarding flow
- Added migration for Round.entryNotificationType column
- Added seed script with ~90 comprehensive expertise tags

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-04 01:15:21 +01:00
parent 41a36f72b3
commit 8cdf6c9e5e
5 changed files with 475 additions and 80 deletions

View File

@@ -1,6 +1,6 @@
'use client'
import { useState, useMemo } from 'react'
import { useState, useMemo, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { trpc } from '@/lib/trpc/client'
import { Button } from '@/components/ui/button'
@@ -39,15 +39,44 @@ type Step = 'name' | 'phone' | 'tags' | 'preferences' | 'complete'
export default function OnboardingPage() {
const router = useRouter()
const [step, setStep] = useState<Step>('name')
const [initialized, setInitialized] = useState(false)
// Form state
const [name, setName] = useState('')
const [phoneNumber, setPhoneNumber] = useState('')
const [expertiseTags, setExpertiseTags] = useState<string[]>([])
const [lockedTags, setLockedTags] = useState<string[]>([])
const [notificationPreference, setNotificationPreference] = useState<
'EMAIL' | 'WHATSAPP' | 'BOTH' | 'NONE'
>('EMAIL')
// Fetch current user data to get admin-preset tags
const { data: userData, isLoading: userLoading } = trpc.user.me.useQuery()
// Initialize form with user data
useEffect(() => {
if (userData && !initialized) {
// Pre-fill name if available
if (userData.name) {
setName(userData.name)
}
// Pre-fill phone if available
if (userData.phoneNumber) {
setPhoneNumber(userData.phoneNumber)
}
// Set admin-preset tags as both locked and selected
if (userData.expertiseTags && userData.expertiseTags.length > 0) {
setLockedTags(userData.expertiseTags)
setExpertiseTags(userData.expertiseTags)
}
// Pre-fill notification preference if available
if (userData.notificationPreference) {
setNotificationPreference(userData.notificationPreference)
}
setInitialized(true)
}
}, [userData, initialized])
// Fetch feature flags
const { data: featureFlags } = trpc.settings.getFeatureFlags.useQuery()
const whatsappEnabled = featureFlags?.whatsappEnabled ?? false
@@ -95,15 +124,36 @@ export default function OnboardingPage() {
setStep('complete')
toast.success('Welcome to MOPC!')
// Redirect after a short delay
// Redirect after a short delay based on user role
setTimeout(() => {
router.push('/jury')
const role = userData?.role
if (role === 'MENTOR') {
router.push('/mentor')
} else if (role === 'OBSERVER') {
router.push('/observer')
} else {
router.push('/jury')
}
}, 2000)
} catch (error) {
toast.error(error instanceof Error ? error.message : 'Failed to complete onboarding')
}
}
// Show loading while fetching user data
if (userLoading || !initialized) {
return (
<div className="absolute inset-0 -m-4 flex items-center justify-center p-4 md:p-8 bg-gradient-to-br from-[#053d57] to-[#557f8c]">
<Card className="w-full max-w-lg shadow-2xl">
<CardContent className="flex flex-col items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-primary mb-4" />
<p className="text-muted-foreground">Loading your profile...</p>
</CardContent>
</Card>
</div>
)
}
return (
<div className="absolute inset-0 -m-4 flex items-center justify-center p-4 md:p-8 bg-gradient-to-br from-[#053d57] to-[#557f8c]">
<Card className="w-full max-w-lg max-h-[85vh] overflow-y-auto shadow-2xl">
@@ -219,6 +269,7 @@ export default function OnboardingPage() {
value={expertiseTags}
onChange={setExpertiseTags}
maxTags={5}
lockedTags={lockedTags}
/>
<div className="flex gap-2">