Initial commit: MOPC platform with Docker deployment setup

Full Next.js 15 platform with tRPC, Prisma, PostgreSQL, NextAuth.
Includes production Dockerfile (multi-stage, port 7600), docker-compose
with registry-based image pull, Gitea Actions CI workflow, nginx config
for portal.monaco-opc.com, deployment scripts, and DEPLOYMENT.md guide.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-30 13:41:32 +01:00
commit a606292aaa
290 changed files with 70691 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
import type { Metadata } from 'next'
export const metadata: Metadata = { title: 'Mentor Dashboard' }
export default function MentorPageLayout({ children }: { children: React.ReactNode }) {
return children
}

View File

@@ -0,0 +1,252 @@
'use client'
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 { Avatar, AvatarFallback } from '@/components/ui/avatar'
import {
Users,
Briefcase,
ArrowRight,
Mail,
MapPin,
GraduationCap,
Waves,
Crown,
} from 'lucide-react'
import { getInitials, formatDateOnly } from '@/lib/utils'
// Status badge colors
const statusColors: Record<string, 'default' | 'secondary' | 'destructive' | 'outline'> = {
SUBMITTED: 'secondary',
ELIGIBLE: 'default',
ASSIGNED: 'default',
SEMIFINALIST: 'default',
FINALIST: 'default',
REJECTED: 'destructive',
}
function DashboardSkeleton() {
return (
<div className="space-y-6">
<div>
<Skeleton className="h-8 w-48" />
<Skeleton className="h-4 w-64 mt-2" />
</div>
<div className="grid gap-4 md:grid-cols-2">
<Skeleton className="h-24" />
<Skeleton className="h-24" />
</div>
<Skeleton className="h-6 w-32" />
<div className="grid gap-4">
<Skeleton className="h-40" />
<Skeleton className="h-40" />
</div>
</div>
)
}
export default function MentorDashboard() {
const { data: assignments, isLoading } = trpc.mentor.getMyProjects.useQuery()
if (isLoading) {
return <DashboardSkeleton />
}
const projects = assignments || []
return (
<div className="space-y-6">
{/* Header */}
<div>
<h1 className="text-2xl font-semibold tracking-tight">
Mentor Dashboard
</h1>
<p className="text-muted-foreground">
View and manage your assigned mentee projects
</p>
</div>
{/* Stats */}
<div className="grid gap-4 md:grid-cols-2">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Assigned Projects
</CardTitle>
<Briefcase className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{projects.length}</div>
<p className="text-xs text-muted-foreground">
Projects you are mentoring
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total Team Members
</CardTitle>
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{projects.reduce(
(acc, a) => acc + (a.project.teamMembers?.length || 0),
0
)}
</div>
<p className="text-xs text-muted-foreground">
Across all assigned projects
</p>
</CardContent>
</Card>
</div>
{/* Projects List */}
<div>
<h2 className="text-lg font-semibold mb-4">Your Mentees</h2>
{projects.length === 0 ? (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-muted">
<Users className="h-6 w-6 text-muted-foreground" />
</div>
<p className="mt-4 font-medium">No assigned projects yet</p>
<p className="text-sm text-muted-foreground mt-1">
You will see your mentee projects here once they are assigned to
you.
</p>
</CardContent>
</Card>
) : (
<div className="grid gap-4">
{projects.map((assignment) => {
const project = assignment.project
const teamLead = project.teamMembers?.find(
(m) => m.role === 'LEAD'
)
return (
<Card key={assignment.id}>
<CardHeader>
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
<div className="space-y-1">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span>
{project.round.program.name} {project.round.program.year}
</span>
<span></span>
<span>{project.round.name}</span>
</div>
<CardTitle className="flex items-center gap-2">
{project.title}
<Badge
variant={statusColors[project.status] || 'secondary'}
>
{project.status.replace('_', ' ')}
</Badge>
</CardTitle>
{project.teamName && (
<CardDescription>{project.teamName}</CardDescription>
)}
</div>
<Button variant="outline" size="sm" asChild>
<Link href={`/mentor/projects/${project.id}` as Route}>
View Details
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</div>
</CardHeader>
<CardContent className="space-y-4">
{/* Category badges */}
<div className="flex flex-wrap gap-2">
{project.competitionCategory && (
<Badge variant="outline" className="gap-1">
<GraduationCap className="h-3 w-3" />
{project.competitionCategory === 'STARTUP'
? 'Start-up'
: 'Business Concept'}
</Badge>
)}
{project.oceanIssue && (
<Badge variant="outline" className="gap-1">
<Waves className="h-3 w-3" />
{project.oceanIssue.replace(/_/g, ' ')}
</Badge>
)}
{project.country && (
<Badge variant="outline" className="gap-1">
<MapPin className="h-3 w-3" />
{project.country}
</Badge>
)}
</div>
{/* Description preview */}
{project.description && (
<p className="text-sm text-muted-foreground line-clamp-2">
{project.description}
</p>
)}
{/* Team Lead Info */}
{teamLead && (
<div className="flex items-center gap-3 pt-2 border-t">
<Avatar className="h-8 w-8">
<AvatarFallback className="text-xs">
<Crown className="h-4 w-4 text-yellow-500" />
</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium">
{teamLead.user.name || 'Unnamed'}{' '}
<span className="text-muted-foreground font-normal">
(Team Lead)
</span>
</p>
<a
href={`mailto:${teamLead.user.email}`}
className="text-xs text-muted-foreground hover:text-primary flex items-center gap-1"
>
<Mail className="h-3 w-3" />
{teamLead.user.email}
</a>
</div>
<div className="text-xs text-muted-foreground">
{project.teamMembers?.length || 0} team member
{(project.teamMembers?.length || 0) !== 1 ? 's' : ''}
</div>
</div>
)}
{/* Assignment date */}
<p className="text-xs text-muted-foreground">
Assigned {formatDateOnly(assignment.assignedAt)}
</p>
</CardContent>
</Card>
)
})}
</div>
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,415 @@
'use client'
import { Suspense, use } 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 { FileViewer } from '@/components/shared/file-viewer'
import { ProjectLogoWithUrl } from '@/components/shared/project-logo-with-url'
import {
ArrowLeft,
AlertCircle,
Users,
MapPin,
Waves,
GraduationCap,
Crown,
Mail,
Phone,
Calendar,
FileText,
ExternalLink,
} from 'lucide-react'
import { formatDateOnly, getInitials } from '@/lib/utils'
interface PageProps {
params: Promise<{ id: string }>
}
// Status badge colors
const statusColors: Record<string, 'default' | 'secondary' | 'destructive' | 'outline'> = {
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,
})
if (isLoading) {
return <ProjectDetailSkeleton />
}
if (error || !project) {
return (
<div className="space-y-6">
<Button variant="ghost" asChild className="-ml-4">
<Link href={'/mentor' as Route}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Dashboard
</Link>
</Button>
<Card>
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
<AlertCircle className="h-12 w-12 text-destructive/50" />
<p className="mt-2 font-medium">
{error?.message || 'Project Not Found'}
</p>
<p className="text-sm text-muted-foreground mt-1">
You may not have access to view this project.
</p>
<Button asChild className="mt-4">
<Link href={'/mentor' as Route}>Back to Dashboard</Link>
</Button>
</CardContent>
</Card>
</div>
)
}
const teamLead = project.teamMembers?.find((m) => m.role === 'LEAD')
const otherMembers = project.teamMembers?.filter((m) => m.role !== 'LEAD') || []
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center gap-4">
<Button variant="ghost" asChild className="-ml-4">
<Link href={'/mentor' as Route}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Dashboard
</Link>
</Button>
</div>
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div className="flex items-start gap-4">
<ProjectLogoWithUrl
project={project}
size="lg"
fallback="initials"
/>
<div className="space-y-1">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span>
{project.round.program.name} {project.round.program.year}
</span>
<span></span>
<span>{project.round.name}</span>
</div>
<div className="flex items-center gap-3">
<h1 className="text-2xl font-semibold tracking-tight">
{project.title}
</h1>
<Badge variant={statusColors[project.status] || 'secondary'}>
{project.status.replace('_', ' ')}
</Badge>
</div>
{project.teamName && (
<p className="text-muted-foreground">{project.teamName}</p>
)}
</div>
</div>
</div>
{project.assignedAt && (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Calendar className="h-4 w-4" />
<span>Assigned to you on {formatDateOnly(project.assignedAt)}</span>
</div>
)}
<Separator />
{/* Project Info */}
<Card>
<CardHeader>
<CardTitle className="text-lg">Project Information</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* Category & Ocean Issue badges */}
<div className="flex flex-wrap gap-2">
{project.competitionCategory && (
<Badge variant="outline" className="gap-1">
<GraduationCap className="h-3 w-3" />
{project.competitionCategory === 'STARTUP' ? 'Start-up' : 'Business Concept'}
</Badge>
)}
{project.oceanIssue && (
<Badge variant="outline" className="gap-1">
<Waves className="h-3 w-3" />
{project.oceanIssue.replace(/_/g, ' ')}
</Badge>
)}
</div>
{project.description && (
<div>
<p className="text-sm font-medium text-muted-foreground mb-1">
Description
</p>
<p className="text-sm whitespace-pre-wrap">{project.description}</p>
</div>
)}
{/* Location & Institution */}
<div className="grid gap-4 sm:grid-cols-2">
{(project.country || project.geographicZone) && (
<div className="flex items-start gap-2">
<MapPin className="h-4 w-4 text-muted-foreground mt-0.5" />
<div>
<p className="text-sm font-medium text-muted-foreground">Location</p>
<p className="text-sm">
{[project.geographicZone, project.country].filter(Boolean).join(', ')}
</p>
</div>
</div>
)}
{project.institution && (
<div className="flex items-start gap-2">
<GraduationCap className="h-4 w-4 text-muted-foreground mt-0.5" />
<div>
<p className="text-sm font-medium text-muted-foreground">Institution</p>
<p className="text-sm">{project.institution}</p>
</div>
</div>
)}
</div>
{/* Submission URLs */}
{(project.phase1SubmissionUrl || project.phase2SubmissionUrl) && (
<div className="space-y-2">
<p className="text-sm font-medium text-muted-foreground">Submission Links</p>
<div className="flex flex-wrap gap-2">
{project.phase1SubmissionUrl && (
<Button variant="outline" size="sm" asChild>
<a href={project.phase1SubmissionUrl} target="_blank" rel="noopener noreferrer">
<ExternalLink className="mr-2 h-4 w-4" />
Phase 1 Submission
</a>
</Button>
)}
{project.phase2SubmissionUrl && (
<Button variant="outline" size="sm" asChild>
<a href={project.phase2SubmissionUrl} target="_blank" rel="noopener noreferrer">
<ExternalLink className="mr-2 h-4 w-4" />
Phase 2 Submission
</a>
</Button>
)}
</div>
</div>
)}
{project.tags && project.tags.length > 0 && (
<div>
<p className="text-sm font-medium text-muted-foreground mb-2">Tags</p>
<div className="flex flex-wrap gap-2">
{project.tags.map((tag) => (
<Badge key={tag} variant="secondary">
{tag}
</Badge>
))}
</div>
</div>
)}
</CardContent>
</Card>
{/* Team Members Section */}
<Card>
<CardHeader>
<CardTitle className="text-lg flex items-center gap-2">
<Users className="h-5 w-5" />
Team Members ({project.teamMembers?.length || 0})
</CardTitle>
<CardDescription>
Contact information for the project team
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* Team Lead */}
{teamLead && (
<div className="p-4 rounded-lg border bg-muted/30">
<div className="flex items-start gap-3">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-yellow-100">
<Crown className="h-6 w-6 text-yellow-600" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<p className="font-medium">{teamLead.user.name || 'Unnamed'}</p>
<Badge variant="secondary" className="text-xs">Team Lead</Badge>
</div>
{teamLead.title && (
<p className="text-sm text-muted-foreground mb-2">{teamLead.title}</p>
)}
<div className="flex flex-wrap gap-4 text-sm">
<a
href={`mailto:${teamLead.user.email}`}
className="flex items-center gap-1 text-primary hover:underline"
>
<Mail className="h-4 w-4" />
{teamLead.user.email}
</a>
{teamLead.user.phoneNumber && (
<a
href={`tel:${teamLead.user.phoneNumber}`}
className="flex items-center gap-1 text-primary hover:underline"
>
<Phone className="h-4 w-4" />
{teamLead.user.phoneNumber}
</a>
)}
</div>
</div>
</div>
</div>
)}
{/* Other Team Members */}
{otherMembers.length > 0 && (
<div className="grid gap-3 sm:grid-cols-2">
{otherMembers.map((member) => (
<div key={member.id} className="flex items-start gap-3 p-3 rounded-lg border">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-muted">
<span className="text-sm font-medium">
{getInitials(member.user.name || member.user.email)}
</span>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<p className="font-medium text-sm truncate">
{member.user.name || 'Unnamed'}
</p>
<Badge variant="outline" className="text-xs">
{member.role === 'ADVISOR' ? 'Advisor' : 'Member'}
</Badge>
</div>
{member.title && (
<p className="text-xs text-muted-foreground">{member.title}</p>
)}
<a
href={`mailto:${member.user.email}`}
className="text-xs text-muted-foreground hover:text-primary flex items-center gap-1 mt-1"
>
<Mail className="h-3 w-3" />
{member.user.email}
</a>
</div>
</div>
))}
</div>
)}
{!project.teamMembers?.length && (
<p className="text-sm text-muted-foreground text-center py-4">
No team members listed for this project.
</p>
)}
</CardContent>
</Card>
{/* Files Section */}
<Card>
<CardHeader>
<CardTitle className="text-lg flex items-center gap-2">
<FileText className="h-5 w-5" />
Project Files
</CardTitle>
<CardDescription>
Documents and materials submitted by the team
</CardDescription>
</CardHeader>
<CardContent>
{project.files && project.files.length > 0 ? (
<FileViewer
files={project.files.map((f) => ({
id: f.id,
fileName: f.fileName,
fileType: f.fileType,
mimeType: f.mimeType,
size: f.size,
bucket: f.bucket,
objectKey: f.objectKey,
}))}
/>
) : (
<p className="text-sm text-muted-foreground text-center py-6">
No files have been uploaded for this project yet.
</p>
)}
</CardContent>
</Card>
</div>
)
}
function ProjectDetailSkeleton() {
return (
<div className="space-y-6">
<Skeleton className="h-9 w-36" />
<div className="flex items-start gap-4">
<Skeleton className="h-16 w-16 rounded-lg" />
<div className="space-y-2">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-8 w-64" />
<Skeleton className="h-4 w-40" />
</div>
</div>
<Skeleton className="h-px w-full" />
<Card>
<CardHeader>
<Skeleton className="h-5 w-40" />
</CardHeader>
<CardContent>
<Skeleton className="h-24 w-full" />
</CardContent>
</Card>
<Card>
<CardHeader>
<Skeleton className="h-5 w-32" />
</CardHeader>
<CardContent>
<div className="space-y-3">
<Skeleton className="h-20 w-full" />
<div className="grid gap-3 sm:grid-cols-2">
<Skeleton className="h-16 w-full" />
<Skeleton className="h-16 w-full" />
</div>
</div>
</CardContent>
</Card>
</div>
)
}
export default function MentorProjectDetailPage({ params }: PageProps) {
const { id } = use(params)
return (
<Suspense fallback={<ProjectDetailSkeleton />}>
<ProjectDetailContent projectId={id} />
</Suspense>
)
}

View File

@@ -0,0 +1,192 @@
'use client'
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 {
Users,
ArrowRight,
Mail,
MapPin,
GraduationCap,
Waves,
Crown,
} from 'lucide-react'
import { formatDateOnly } from '@/lib/utils'
// Status badge colors
const statusColors: Record<string, 'default' | 'secondary' | 'destructive' | 'outline'> = {
SUBMITTED: 'secondary',
ELIGIBLE: 'default',
ASSIGNED: 'default',
SEMIFINALIST: 'default',
FINALIST: 'default',
REJECTED: 'destructive',
}
function ProjectsSkeleton() {
return (
<div className="space-y-6">
<div>
<Skeleton className="h-8 w-32" />
<Skeleton className="h-4 w-48 mt-2" />
</div>
<div className="grid gap-4">
<Skeleton className="h-48" />
<Skeleton className="h-48" />
</div>
</div>
)
}
export default function MentorProjectsPage() {
const { data: assignments, isLoading } = trpc.mentor.getMyProjects.useQuery()
if (isLoading) {
return <ProjectsSkeleton />
}
const projects = assignments || []
return (
<div className="space-y-6">
{/* Header */}
<div>
<h1 className="text-2xl font-semibold tracking-tight">My Mentees</h1>
<p className="text-muted-foreground">
All projects assigned to you for mentorship
</p>
</div>
{/* Projects List */}
{projects.length === 0 ? (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-muted">
<Users className="h-6 w-6 text-muted-foreground" />
</div>
<p className="mt-4 font-medium">No assigned projects yet</p>
<p className="text-sm text-muted-foreground mt-1">
You will see your mentee projects here once they are assigned to you.
</p>
</CardContent>
</Card>
) : (
<div className="grid gap-4">
{projects.map((assignment) => {
const project = assignment.project
const teamLead = project.teamMembers?.find((m) => m.role === 'LEAD')
return (
<Card key={assignment.id}>
<CardHeader>
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
<div className="space-y-1">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span>
{project.round.program.name} {project.round.program.year}
</span>
<span></span>
<span>{project.round.name}</span>
</div>
<CardTitle className="flex items-center gap-2">
{project.title}
<Badge variant={statusColors[project.status] || 'secondary'}>
{project.status.replace('_', ' ')}
</Badge>
</CardTitle>
{project.teamName && (
<CardDescription>{project.teamName}</CardDescription>
)}
</div>
<Button variant="outline" size="sm" asChild>
<Link href={`/mentor/projects/${project.id}` as Route}>
View Details
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</div>
</CardHeader>
<CardContent className="space-y-4">
{/* Category badges */}
<div className="flex flex-wrap gap-2">
{project.competitionCategory && (
<Badge variant="outline" className="gap-1">
<GraduationCap className="h-3 w-3" />
{project.competitionCategory === 'STARTUP'
? 'Start-up'
: 'Business Concept'}
</Badge>
)}
{project.oceanIssue && (
<Badge variant="outline" className="gap-1">
<Waves className="h-3 w-3" />
{project.oceanIssue.replace(/_/g, ' ')}
</Badge>
)}
{project.country && (
<Badge variant="outline" className="gap-1">
<MapPin className="h-3 w-3" />
{project.country}
</Badge>
)}
</div>
{/* Description preview */}
{project.description && (
<p className="text-sm text-muted-foreground line-clamp-2">
{project.description}
</p>
)}
{/* Team Lead Info */}
{teamLead && (
<div className="flex items-center gap-3 pt-2 border-t">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-yellow-100">
<Crown className="h-4 w-4 text-yellow-600" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium">
{teamLead.user.name || 'Unnamed'}{' '}
<span className="text-muted-foreground font-normal">
(Team Lead)
</span>
</p>
<a
href={`mailto:${teamLead.user.email}`}
className="text-xs text-muted-foreground hover:text-primary flex items-center gap-1"
>
<Mail className="h-3 w-3" />
{teamLead.user.email}
</a>
</div>
<div className="text-xs text-muted-foreground">
{project.teamMembers?.length || 0} team member
{(project.teamMembers?.length || 0) !== 1 ? 's' : ''}
</div>
</div>
)}
{/* Assignment date */}
<p className="text-xs text-muted-foreground">
Assigned {formatDateOnly(assignment.assignedAt)}
</p>
</CardContent>
</Card>
)
})}
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,159 @@
'use client'
import { useState } from 'react'
import { trpc } from '@/lib/trpc/client'
import {
Card,
CardContent,
} from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
import {
FileText,
Video,
Link as LinkIcon,
File,
Download,
ExternalLink,
BookOpen,
} from 'lucide-react'
const resourceTypeIcons = {
PDF: FileText,
VIDEO: Video,
DOCUMENT: File,
LINK: LinkIcon,
OTHER: File,
}
const cohortColors: Record<string, string> = {
ALL: 'bg-gray-100 text-gray-800',
SEMIFINALIST: 'bg-blue-100 text-blue-800',
FINALIST: 'bg-purple-100 text-purple-800',
}
export default function MentorResourcesPage() {
const [downloadingId, setDownloadingId] = useState<string | null>(null)
const { data, isLoading } = trpc.learningResource.myResources.useQuery({})
const utils = trpc.useUtils()
const handleDownload = async (resourceId: string) => {
setDownloadingId(resourceId)
try {
const { url } = await utils.learningResource.getDownloadUrl.fetch({ id: resourceId })
window.open(url, '_blank')
} catch (error) {
console.error('Download failed:', error)
} finally {
setDownloadingId(null)
}
}
if (isLoading) {
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold tracking-tight">Mentor Resources</h1>
<p className="text-muted-foreground">
Guides and materials to help you mentor effectively
</p>
</div>
<div className="grid gap-4">
{[1, 2, 3].map((i) => (
<Card key={i}>
<CardContent className="flex items-center gap-4 py-4">
<Skeleton className="h-10 w-10 rounded-lg" />
<div className="flex-1 space-y-2">
<Skeleton className="h-5 w-48" />
<Skeleton className="h-4 w-32" />
</div>
</CardContent>
</Card>
))}
</div>
</div>
)
}
const resources = data?.resources || []
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold tracking-tight">Mentor Resources</h1>
<p className="text-muted-foreground">
Guides and materials to help you mentor effectively
</p>
</div>
{resources.length === 0 ? (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12">
<BookOpen className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-medium mb-2">No resources available yet</h3>
<p className="text-muted-foreground text-center max-w-md">
Mentor guides and training materials will appear here once they are published by the program administrators.
</p>
</CardContent>
</Card>
) : (
<div className="grid gap-4">
{resources.map((resource) => {
const Icon = resourceTypeIcons[resource.resourceType as keyof typeof resourceTypeIcons] || File
const isDownloading = downloadingId === resource.id
return (
<Card key={resource.id}>
<CardContent className="flex items-center gap-4 py-4">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-muted shrink-0">
<Icon className="h-5 w-5" />
</div>
<div className="flex-1 min-w-0">
<h3 className="font-medium">{resource.title}</h3>
{resource.description && (
<p className="text-sm text-muted-foreground mt-1 line-clamp-2">
{resource.description}
</p>
)}
<div className="flex items-center gap-2 mt-2">
<Badge variant="outline" className={cohortColors[resource.cohortLevel] || cohortColors.ALL}>
{resource.cohortLevel}
</Badge>
<Badge variant="secondary">
{resource.resourceType}
</Badge>
</div>
</div>
<div>
{resource.externalUrl ? (
<a
href={resource.externalUrl}
target="_blank"
rel="noopener noreferrer"
>
<Button>
<ExternalLink className="mr-2 h-4 w-4" />
Open
</Button>
</a>
) : resource.objectKey ? (
<Button
onClick={() => handleDownload(resource.id)}
disabled={isDownloading}
>
<Download className="mr-2 h-4 w-4" />
{isDownloading ? 'Loading...' : 'Download'}
</Button>
) : null}
</div>
</CardContent>
</Card>
)
})}
</div>
)}
</div>
)
}