'use client' import { Suspense, use, useState, useEffect } from 'react' import Link from 'next/link' import type { Route } from 'next' import { trpc } from '@/lib/trpc/client' 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 { Separator } from '@/components/ui/separator' import { Textarea } from '@/components/ui/textarea' import { Checkbox } from '@/components/ui/checkbox' import { Label } from '@/components/ui/label' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog' import { FileViewer } from '@/components/shared/file-viewer' import { MentorChat } from '@/components/shared/mentor-chat' import { ProjectLogoWithUrl } from '@/components/shared/project-logo-with-url' import { ArrowLeft, AlertCircle, Users, MapPin, Waves, GraduationCap, Crown, Mail, Phone, Calendar, FileText, ExternalLink, MessageSquare, StickyNote, Plus, Pencil, Trash2, Loader2, Target, CheckCircle2, Circle, Eye, EyeOff, } from 'lucide-react' import { formatDateOnly, getInitials } from '@/lib/utils' import { toast } from 'sonner' interface PageProps { params: Promise<{ id: string }> } // Status badge colors const statusColors: Record = { SUBMITTED: 'secondary', ELIGIBLE: 'default', ASSIGNED: 'default', SEMIFINALIST: 'default', FINALIST: 'default', REJECTED: 'destructive', } function ProjectDetailContent({ projectId }: { projectId: string }) { const { data: project, isLoading, error } = trpc.mentor.getProjectDetail.useQuery({ projectId, }) const { data: mentorMessages, isLoading: messagesLoading } = trpc.mentor.getMessages.useQuery({ projectId, }) const utils = trpc.useUtils() const sendMessage = trpc.mentor.sendMessage.useMutation({ onSuccess: () => { utils.mentor.getMessages.invalidate({ projectId }) }, }) // Track view when project loads const trackView = trpc.mentor.trackView.useMutation() useEffect(() => { if (project?.mentorAssignment?.id) { trackView.mutate({ mentorAssignmentId: project.mentorAssignment.id }) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [project?.mentorAssignment?.id]) if (isLoading) { return } if (error || !project) { return (

{error?.message || 'Project Not Found'}

You may not have access to view this project.

) } const teamLead = project.teamMembers?.find((m) => m.role === 'LEAD') const otherMembers = project.teamMembers?.filter((m) => m.role !== 'LEAD') || [] const mentorAssignmentId = project.mentorAssignment?.id const programId = project.round?.program?.id return (
{/* Header */}
{project.round?.program?.year} Edition {project.round && ( <> - {project.round.name} )}

{project.title}

{project.status && ( {project.status.replace('_', ' ')} )}
{project.teamName && (

{project.teamName}

)}
{project.assignedAt && (
Assigned to you on {formatDateOnly(project.assignedAt)}
)} {/* Milestones Section */} {programId && mentorAssignmentId && ( )} {/* Private Notes Section */} {mentorAssignmentId && ( )} {/* Project Info */} Project Information {/* Category & Ocean Issue badges */}
{project.competitionCategory && ( {project.competitionCategory === 'STARTUP' ? 'Start-up' : 'Business Concept'} )} {project.oceanIssue && ( {project.oceanIssue.replace(/_/g, ' ')} )}
{project.description && (

Description

{project.description}

)} {/* Location & Institution */}
{(project.country || project.geographicZone) && (

Location

{[project.geographicZone, project.country].filter(Boolean).join(', ')}

)} {project.institution && (

Institution

{project.institution}

)}
{/* Submission URLs */} {(project.phase1SubmissionUrl || project.phase2SubmissionUrl) && (

Submission Links

{project.phase1SubmissionUrl && ( )} {project.phase2SubmissionUrl && ( )}
)} {project.tags && project.tags.length > 0 && (

Tags

{project.tags.map((tag) => ( {tag} ))}
)}
{/* Team Members Section */} Team Members ({project.teamMembers?.length || 0}) Contact information for the project team {/* Team Lead */} {teamLead && (

{teamLead.user.name || 'Unnamed'}

Team Lead
{teamLead.title && (

{teamLead.title}

)}
)} {/* Other Team Members */} {otherMembers.length > 0 && (
{otherMembers.map((member) => (
{getInitials(member.user.name || member.user.email)}

{member.user.name || 'Unnamed'}

{member.role === 'ADVISOR' ? 'Advisor' : 'Member'}
{member.title && (

{member.title}

)} {member.user.email}
))}
)} {!project.teamMembers?.length && (

No team members listed for this project.

)}
{/* Files Section */} Project Files Documents and materials submitted by the team {project.files && project.files.length > 0 ? ( ({ id: f.id, fileName: f.fileName, fileType: f.fileType, mimeType: f.mimeType, size: f.size, bucket: f.bucket, objectKey: f.objectKey, version: f.version, }))} /> ) : (

No files have been uploaded for this project yet.

)}
{/* Messaging Section */} Messages Communicate with the project team { await sendMessage.mutateAsync({ projectId, message }) }} isLoading={messagesLoading} isSending={sendMessage.isPending} />
) } // ============================================================================= // Milestones Section // ============================================================================= function MilestonesSection({ programId, mentorAssignmentId, }: { programId: string mentorAssignmentId: string }) { const { data: milestones, isLoading } = trpc.mentor.getMilestones.useQuery({ programId }) const utils = trpc.useUtils() const completeMutation = trpc.mentor.completeMilestone.useMutation({ onSuccess: (data) => { utils.mentor.getMilestones.invalidate({ programId }) if (data.allRequiredDone) { toast.success('All required milestones completed!') } else { toast.success('Milestone completed') } }, onError: (e) => toast.error(e.message), }) const uncompleteMutation = trpc.mentor.uncompleteMilestone.useMutation({ onSuccess: () => { utils.mentor.getMilestones.invalidate({ programId }) toast.success('Milestone unchecked') }, onError: (e) => toast.error(e.message), }) if (isLoading) { return (
) } if (!milestones || milestones.length === 0) { return null } const completedCount = milestones.filter( (m) => m.myCompletions.length > 0 ).length const totalRequired = milestones.filter((m) => m.isRequired).length const requiredCompleted = milestones.filter( (m) => m.isRequired && m.myCompletions.length > 0 ).length const handleToggle = (milestoneId: string, isCompleted: boolean) => { if (isCompleted) { uncompleteMutation.mutate({ milestoneId, mentorAssignmentId }) } else { completeMutation.mutate({ milestoneId, mentorAssignmentId }) } } return (
Milestones {completedCount}/{milestones.length} done
{totalRequired > 0 && ( {requiredCompleted}/{totalRequired} required milestones completed )}
{milestones.map((milestone) => { const isCompleted = milestone.myCompletions.length > 0 const isPending = completeMutation.isPending || uncompleteMutation.isPending return (
handleToggle(milestone.id, isCompleted)} className="mt-0.5" />

{milestone.name}

{milestone.isRequired && ( Required )}
{milestone.description && (

{milestone.description}

)} {isCompleted && milestone.myCompletions[0] && (

Completed {formatDateOnly(milestone.myCompletions[0].completedAt)}

)}
{isCompleted ? ( ) : ( )}
) })}
) } // ============================================================================= // Notes Section // ============================================================================= function NotesSection({ mentorAssignmentId }: { mentorAssignmentId: string }) { const [isAdding, setIsAdding] = useState(false) const [editingId, setEditingId] = useState(null) const [deleteId, setDeleteId] = useState(null) const [noteContent, setNoteContent] = useState('') const [isVisibleToAdmin, setIsVisibleToAdmin] = useState(true) const { data: notes, isLoading } = trpc.mentor.getNotes.useQuery({ mentorAssignmentId }) const utils = trpc.useUtils() const createMutation = trpc.mentor.createNote.useMutation({ onSuccess: () => { utils.mentor.getNotes.invalidate({ mentorAssignmentId }) toast.success('Note saved') resetForm() }, onError: (e) => toast.error(e.message), }) const updateMutation = trpc.mentor.updateNote.useMutation({ onSuccess: () => { utils.mentor.getNotes.invalidate({ mentorAssignmentId }) toast.success('Note updated') resetForm() }, onError: (e) => toast.error(e.message), }) const deleteMutation = trpc.mentor.deleteNote.useMutation({ onSuccess: () => { utils.mentor.getNotes.invalidate({ mentorAssignmentId }) toast.success('Note deleted') setDeleteId(null) }, onError: (e) => toast.error(e.message), }) const resetForm = () => { setIsAdding(false) setEditingId(null) setNoteContent('') setIsVisibleToAdmin(true) } const handleEdit = (note: { id: string; content: string; isVisibleToAdmin: boolean }) => { setEditingId(note.id) setNoteContent(note.content) setIsVisibleToAdmin(note.isVisibleToAdmin) setIsAdding(false) } const handleSubmit = () => { if (!noteContent.trim()) return if (editingId) { updateMutation.mutate({ noteId: editingId, content: noteContent.trim(), isVisibleToAdmin, }) } else { createMutation.mutate({ mentorAssignmentId, content: noteContent.trim(), isVisibleToAdmin, }) } } const isPending = createMutation.isPending || updateMutation.isPending return ( <>
Private Notes {!isAdding && !editingId && ( )}
Personal notes about this mentorship (private to you unless shared with admin)
{/* Add/Edit form */} {(isAdding || editingId) && (