Apply full refactor updates plus pipeline/email UX confirmations
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m33s

This commit is contained in:
Matt
2026-02-14 15:26:42 +01:00
parent e56e143a40
commit b5425e705e
374 changed files with 116737 additions and 111969 deletions

View File

@@ -18,6 +18,7 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { Badge } from '@/components/ui/badge'
import { Checkbox } from '@/components/ui/checkbox'
import { Skeleton } from '@/components/ui/skeleton'
import {
Select,
@@ -103,6 +104,7 @@ const fileTypeIcons: Record<string, React.ReactNode> = {
function EditProjectContent({ projectId }: { projectId: string }) {
const router = useRouter()
const [tagInput, setTagInput] = useState('')
const [statusNotificationConfirmed, setStatusNotificationConfirmed] = useState(false)
// Fetch project data
const { data: project, isLoading } = trpc.project.get.useQuery({
@@ -172,6 +174,24 @@ function EditProjectContent({ projectId }: { projectId: string }) {
}, [project, form])
const tags = form.watch('tags')
const selectedStatus = form.watch('status')
const previousStatus = (project?.status ?? 'SUBMITTED') as UpdateProjectForm['status']
const statusTriggersNotifications = ['SEMIFINALIST', 'FINALIST', 'REJECTED'].includes(selectedStatus)
const requiresStatusNotificationConfirmation = Boolean(
project && selectedStatus !== previousStatus && statusTriggersNotifications
)
const notificationRecipientEmails = Array.from(
new Set(
(project?.teamMembers ?? [])
.map((member) => member.user?.email?.toLowerCase().trim() ?? '')
.filter((email) => email.length > 0)
)
)
useEffect(() => {
setStatusNotificationConfirmed(false)
form.clearErrors('status')
}, [selectedStatus, form])
// Add tag
const addTag = useCallback(() => {
@@ -194,6 +214,14 @@ function EditProjectContent({ projectId }: { projectId: string }) {
)
const onSubmit = async (data: UpdateProjectForm) => {
if (requiresStatusNotificationConfirmation && !statusNotificationConfirmed) {
form.setError('status', {
type: 'manual',
message: 'Confirm participant notifications before saving this status change.',
})
return
}
await updateProject.mutateAsync({
id: projectId,
title: data.title,
@@ -370,6 +398,39 @@ function EditProjectContent({ projectId }: { projectId: string }) {
<SelectItem value="REJECTED">Rejected</SelectItem>
</SelectContent>
</Select>
{requiresStatusNotificationConfirmation && (
<div className="space-y-2 rounded-md border bg-muted/20 p-3">
<p className="text-xs font-medium">
Participant Notification Check
</p>
<p className="text-xs text-muted-foreground">
Saving this status will send automated notifications.
</p>
<p className="text-xs text-muted-foreground">
Recipients ({notificationRecipientEmails.length}):{' '}
{notificationRecipientEmails.length > 0
? notificationRecipientEmails.slice(0, 8).join(', ')
: 'No linked participant accounts found'}
{notificationRecipientEmails.length > 8 ? ', ...' : ''}
</p>
<div className="flex items-start gap-2">
<Checkbox
id="confirm-status-notifications"
checked={statusNotificationConfirmed}
onCheckedChange={(checked) => {
const confirmed = checked === true
setStatusNotificationConfirmed(confirmed)
if (confirmed) {
form.clearErrors('status')
}
}}
/>
<FormLabel htmlFor="confirm-status-notifications" className="text-sm font-normal leading-5">
I verified participant recipients and approve sending automated notifications.
</FormLabel>
</div>
</div>
)}
<FormMessage />
</FormItem>
)}
@@ -557,7 +618,10 @@ function EditProjectContent({ projectId }: { projectId: string }) {
<Button type="button" variant="outline" asChild>
<Link href={`/admin/projects/${projectId}`}>Cancel</Link>
</Button>
<Button type="submit" disabled={isPending}>
<Button
type="submit"
disabled={isPending || (requiresStatusNotificationConfirmation && !statusNotificationConfirmed)}
>
{updateProject.isPending && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}

View File

@@ -1,393 +1,393 @@
'use client'
import { Suspense, use, useState } from 'react'
import Link from 'next/link'
import { trpc } from '@/lib/trpc/client'
import { toast } from 'sonner'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Skeleton } from '@/components/ui/skeleton'
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import { Progress } from '@/components/ui/progress'
import {
ArrowLeft,
Loader2,
Users,
User,
Check,
RefreshCw,
} from 'lucide-react'
import { getInitials } from '@/lib/utils'
interface PageProps {
params: Promise<{ id: string }>
}
// Type for mentor suggestion from the API
interface MentorSuggestion {
mentorId: string
confidenceScore: number
expertiseMatchScore: number
reasoning: string
mentor: {
id: string
name: string | null
email: string
expertiseTags: string[]
assignmentCount: number
} | null
}
function MentorAssignmentContent({ projectId }: { projectId: string }) {
const [selectedMentorId, setSelectedMentorId] = useState<string | null>(null)
const utils = trpc.useUtils()
// Fetch project
const { data: project, isLoading: projectLoading } = trpc.project.get.useQuery({
id: projectId,
})
// Fetch suggestions
const { data: suggestions, isLoading: suggestionsLoading, refetch } = trpc.mentor.getSuggestions.useQuery(
{ projectId, limit: 5 },
{ enabled: !!project && !project.mentorAssignment }
)
// Assign mentor mutation
const assignMutation = trpc.mentor.assign.useMutation({
onSuccess: () => {
toast.success('Mentor assigned!')
utils.project.get.invalidate({ id: projectId })
utils.mentor.getSuggestions.invalidate({ projectId })
},
onError: (error) => {
toast.error(error.message)
},
})
// Auto-assign mutation
const autoAssignMutation = trpc.mentor.autoAssign.useMutation({
onSuccess: () => {
toast.success('Mentor auto-assigned!')
utils.project.get.invalidate({ id: projectId })
utils.mentor.getSuggestions.invalidate({ projectId })
},
onError: (error) => {
toast.error(error.message)
},
})
// Unassign mutation
const unassignMutation = trpc.mentor.unassign.useMutation({
onSuccess: () => {
toast.success('Mentor removed')
utils.project.get.invalidate({ id: projectId })
utils.mentor.getSuggestions.invalidate({ projectId })
},
onError: (error) => {
toast.error(error.message)
},
})
const handleAssign = (mentorId: string, suggestion?: MentorSuggestion) => {
assignMutation.mutate({
projectId,
mentorId,
method: suggestion ? 'AI_SUGGESTED' : 'MANUAL',
aiConfidenceScore: suggestion?.confidenceScore,
expertiseMatchScore: suggestion?.expertiseMatchScore,
aiReasoning: suggestion?.reasoning,
})
}
if (projectLoading) {
return <MentorAssignmentSkeleton />
}
if (!project) {
return (
<Card>
<CardContent className="py-12 text-center">
<p>Project not found</p>
</CardContent>
</Card>
)
}
const hasMentor = !!project.mentorAssignment
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center gap-4">
<Button variant="ghost" asChild className="-ml-4">
<Link href={`/admin/projects/${projectId}`}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Project
</Link>
</Button>
</div>
<div>
<h1 className="text-2xl font-semibold tracking-tight">Mentor Assignment</h1>
<p className="text-muted-foreground">{project.title}</p>
</div>
{/* Current Assignment */}
{hasMentor && (
<Card>
<CardHeader>
<CardTitle className="text-lg">Current Mentor</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Avatar className="h-12 w-12">
<AvatarFallback>
{getInitials(project.mentorAssignment!.mentor.name || project.mentorAssignment!.mentor.email)}
</AvatarFallback>
</Avatar>
<div>
<p className="font-medium">{project.mentorAssignment!.mentor.name || 'Unnamed'}</p>
<p className="text-sm text-muted-foreground">{project.mentorAssignment!.mentor.email}</p>
{project.mentorAssignment!.mentor.expertiseTags && project.mentorAssignment!.mentor.expertiseTags.length > 0 && (
<div className="flex flex-wrap gap-1 mt-1">
{project.mentorAssignment!.mentor.expertiseTags.slice(0, 3).map((tag: string) => (
<Badge key={tag} variant="secondary" className="text-xs">{tag}</Badge>
))}
</div>
)}
</div>
</div>
<div className="text-right">
<Badge variant="outline" className="mb-2">
{project.mentorAssignment!.method.replace(/_/g, ' ')}
</Badge>
<div>
<Button
variant="destructive"
size="sm"
onClick={() => unassignMutation.mutate({ projectId })}
disabled={unassignMutation.isPending}
>
{unassignMutation.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
'Remove'
)}
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
)}
{/* AI Suggestions */}
{!hasMentor && (
<>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-lg flex items-center gap-2">
<Users className="h-5 w-5 text-primary" />
AI-Suggested Mentors
</CardTitle>
<CardDescription>
Mentors matched based on expertise and project needs
</CardDescription>
</div>
<div className="flex gap-2">
<Button
variant="outline"
onClick={() => refetch()}
disabled={suggestionsLoading}
>
{suggestionsLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
'Refresh'
)}
</Button>
<Button
onClick={() => autoAssignMutation.mutate({ projectId, useAI: true })}
disabled={autoAssignMutation.isPending}
>
{autoAssignMutation.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<RefreshCw className="mr-2 h-4 w-4" />
)}
Auto-Assign Best Match
</Button>
</div>
</div>
</CardHeader>
<CardContent>
{suggestionsLoading ? (
<div className="space-y-4">
{[1, 2, 3].map((i) => (
<Skeleton key={i} className="h-24 w-full" />
))}
</div>
) : suggestions?.suggestions.length === 0 ? (
<p className="text-muted-foreground text-center py-8">
No mentor suggestions available. Try adding more users with expertise tags.
</p>
) : (
<div className="space-y-4">
{suggestions?.suggestions.map((suggestion, index) => (
<div
key={suggestion.mentorId}
className={`p-4 rounded-lg border-2 transition-colors ${
selectedMentorId === suggestion.mentorId
? 'border-primary bg-primary/5'
: 'border-border hover:border-primary/50'
}`}
>
<div className="flex items-start justify-between gap-4">
<div className="flex items-start gap-4 flex-1">
<div className="relative">
<Avatar className="h-12 w-12">
<AvatarFallback>
{suggestion.mentor ? getInitials(suggestion.mentor.name || suggestion.mentor.email) : '?'}
</AvatarFallback>
</Avatar>
{index === 0 && (
<div className="absolute -top-1 -right-1 h-5 w-5 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-xs font-bold">
1
</div>
)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<p className="font-medium">{suggestion.mentor?.name || 'Unnamed'}</p>
<Badge variant="outline" className="text-xs">
{suggestion.mentor?.assignmentCount || 0} projects
</Badge>
</div>
<p className="text-sm text-muted-foreground">{suggestion.mentor?.email}</p>
{/* Expertise tags */}
{suggestion.mentor?.expertiseTags && suggestion.mentor.expertiseTags.length > 0 && (
<div className="flex flex-wrap gap-1 mt-2">
{suggestion.mentor.expertiseTags.map((tag) => (
<Badge key={tag} variant="secondary" className="text-xs">
{tag}
</Badge>
))}
</div>
)}
{/* Match scores */}
<div className="mt-3 space-y-2">
<div className="flex items-center gap-2 text-sm">
<span className="text-muted-foreground w-28">Confidence:</span>
<Progress value={suggestion.confidenceScore * 100} className="flex-1 h-2" />
<span className="w-12 text-right">{(suggestion.confidenceScore * 100).toFixed(0)}%</span>
</div>
<div className="flex items-center gap-2 text-sm">
<span className="text-muted-foreground w-28">Expertise Match:</span>
<Progress value={suggestion.expertiseMatchScore * 100} className="flex-1 h-2" />
<span className="w-12 text-right">{(suggestion.expertiseMatchScore * 100).toFixed(0)}%</span>
</div>
</div>
{/* AI Reasoning */}
{suggestion.reasoning && (
<p className="mt-2 text-sm text-muted-foreground italic">
&quot;{suggestion.reasoning}&quot;
</p>
)}
</div>
</div>
<Button
onClick={() => handleAssign(suggestion.mentorId, suggestion)}
disabled={assignMutation.isPending}
variant={selectedMentorId === suggestion.mentorId ? 'default' : 'outline'}
>
{assignMutation.isPending && selectedMentorId === suggestion.mentorId ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<>
<Check className="mr-2 h-4 w-4" />
Assign
</>
)}
</Button>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
{/* Manual Assignment */}
<Card>
<CardHeader>
<CardTitle className="text-lg flex items-center gap-2">
<User className="h-5 w-5" />
Manual Assignment
</CardTitle>
<CardDescription>
Search and select a mentor manually
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Use the AI suggestions above or search for a specific user in the Users section
to assign them as a mentor manually.
</p>
</CardContent>
</Card>
</>
)}
</div>
)
}
function MentorAssignmentSkeleton() {
return (
<div className="space-y-6">
<Skeleton className="h-9 w-36" />
<div className="space-y-2">
<Skeleton className="h-8 w-64" />
<Skeleton className="h-4 w-48" />
</div>
<Card>
<CardHeader>
<Skeleton className="h-6 w-48" />
<Skeleton className="h-4 w-64" />
</CardHeader>
<CardContent>
<div className="space-y-4">
{[1, 2, 3].map((i) => (
<Skeleton key={i} className="h-24 w-full" />
))}
</div>
</CardContent>
</Card>
</div>
)
}
export default function MentorAssignmentPage({ params }: PageProps) {
const { id } = use(params)
return (
<Suspense fallback={<MentorAssignmentSkeleton />}>
<MentorAssignmentContent projectId={id} />
</Suspense>
)
}
'use client'
import { Suspense, use, useState } from 'react'
import Link from 'next/link'
import { trpc } from '@/lib/trpc/client'
import { toast } from 'sonner'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Skeleton } from '@/components/ui/skeleton'
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import { Progress } from '@/components/ui/progress'
import {
ArrowLeft,
Loader2,
Users,
User,
Check,
RefreshCw,
} from 'lucide-react'
import { getInitials } from '@/lib/utils'
interface PageProps {
params: Promise<{ id: string }>
}
// Type for mentor suggestion from the API
interface MentorSuggestion {
mentorId: string
confidenceScore: number
expertiseMatchScore: number
reasoning: string
mentor: {
id: string
name: string | null
email: string
expertiseTags: string[]
assignmentCount: number
} | null
}
function MentorAssignmentContent({ projectId }: { projectId: string }) {
const [selectedMentorId, setSelectedMentorId] = useState<string | null>(null)
const utils = trpc.useUtils()
// Fetch project
const { data: project, isLoading: projectLoading } = trpc.project.get.useQuery({
id: projectId,
})
// Fetch suggestions
const { data: suggestions, isLoading: suggestionsLoading, refetch } = trpc.mentor.getSuggestions.useQuery(
{ projectId, limit: 5 },
{ enabled: !!project && !project.mentorAssignment }
)
// Assign mentor mutation
const assignMutation = trpc.mentor.assign.useMutation({
onSuccess: () => {
toast.success('Mentor assigned!')
utils.project.get.invalidate({ id: projectId })
utils.mentor.getSuggestions.invalidate({ projectId })
},
onError: (error) => {
toast.error(error.message)
},
})
// Auto-assign mutation
const autoAssignMutation = trpc.mentor.autoAssign.useMutation({
onSuccess: () => {
toast.success('Mentor auto-assigned!')
utils.project.get.invalidate({ id: projectId })
utils.mentor.getSuggestions.invalidate({ projectId })
},
onError: (error) => {
toast.error(error.message)
},
})
// Unassign mutation
const unassignMutation = trpc.mentor.unassign.useMutation({
onSuccess: () => {
toast.success('Mentor removed')
utils.project.get.invalidate({ id: projectId })
utils.mentor.getSuggestions.invalidate({ projectId })
},
onError: (error) => {
toast.error(error.message)
},
})
const handleAssign = (mentorId: string, suggestion?: MentorSuggestion) => {
assignMutation.mutate({
projectId,
mentorId,
method: suggestion ? 'AI_SUGGESTED' : 'MANUAL',
aiConfidenceScore: suggestion?.confidenceScore,
expertiseMatchScore: suggestion?.expertiseMatchScore,
aiReasoning: suggestion?.reasoning,
})
}
if (projectLoading) {
return <MentorAssignmentSkeleton />
}
if (!project) {
return (
<Card>
<CardContent className="py-12 text-center">
<p>Project not found</p>
</CardContent>
</Card>
)
}
const hasMentor = !!project.mentorAssignment
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center gap-4">
<Button variant="ghost" asChild className="-ml-4">
<Link href={`/admin/projects/${projectId}`}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Project
</Link>
</Button>
</div>
<div>
<h1 className="text-2xl font-semibold tracking-tight">Mentor Assignment</h1>
<p className="text-muted-foreground">{project.title}</p>
</div>
{/* Current Assignment */}
{hasMentor && (
<Card>
<CardHeader>
<CardTitle className="text-lg">Current Mentor</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Avatar className="h-12 w-12">
<AvatarFallback>
{getInitials(project.mentorAssignment!.mentor.name || project.mentorAssignment!.mentor.email)}
</AvatarFallback>
</Avatar>
<div>
<p className="font-medium">{project.mentorAssignment!.mentor.name || 'Unnamed'}</p>
<p className="text-sm text-muted-foreground">{project.mentorAssignment!.mentor.email}</p>
{project.mentorAssignment!.mentor.expertiseTags && project.mentorAssignment!.mentor.expertiseTags.length > 0 && (
<div className="flex flex-wrap gap-1 mt-1">
{project.mentorAssignment!.mentor.expertiseTags.slice(0, 3).map((tag: string) => (
<Badge key={tag} variant="secondary" className="text-xs">{tag}</Badge>
))}
</div>
)}
</div>
</div>
<div className="text-right">
<Badge variant="outline" className="mb-2">
{project.mentorAssignment!.method.replace(/_/g, ' ')}
</Badge>
<div>
<Button
variant="destructive"
size="sm"
onClick={() => unassignMutation.mutate({ projectId })}
disabled={unassignMutation.isPending}
>
{unassignMutation.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
'Remove'
)}
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
)}
{/* AI Suggestions */}
{!hasMentor && (
<>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-lg flex items-center gap-2">
<Users className="h-5 w-5 text-primary" />
AI-Suggested Mentors
</CardTitle>
<CardDescription>
Mentors matched based on expertise and project needs
</CardDescription>
</div>
<div className="flex gap-2">
<Button
variant="outline"
onClick={() => refetch()}
disabled={suggestionsLoading}
>
{suggestionsLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
'Refresh'
)}
</Button>
<Button
onClick={() => autoAssignMutation.mutate({ projectId, useAI: true })}
disabled={autoAssignMutation.isPending}
>
{autoAssignMutation.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<RefreshCw className="mr-2 h-4 w-4" />
)}
Auto-Assign Best Match
</Button>
</div>
</div>
</CardHeader>
<CardContent>
{suggestionsLoading ? (
<div className="space-y-4">
{[1, 2, 3].map((i) => (
<Skeleton key={i} className="h-24 w-full" />
))}
</div>
) : suggestions?.suggestions.length === 0 ? (
<p className="text-muted-foreground text-center py-8">
No mentor suggestions available. Try adding more users with expertise tags.
</p>
) : (
<div className="space-y-4">
{suggestions?.suggestions.map((suggestion, index) => (
<div
key={suggestion.mentorId}
className={`p-4 rounded-lg border-2 transition-colors ${
selectedMentorId === suggestion.mentorId
? 'border-primary bg-primary/5'
: 'border-border hover:border-primary/50'
}`}
>
<div className="flex items-start justify-between gap-4">
<div className="flex items-start gap-4 flex-1">
<div className="relative">
<Avatar className="h-12 w-12">
<AvatarFallback>
{suggestion.mentor ? getInitials(suggestion.mentor.name || suggestion.mentor.email) : '?'}
</AvatarFallback>
</Avatar>
{index === 0 && (
<div className="absolute -top-1 -right-1 h-5 w-5 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-xs font-bold">
1
</div>
)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<p className="font-medium">{suggestion.mentor?.name || 'Unnamed'}</p>
<Badge variant="outline" className="text-xs">
{suggestion.mentor?.assignmentCount || 0} projects
</Badge>
</div>
<p className="text-sm text-muted-foreground">{suggestion.mentor?.email}</p>
{/* Expertise tags */}
{suggestion.mentor?.expertiseTags && suggestion.mentor.expertiseTags.length > 0 && (
<div className="flex flex-wrap gap-1 mt-2">
{suggestion.mentor.expertiseTags.map((tag) => (
<Badge key={tag} variant="secondary" className="text-xs">
{tag}
</Badge>
))}
</div>
)}
{/* Match scores */}
<div className="mt-3 space-y-2">
<div className="flex items-center gap-2 text-sm">
<span className="text-muted-foreground w-28">Confidence:</span>
<Progress value={suggestion.confidenceScore * 100} className="flex-1 h-2" />
<span className="w-12 text-right">{(suggestion.confidenceScore * 100).toFixed(0)}%</span>
</div>
<div className="flex items-center gap-2 text-sm">
<span className="text-muted-foreground w-28">Expertise Match:</span>
<Progress value={suggestion.expertiseMatchScore * 100} className="flex-1 h-2" />
<span className="w-12 text-right">{(suggestion.expertiseMatchScore * 100).toFixed(0)}%</span>
</div>
</div>
{/* AI Reasoning */}
{suggestion.reasoning && (
<p className="mt-2 text-sm text-muted-foreground italic">
&quot;{suggestion.reasoning}&quot;
</p>
)}
</div>
</div>
<Button
onClick={() => handleAssign(suggestion.mentorId, suggestion)}
disabled={assignMutation.isPending}
variant={selectedMentorId === suggestion.mentorId ? 'default' : 'outline'}
>
{assignMutation.isPending && selectedMentorId === suggestion.mentorId ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<>
<Check className="mr-2 h-4 w-4" />
Assign
</>
)}
</Button>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
{/* Manual Assignment */}
<Card>
<CardHeader>
<CardTitle className="text-lg flex items-center gap-2">
<User className="h-5 w-5" />
Manual Assignment
</CardTitle>
<CardDescription>
Search and select a mentor manually
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Use the AI suggestions above or search for a specific user in the Users section
to assign them as a mentor manually.
</p>
</CardContent>
</Card>
</>
)}
</div>
)
}
function MentorAssignmentSkeleton() {
return (
<div className="space-y-6">
<Skeleton className="h-9 w-36" />
<div className="space-y-2">
<Skeleton className="h-8 w-64" />
<Skeleton className="h-4 w-48" />
</div>
<Card>
<CardHeader>
<Skeleton className="h-6 w-48" />
<Skeleton className="h-4 w-64" />
</CardHeader>
<CardContent>
<div className="space-y-4">
{[1, 2, 3].map((i) => (
<Skeleton key={i} className="h-24 w-full" />
))}
</div>
</CardContent>
</Card>
</div>
)
}
export default function MentorAssignmentPage({ params }: PageProps) {
const { id } = use(params)
return (
<Suspense fallback={<MentorAssignmentSkeleton />}>
<MentorAssignmentContent projectId={id} />
</Suspense>
)
}

File diff suppressed because it is too large Load Diff