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.'} +

+ +
+
+ ) : ( + + + +

+ 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. + + +
+ {mentors.length > 0 && ( +
+ + +

+ Optional. Use this if your request is about one of your co-mentors in particular. +

+
+ )} +
+ +