- /applicant/mentor renders all co-mentors as cards - New "Request a mentor change" dialog opens a free-form reason + optional per-mentor target; calls mentor.requestChange and shows admin-routed confirmation toast - Pending-request guard disables the button until the admin resolves
231 lines
7.8 KiB
TypeScript
231 lines
7.8 KiB
TypeScript
'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 (
|
|
<div className="space-y-6">
|
|
<div className="space-y-2">
|
|
<Skeleton className="h-8 w-48" />
|
|
<Skeleton className="h-4 w-64" />
|
|
</div>
|
|
<Skeleton className="h-96 w-full" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (!projectId) {
|
|
return (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h1 className="text-2xl font-semibold tracking-tight">Mentor</h1>
|
|
</div>
|
|
<Card>
|
|
<CardContent className="flex flex-col items-center justify-center py-12">
|
|
<FileText className="h-12 w-12 text-muted-foreground/50 mb-4" />
|
|
<h2 className="text-xl font-semibold mb-2">No Project</h2>
|
|
<p className="text-muted-foreground text-center">
|
|
Submit a project first to communicate with your mentor.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
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 (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div>
|
|
<h1 className="text-2xl font-semibold tracking-tight flex items-center gap-2">
|
|
<MessageSquare className="h-6 w-6" />
|
|
Mentor Communication
|
|
</h1>
|
|
<p className="text-muted-foreground">
|
|
{assignments.length > 1
|
|
? 'Chat with your assigned mentor team'
|
|
: 'Chat with your assigned mentor'}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Mentor list */}
|
|
{hasMentors ? (
|
|
<section className="space-y-3">
|
|
<h2 className="text-lg font-semibold tracking-tight">{teamHeading}</h2>
|
|
<div className="grid gap-3 md:grid-cols-2">
|
|
{assignments.map((assignment) => {
|
|
const mentor = assignment.mentor
|
|
if (!mentor) return null
|
|
const expertise = mentor.expertiseTags ?? []
|
|
return (
|
|
<Card key={assignment.id} className="bg-muted/50">
|
|
<CardContent className="p-4 space-y-3">
|
|
<div className="flex items-start gap-3">
|
|
<UserCircle className="h-10 w-10 text-muted-foreground shrink-0" />
|
|
<div className="min-w-0 flex-1">
|
|
<p className="font-medium truncate">
|
|
{mentor.name || 'Mentor'}
|
|
</p>
|
|
<p className="text-sm text-muted-foreground truncate">
|
|
{mentor.email}
|
|
</p>
|
|
{assignment.assignedAt && (
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
Assigned since {format(new Date(assignment.assignedAt), 'MMM d, yyyy')}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
{expertise.length > 0 && (
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{expertise.map((tag) => (
|
|
<Badge key={tag} variant="secondary" className="font-normal">
|
|
{tag}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
{/* Request change action */}
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 pt-1">
|
|
<p className="text-sm text-muted-foreground">
|
|
{hasPendingChangeRequest
|
|
? "You have a pending mentor change request — admins will follow up soon."
|
|
: 'Need a different match? Let the program admins know.'}
|
|
</p>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setIsChangeOpen(true)}
|
|
disabled={hasPendingChangeRequest}
|
|
>
|
|
<UserCog className="mr-2 h-4 w-4" />
|
|
{hasPendingChangeRequest ? 'Change requested' : 'Request a mentor change'}
|
|
</Button>
|
|
</div>
|
|
</section>
|
|
) : (
|
|
<Card className="bg-muted/50">
|
|
<CardContent className="flex flex-col items-center justify-center py-8">
|
|
<UserCircle className="h-12 w-12 text-muted-foreground/50 mb-3" />
|
|
<p className="text-muted-foreground text-center">
|
|
No mentor has been assigned to your project yet.
|
|
You'll be notified when a mentor is assigned.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Chat */}
|
|
{primaryMentor && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Messages</CardTitle>
|
|
<CardDescription>
|
|
{assignments.length > 1
|
|
? 'Your conversation history with your mentor team'
|
|
: `Your conversation history with ${primaryMentor.name || 'your mentor'}`}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<MentorChat
|
|
messages={mentorMessages || []}
|
|
currentUserId={session?.user?.id || ''}
|
|
onSendMessage={async (message) => {
|
|
await sendMessage.mutateAsync({ projectId: projectId!, message })
|
|
}}
|
|
isLoading={messagesLoading}
|
|
isSending={sendMessage.isPending}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Files */}
|
|
{primaryAssignment?.id && projectId && (
|
|
<WorkspaceFilesPanel
|
|
projectId={projectId}
|
|
mentorAssignmentId={primaryAssignment.id}
|
|
asApplicant
|
|
/>
|
|
)}
|
|
|
|
{/* Request change dialog */}
|
|
{projectId && (
|
|
<RequestChangeDialog
|
|
projectId={projectId}
|
|
mentors={dialogMentors}
|
|
open={isChangeOpen}
|
|
onOpenChange={setIsChangeOpen}
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|