diff --git a/src/app/(applicant)/applicant/mentor/page.tsx b/src/app/(applicant)/applicant/mentor/page.tsx
index 6a8d4a8..683208d 100644
--- a/src/app/(applicant)/applicant/mentor/page.tsx
+++ b/src/app/(applicant)/applicant/mentor/page.tsx
@@ -1,151 +1,230 @@
-'use client'
-
-import { useSession } from 'next-auth/react'
-import { trpc } from '@/lib/trpc/client'
-import {
- Card,
- CardContent,
- CardDescription,
- CardHeader,
- CardTitle,
-} from '@/components/ui/card'
-import { Skeleton } from '@/components/ui/skeleton'
-import { MentorChat } from '@/components/shared/mentor-chat'
-import { WorkspaceFilesPanel } from '@/components/mentor/workspace-files-panel'
-import {
- MessageSquare,
- UserCircle,
- FileText,
-} from 'lucide-react'
-
-export default function ApplicantMentorPage() {
- const { data: session, status: sessionStatus } = useSession()
- const isAuthenticated = sessionStatus === 'authenticated'
-
- const { data: dashboardData, isLoading: dashLoading } = trpc.applicant.getMyDashboard.useQuery(
- undefined,
- { enabled: isAuthenticated }
- )
-
- const projectId = dashboardData?.project?.id
-
- const { data: mentorMessages, isLoading: messagesLoading } = trpc.applicant.getMentorMessages.useQuery(
- { projectId: projectId! },
- { enabled: !!projectId }
- )
-
- const utils = trpc.useUtils()
- const sendMessage = trpc.applicant.sendMentorMessage.useMutation({
- onSuccess: () => {
- utils.applicant.getMentorMessages.invalidate({ projectId: projectId! })
- },
- })
-
- if (dashLoading) {
- return (
-
- )
- }
-
- if (!projectId) {
- return (
-
-
-
Mentor
-
-
-
-
- No Project
-
- Submit a project first to communicate with your mentor.
-
-
-
-
- )
- }
-
- // TODO(PR8 Task 7): show ALL assigned mentors. For now we display only the
- // first one until the multi-mentor applicant UI ships.
- const primaryAssignment = dashboardData?.project?.mentorAssignments?.[0] ?? null
- const mentor = primaryAssignment?.mentor
-
- return (
-
- {/* Header */}
-
-
-
- Mentor Communication
-
-
- Chat with your assigned mentor
-
-
-
- {/* Mentor info */}
- {mentor ? (
-
-
-
-
-
-
{mentor.name || 'Mentor'}
-
{mentor.email}
-
-
-
-
- ) : (
-
-
-
-
- No mentor has been assigned to your project yet.
- You'll be notified when a mentor is assigned.
-
-
-
- )}
-
- {/* Chat */}
- {mentor && (
-
-
- Messages
-
- Your conversation history with {mentor.name || 'your mentor'}
-
-
-
- {
- await sendMessage.mutateAsync({ projectId: projectId!, message })
- }}
- isLoading={messagesLoading}
- isSending={sendMessage.isPending}
- />
-
-
- )}
-
- {/* Files */}
- {primaryAssignment?.id && projectId && (
-
- )}
-
- )
-}
+'use client'
+
+import { useState } from 'react'
+import { useSession } from 'next-auth/react'
+import { format } from 'date-fns'
+import { trpc } from '@/lib/trpc/client'
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/components/ui/card'
+import { Badge } from '@/components/ui/badge'
+import { Button } from '@/components/ui/button'
+import { Skeleton } from '@/components/ui/skeleton'
+import { MentorChat } from '@/components/shared/mentor-chat'
+import { WorkspaceFilesPanel } from '@/components/mentor/workspace-files-panel'
+import { RequestChangeDialog } from './request-change-dialog'
+import {
+ MessageSquare,
+ UserCircle,
+ FileText,
+ UserCog,
+} from 'lucide-react'
+
+export default function ApplicantMentorPage() {
+ const { data: session, status: sessionStatus } = useSession()
+ const isAuthenticated = sessionStatus === 'authenticated'
+
+ const { data: dashboardData, isLoading: dashLoading } = trpc.applicant.getMyDashboard.useQuery(
+ undefined,
+ { enabled: isAuthenticated }
+ )
+
+ const projectId = dashboardData?.project?.id
+
+ const { data: mentorMessages, isLoading: messagesLoading } = trpc.applicant.getMentorMessages.useQuery(
+ { projectId: projectId! },
+ { enabled: !!projectId }
+ )
+
+ const utils = trpc.useUtils()
+ const sendMessage = trpc.applicant.sendMentorMessage.useMutation({
+ onSuccess: () => {
+ utils.applicant.getMentorMessages.invalidate({ projectId: projectId! })
+ },
+ })
+
+ const [isChangeOpen, setIsChangeOpen] = useState(false)
+
+ if (dashLoading) {
+ return (
+
+ )
+ }
+
+ if (!projectId) {
+ return (
+
+
+
Mentor
+
+
+
+
+ No Project
+
+ Submit a project first to communicate with your mentor.
+
+
+
+
+ )
+ }
+
+ const assignments = dashboardData?.project?.mentorAssignments ?? []
+ const hasMentors = assignments.length > 0
+ const primaryAssignment = assignments[0] ?? null
+ const primaryMentor = primaryAssignment?.mentor
+ const hasPendingChangeRequest = !!dashboardData?.hasPendingMentorChangeRequest
+
+ const dialogMentors = assignments
+ .filter((a) => !!a.mentor)
+ .map((a) => ({
+ assignmentId: a.id,
+ name: a.mentor?.name || a.mentor?.email || 'Mentor',
+ }))
+
+ const teamHeading = assignments.length > 1 ? 'Your mentor team' : 'Your mentor'
+
+ return (
+
+ {/* Header */}
+
+
+
+ Mentor Communication
+
+
+ {assignments.length > 1
+ ? 'Chat with your assigned mentor team'
+ : 'Chat with your assigned mentor'}
+
+
+
+ {/* Mentor list */}
+ {hasMentors ? (
+
+ {teamHeading}
+
+ {assignments.map((assignment) => {
+ const mentor = assignment.mentor
+ if (!mentor) return null
+ const expertise = mentor.expertiseTags ?? []
+ return (
+
+
+
+
+
+
+ {mentor.name || 'Mentor'}
+
+
+ {mentor.email}
+
+ {assignment.assignedAt && (
+
+ Assigned since {format(new Date(assignment.assignedAt), 'MMM d, yyyy')}
+
+ )}
+
+
+ {expertise.length > 0 && (
+
+ {expertise.map((tag) => (
+
+ {tag}
+
+ ))}
+
+ )}
+
+
+ )
+ })}
+
+
+ {/* Request change action */}
+
+
+ {hasPendingChangeRequest
+ ? "You have a pending mentor change request — admins will follow up soon."
+ : 'Need a different match? Let the program admins know.'}
+
+
setIsChangeOpen(true)}
+ disabled={hasPendingChangeRequest}
+ >
+
+ {hasPendingChangeRequest ? 'Change requested' : 'Request a mentor change'}
+
+
+
+ ) : (
+
+
+
+
+ No mentor has been assigned to your project yet.
+ You'll be notified when a mentor is assigned.
+
+
+
+ )}
+
+ {/* Chat */}
+ {primaryMentor && (
+
+
+ Messages
+
+ {assignments.length > 1
+ ? 'Your conversation history with your mentor team'
+ : `Your conversation history with ${primaryMentor.name || 'your mentor'}`}
+
+
+
+ {
+ await sendMessage.mutateAsync({ projectId: projectId!, message })
+ }}
+ isLoading={messagesLoading}
+ isSending={sendMessage.isPending}
+ />
+
+
+ )}
+
+ {/* Files */}
+ {primaryAssignment?.id && projectId && (
+
+ )}
+
+ {/* Request change dialog */}
+ {projectId && (
+
+ )}
+
+ )
+}
diff --git a/src/app/(applicant)/applicant/mentor/request-change-dialog.tsx b/src/app/(applicant)/applicant/mentor/request-change-dialog.tsx
new file mode 100644
index 0000000..acfc4b1
--- /dev/null
+++ b/src/app/(applicant)/applicant/mentor/request-change-dialog.tsx
@@ -0,0 +1,179 @@
+'use client'
+
+import { useEffect, useState } from 'react'
+import { toast } from 'sonner'
+import { Loader2 } from 'lucide-react'
+
+import { trpc } from '@/lib/trpc/client'
+import { Button } from '@/components/ui/button'
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog'
+import { Label } from '@/components/ui/label'
+import { Textarea } from '@/components/ui/textarea'
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select'
+
+const REASON_MIN = 10
+const REASON_MAX = 2000
+const TARGET_ANY = '__any__'
+
+type MentorOption = {
+ assignmentId: string
+ name: string
+}
+
+type RequestChangeDialogProps = {
+ projectId: string
+ mentors: MentorOption[]
+ open: boolean
+ onOpenChange: (open: boolean) => void
+}
+
+export function RequestChangeDialog({
+ projectId,
+ mentors,
+ open,
+ onOpenChange,
+}: RequestChangeDialogProps) {
+ const [reason, setReason] = useState('')
+ const [target, setTarget] = useState(TARGET_ANY)
+ const [touched, setTouched] = useState(false)
+
+ const utils = trpc.useUtils()
+ const requestChange = trpc.mentor.requestChange.useMutation({
+ onSuccess: async () => {
+ toast.success(
+ "Your request has been sent to the program admins. We'll review it and follow up.",
+ )
+ onOpenChange(false)
+ // Refresh dashboard so the disabled state for the button updates.
+ await utils.applicant.getMyDashboard.invalidate()
+ },
+ onError: (error) => {
+ toast.error(error.message || 'Could not send your request. Please try again.')
+ },
+ })
+
+ // Reset form when the dialog is closed.
+ useEffect(() => {
+ if (!open) {
+ setReason('')
+ setTarget(TARGET_ANY)
+ setTouched(false)
+ }
+ }, [open])
+
+ const trimmedReason = reason.trim()
+ const reasonTooShort = trimmedReason.length < REASON_MIN
+ const reasonTooLong = trimmedReason.length > REASON_MAX
+ const reasonInvalid = reasonTooShort || reasonTooLong
+ const showReasonError = touched && reasonInvalid
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault()
+ setTouched(true)
+ if (reasonInvalid) return
+
+ requestChange.mutate({
+ projectId,
+ targetAssignmentId: target === TARGET_ANY ? undefined : target,
+ reason: trimmedReason,
+ })
+ }
+
+ return (
+
+
+
+ Request a mentor change
+
+ Share a few details so the program admins can follow up with you.
+ Your current mentor will not see this message.
+
+
+
+
+
+ )
+}
diff --git a/src/server/routers/applicant.ts b/src/server/routers/applicant.ts
index cb88368..2636452 100644
--- a/src/server/routers/applicant.ts
+++ b/src/server/routers/applicant.ts
@@ -1319,9 +1319,10 @@ export const applicantRouter = router({
mentorAssignments: {
include: {
mentor: {
- select: { id: true, name: true, email: true },
+ select: { id: true, name: true, email: true, expertiseTags: true },
},
},
+ orderBy: { assignedAt: 'asc' },
},
wonAwards: {
select: { id: true, name: true },
@@ -1492,6 +1493,17 @@ export const applicantRouter = router({
logoUrl = await provider.getDownloadUrl(project.logoKey)
}
+ // Does this user have an open mentor-change request for this project?
+ // (Used by the applicant mentor page to disable the "Request a change" button.)
+ const myPendingChangeRequest = await ctx.prisma.mentorChangeRequest.findFirst({
+ where: {
+ projectId: project.id,
+ requestedByUserId: ctx.user.id,
+ status: 'PENDING',
+ },
+ select: { id: true },
+ })
+
return {
project: {
...project,
@@ -1505,6 +1517,7 @@ export const applicantRouter = router({
hasPassedIntake: !!passedIntake,
isIntakeOpen: !!activeIntakeRound,
logoUrl,
+ hasPendingMentorChangeRequest: !!myPendingChangeRequest,
}
}),