2026-04-28 15:28:09 +02:00
|
|
|
'use client'
|
Add profile settings page, mentor management, and S3 email logos
- Add universal /settings/profile page accessible to all roles with
avatar upload, bio, phone, password change, and account deletion
- Expand updateProfile endpoint to accept bio (metadataJson), phone,
and notification preference
- Add deleteAccount endpoint with password confirmation
- Add Profile Settings link to all nav components (admin, jury, mentor,
observer)
- Add /admin/mentors list page and /admin/mentors/[id] detail page for
mentor management
- Add Mentors nav item to admin sidebar
- Update email logo URLs to S3 (s3.monaco-opc.com/public/)
- Add ocean.png background image to email wrapper
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 19:57:12 +01:00
|
|
|
|
2026-04-28 15:28:09 +02:00
|
|
|
import { useMemo, useState } from 'react'
|
|
|
|
|
import Link from 'next/link'
|
|
|
|
|
import { trpc } from '@/lib/trpc/client'
|
|
|
|
|
import {
|
|
|
|
|
Card,
|
|
|
|
|
CardContent,
|
|
|
|
|
CardHeader,
|
|
|
|
|
CardTitle,
|
|
|
|
|
} from '@/components/ui/card'
|
|
|
|
|
import { Input } from '@/components/ui/input'
|
|
|
|
|
import { Badge } from '@/components/ui/badge'
|
|
|
|
|
import { Button } from '@/components/ui/button'
|
|
|
|
|
import { Skeleton } from '@/components/ui/skeleton'
|
2026-04-28 16:47:53 +02:00
|
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
2026-04-28 15:28:09 +02:00
|
|
|
import {
|
|
|
|
|
Table,
|
|
|
|
|
TableBody,
|
|
|
|
|
TableCell,
|
|
|
|
|
TableHead,
|
|
|
|
|
TableHeader,
|
|
|
|
|
TableRow,
|
|
|
|
|
} from '@/components/ui/table'
|
2026-04-28 16:47:53 +02:00
|
|
|
import { ArrowUpDown, GraduationCap, Search, Users } from 'lucide-react'
|
|
|
|
|
import { formatEnumLabel } from '@/lib/utils'
|
2026-04-28 19:52:17 +02:00
|
|
|
import { MentorDetailSheet } from '@/components/admin/mentor/mentor-detail-sheet'
|
2026-04-28 15:28:09 +02:00
|
|
|
|
|
|
|
|
type SortKey = 'name' | 'load' | 'capacity' | 'lastActivity'
|
|
|
|
|
|
|
|
|
|
function formatRelativePast(date: Date | string | null): string {
|
|
|
|
|
if (!date) return '—'
|
|
|
|
|
const d = typeof date === 'string' ? new Date(date) : date
|
|
|
|
|
const ms = Date.now() - d.getTime()
|
|
|
|
|
const days = Math.floor(ms / 86_400_000)
|
|
|
|
|
const hours = Math.floor(ms / 3_600_000)
|
|
|
|
|
if (days >= 1) return `${days}d ago`
|
|
|
|
|
if (hours >= 1) return `${hours}h ago`
|
|
|
|
|
const minutes = Math.floor(ms / 60_000)
|
|
|
|
|
return `${Math.max(0, minutes)}m ago`
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 16:47:53 +02:00
|
|
|
const STATUS_BADGE: Record<
|
|
|
|
|
'unassigned' | 'assigned' | 'active' | 'stalled',
|
|
|
|
|
{ label: string; variant: 'default' | 'secondary' | 'destructive' | 'outline' }
|
|
|
|
|
> = {
|
|
|
|
|
unassigned: { label: 'Unassigned', variant: 'outline' },
|
|
|
|
|
assigned: { label: 'Assigned', variant: 'secondary' },
|
|
|
|
|
active: { label: 'Active', variant: 'default' },
|
|
|
|
|
stalled: { label: 'Stalled', variant: 'destructive' },
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 15:28:09 +02:00
|
|
|
type Mentor = {
|
|
|
|
|
id: string
|
|
|
|
|
name: string | null
|
|
|
|
|
email: string
|
|
|
|
|
country: string | null
|
|
|
|
|
expertiseTags: string[]
|
|
|
|
|
currentAssignments: number
|
|
|
|
|
completedAssignments: number
|
|
|
|
|
maxAssignments: number | null
|
|
|
|
|
capacityRemaining: number | null
|
|
|
|
|
lastActivityAt: Date | string | null
|
2026-04-28 19:52:17 +02:00
|
|
|
activeTeams: { id: string; title: string }[]
|
2026-04-28 15:28:09 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-28 16:47:53 +02:00
|
|
|
function MentorListPanel() {
|
2026-04-28 15:28:09 +02:00
|
|
|
const [search, setSearch] = useState('')
|
|
|
|
|
const [sortKey, setSortKey] = useState<SortKey>('load')
|
|
|
|
|
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc')
|
2026-04-28 19:52:17 +02:00
|
|
|
const [detailMentorId, setDetailMentorId] = useState<string | null>(null)
|
2026-04-28 15:28:09 +02:00
|
|
|
|
|
|
|
|
const { data, isLoading } = trpc.mentor.getMentorPool.useQuery({})
|
|
|
|
|
|
|
|
|
|
const filtered = useMemo<Mentor[]>(() => {
|
|
|
|
|
if (!data) return []
|
|
|
|
|
const q = search.trim().toLowerCase()
|
|
|
|
|
let rows: Mentor[] = data.mentors
|
|
|
|
|
if (q) {
|
|
|
|
|
rows = rows.filter((m) =>
|
|
|
|
|
[m.name ?? '', m.email, m.country ?? '', ...m.expertiseTags]
|
|
|
|
|
.join(' ')
|
|
|
|
|
.toLowerCase()
|
|
|
|
|
.includes(q),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
rows = [...rows].sort((a, b) => {
|
|
|
|
|
let av: string | number = 0
|
|
|
|
|
let bv: string | number = 0
|
|
|
|
|
switch (sortKey) {
|
|
|
|
|
case 'name':
|
|
|
|
|
av = (a.name ?? '').toLowerCase()
|
|
|
|
|
bv = (b.name ?? '').toLowerCase()
|
|
|
|
|
break
|
|
|
|
|
case 'load':
|
|
|
|
|
av = a.currentAssignments
|
|
|
|
|
bv = b.currentAssignments
|
|
|
|
|
break
|
|
|
|
|
case 'capacity':
|
|
|
|
|
av = a.capacityRemaining ?? Number.MAX_SAFE_INTEGER
|
|
|
|
|
bv = b.capacityRemaining ?? Number.MAX_SAFE_INTEGER
|
|
|
|
|
break
|
|
|
|
|
case 'lastActivity':
|
|
|
|
|
av = a.lastActivityAt ? new Date(a.lastActivityAt).getTime() : 0
|
|
|
|
|
bv = b.lastActivityAt ? new Date(b.lastActivityAt).getTime() : 0
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
if (av < bv) return sortDir === 'asc' ? -1 : 1
|
|
|
|
|
if (av > bv) return sortDir === 'asc' ? 1 : -1
|
|
|
|
|
return 0
|
|
|
|
|
})
|
|
|
|
|
return rows
|
|
|
|
|
}, [data, search, sortKey, sortDir])
|
|
|
|
|
|
|
|
|
|
const toggleSort = (key: SortKey) => {
|
|
|
|
|
if (sortKey === key) setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'))
|
|
|
|
|
else {
|
|
|
|
|
setSortKey(key)
|
|
|
|
|
setSortDir(key === 'name' ? 'asc' : 'desc')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const SortHeader = ({
|
|
|
|
|
k,
|
|
|
|
|
children,
|
|
|
|
|
align = 'left',
|
|
|
|
|
}: {
|
|
|
|
|
k: SortKey
|
|
|
|
|
children: React.ReactNode
|
|
|
|
|
align?: 'left' | 'right'
|
|
|
|
|
}) => (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => toggleSort(k)}
|
|
|
|
|
className={`flex items-center gap-1 text-sm font-medium ${align === 'right' ? 'ml-auto' : ''}`}
|
|
|
|
|
>
|
|
|
|
|
{children}
|
|
|
|
|
<ArrowUpDown
|
|
|
|
|
className={`h-3 w-3 ${sortKey === k ? 'text-foreground' : 'text-muted-foreground/50'}`}
|
|
|
|
|
/>
|
|
|
|
|
</button>
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader className="space-y-4">
|
|
|
|
|
<CardTitle className="text-base">Mentor list</CardTitle>
|
|
|
|
|
<div className="relative">
|
|
|
|
|
<Search className="text-muted-foreground absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2" />
|
|
|
|
|
<Input
|
|
|
|
|
value={search}
|
|
|
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
|
|
|
placeholder="Search by name, email, country, or expertise tag…"
|
|
|
|
|
className="pl-9"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
{isLoading ? (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
{[1, 2, 3, 4, 5].map((i) => (
|
|
|
|
|
<Skeleton key={i} className="h-12 w-full" />
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
) : filtered.length === 0 ? (
|
|
|
|
|
<div className="text-muted-foreground py-12 text-center text-sm">
|
|
|
|
|
{search ? 'No matching mentors.' : 'No mentors yet.'}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="overflow-hidden rounded-md border">
|
|
|
|
|
<Table>
|
|
|
|
|
<TableHeader>
|
|
|
|
|
<TableRow>
|
|
|
|
|
<TableHead>
|
|
|
|
|
<SortHeader k="name">Mentor</SortHeader>
|
|
|
|
|
</TableHead>
|
|
|
|
|
<TableHead>Expertise</TableHead>
|
|
|
|
|
<TableHead>Country</TableHead>
|
|
|
|
|
<TableHead className="text-right">
|
|
|
|
|
<SortHeader k="capacity" align="right">
|
|
|
|
|
Capacity
|
|
|
|
|
</SortHeader>
|
|
|
|
|
</TableHead>
|
|
|
|
|
<TableHead>
|
|
|
|
|
<SortHeader k="lastActivity">Last activity</SortHeader>
|
|
|
|
|
</TableHead>
|
2026-04-28 19:52:17 +02:00
|
|
|
<TableHead>
|
|
|
|
|
<SortHeader k="load">Teams</SortHeader>
|
|
|
|
|
</TableHead>
|
2026-04-28 15:28:09 +02:00
|
|
|
</TableRow>
|
|
|
|
|
</TableHeader>
|
|
|
|
|
<TableBody>
|
|
|
|
|
{filtered.map((m) => (
|
2026-04-28 19:52:17 +02:00
|
|
|
<TableRow
|
|
|
|
|
key={m.id}
|
|
|
|
|
className="cursor-pointer"
|
|
|
|
|
onClick={() => setDetailMentorId(m.id)}
|
|
|
|
|
>
|
2026-04-28 15:28:09 +02:00
|
|
|
<TableCell>
|
2026-04-28 19:52:17 +02:00
|
|
|
<div className="font-medium">{m.name ?? 'Unnamed'}</div>
|
|
|
|
|
<div className="text-muted-foreground text-xs">{m.email}</div>
|
2026-04-28 15:28:09 +02:00
|
|
|
</TableCell>
|
|
|
|
|
<TableCell>
|
|
|
|
|
<div className="flex flex-wrap gap-1">
|
|
|
|
|
{m.expertiseTags.slice(0, 4).map((tag) => (
|
|
|
|
|
<Badge key={tag} variant="secondary" className="text-xs">
|
|
|
|
|
{tag}
|
|
|
|
|
</Badge>
|
|
|
|
|
))}
|
|
|
|
|
{m.expertiseTags.length > 4 && (
|
|
|
|
|
<Badge variant="outline" className="text-xs">
|
|
|
|
|
+{m.expertiseTags.length - 4}
|
|
|
|
|
</Badge>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell className="text-sm">{m.country ?? '—'}</TableCell>
|
|
|
|
|
<TableCell className="text-right text-sm tabular-nums">
|
|
|
|
|
{m.capacityRemaining != null ? m.capacityRemaining : '∞'}
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell className="text-sm">
|
|
|
|
|
{formatRelativePast(m.lastActivityAt)}
|
|
|
|
|
</TableCell>
|
2026-04-28 19:52:17 +02:00
|
|
|
<TableCell>
|
|
|
|
|
{m.activeTeams.length === 0 ? (
|
|
|
|
|
<span className="text-muted-foreground text-xs">—</span>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="flex flex-wrap gap-1">
|
|
|
|
|
{m.activeTeams.slice(0, 2).map((t) => (
|
|
|
|
|
<Badge
|
|
|
|
|
key={t.id}
|
|
|
|
|
variant="outline"
|
|
|
|
|
className="max-w-[12rem] truncate text-xs"
|
|
|
|
|
title={t.title}
|
|
|
|
|
>
|
|
|
|
|
{t.title}
|
|
|
|
|
</Badge>
|
|
|
|
|
))}
|
|
|
|
|
{m.activeTeams.length > 2 && (
|
|
|
|
|
<Badge variant="outline" className="text-xs">
|
|
|
|
|
+{m.activeTeams.length - 2}
|
|
|
|
|
</Badge>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</TableCell>
|
2026-04-28 15:28:09 +02:00
|
|
|
</TableRow>
|
|
|
|
|
))}
|
|
|
|
|
</TableBody>
|
|
|
|
|
</Table>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
2026-04-28 19:52:17 +02:00
|
|
|
|
|
|
|
|
<MentorDetailSheet
|
|
|
|
|
mentorId={detailMentorId}
|
|
|
|
|
open={!!detailMentorId}
|
|
|
|
|
onOpenChange={(next) => {
|
|
|
|
|
if (!next) setDetailMentorId(null)
|
|
|
|
|
}}
|
|
|
|
|
/>
|
2026-04-28 15:28:09 +02:00
|
|
|
</div>
|
|
|
|
|
)
|
Add profile settings page, mentor management, and S3 email logos
- Add universal /settings/profile page accessible to all roles with
avatar upload, bio, phone, password change, and account deletion
- Expand updateProfile endpoint to accept bio (metadataJson), phone,
and notification preference
- Add deleteAccount endpoint with password confirmation
- Add Profile Settings link to all nav components (admin, jury, mentor,
observer)
- Add /admin/mentors list page and /admin/mentors/[id] detail page for
mentor management
- Add Mentors nav item to admin sidebar
- Update email logo URLs to S3 (s3.monaco-opc.com/public/)
- Add ocean.png background image to email wrapper
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 19:57:12 +01:00
|
|
|
}
|
2026-04-28 16:47:53 +02:00
|
|
|
|
|
|
|
|
type StatusFilter = 'all' | 'unassigned' | 'assigned' | 'active' | 'stalled'
|
|
|
|
|
|
|
|
|
|
function MenteeActivityPanel() {
|
|
|
|
|
const [search, setSearch] = useState('')
|
|
|
|
|
const [statusFilter, setStatusFilter] = useState<StatusFilter>('all')
|
|
|
|
|
|
|
|
|
|
const { data, isLoading } = trpc.mentor.getMenteeActivity.useQuery({})
|
|
|
|
|
|
|
|
|
|
const filtered = useMemo(() => {
|
|
|
|
|
if (!data) return []
|
|
|
|
|
const q = search.trim().toLowerCase()
|
|
|
|
|
return data.rows.filter((r) => {
|
|
|
|
|
if (statusFilter !== 'all' && r.status !== statusFilter) return false
|
|
|
|
|
if (!q) return true
|
|
|
|
|
const hay = [
|
|
|
|
|
r.project.title,
|
|
|
|
|
r.project.country ?? '',
|
|
|
|
|
r.teamLead?.name ?? '',
|
|
|
|
|
r.teamLead?.email ?? '',
|
|
|
|
|
r.mentor?.name ?? '',
|
|
|
|
|
r.mentor?.email ?? '',
|
|
|
|
|
]
|
|
|
|
|
.join(' ')
|
|
|
|
|
.toLowerCase()
|
|
|
|
|
return hay.includes(q)
|
|
|
|
|
})
|
|
|
|
|
}, [data, search, statusFilter])
|
|
|
|
|
|
|
|
|
|
const totals = data?.totals ?? { unassigned: 0, assigned: 0, active: 0, stalled: 0 }
|
|
|
|
|
|
|
|
|
|
const StatusPill = ({ value, label, count }: { value: StatusFilter; label: string; count: number }) => (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setStatusFilter(value)}
|
|
|
|
|
className={`rounded-md border px-2.5 py-1 text-xs font-medium transition-colors ${
|
|
|
|
|
statusFilter === value
|
|
|
|
|
? 'bg-primary text-primary-foreground border-primary'
|
|
|
|
|
: 'bg-background hover:bg-muted'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{label} <span className="tabular-nums opacity-80">({count})</span>
|
|
|
|
|
</button>
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader className="space-y-4">
|
|
|
|
|
<div className="flex items-center justify-between gap-4">
|
|
|
|
|
<CardTitle className="text-base">Mentee teams</CardTitle>
|
|
|
|
|
<div className="flex flex-wrap gap-1.5">
|
|
|
|
|
<StatusPill
|
|
|
|
|
value="all"
|
|
|
|
|
label="All"
|
|
|
|
|
count={
|
|
|
|
|
totals.unassigned + totals.assigned + totals.active + totals.stalled
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
<StatusPill value="unassigned" label="Unassigned" count={totals.unassigned} />
|
|
|
|
|
<StatusPill value="assigned" label="Assigned" count={totals.assigned} />
|
|
|
|
|
<StatusPill value="active" label="Active" count={totals.active} />
|
|
|
|
|
<StatusPill value="stalled" label="Stalled" count={totals.stalled} />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="relative">
|
|
|
|
|
<Search className="text-muted-foreground absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2" />
|
|
|
|
|
<Input
|
|
|
|
|
value={search}
|
|
|
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
|
|
|
placeholder="Search by project, team lead, or mentor…"
|
|
|
|
|
className="pl-9"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
{isLoading ? (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
{[1, 2, 3, 4, 5].map((i) => (
|
|
|
|
|
<Skeleton key={i} className="h-14 w-full" />
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
) : filtered.length === 0 ? (
|
|
|
|
|
<div className="text-muted-foreground py-12 text-center text-sm">
|
|
|
|
|
{search || statusFilter !== 'all'
|
|
|
|
|
? 'No matching teams.'
|
|
|
|
|
: 'No teams have requested mentorship yet.'}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="overflow-hidden rounded-md border">
|
|
|
|
|
<Table>
|
|
|
|
|
<TableHeader>
|
|
|
|
|
<TableRow>
|
|
|
|
|
<TableHead>Project</TableHead>
|
|
|
|
|
<TableHead>Status</TableHead>
|
|
|
|
|
<TableHead>Mentor</TableHead>
|
|
|
|
|
<TableHead className="text-right">Messages</TableHead>
|
|
|
|
|
<TableHead className="text-right">Files</TableHead>
|
|
|
|
|
<TableHead>Last activity</TableHead>
|
|
|
|
|
<TableHead></TableHead>
|
|
|
|
|
</TableRow>
|
|
|
|
|
</TableHeader>
|
|
|
|
|
<TableBody>
|
|
|
|
|
{filtered.map((r) => {
|
|
|
|
|
const badge = STATUS_BADGE[r.status]
|
|
|
|
|
return (
|
|
|
|
|
<TableRow key={r.project.id}>
|
|
|
|
|
<TableCell>
|
|
|
|
|
<div className="font-medium">{r.project.title}</div>
|
|
|
|
|
<div className="text-muted-foreground text-xs">
|
|
|
|
|
{r.teamLead?.name ?? r.teamLead?.email ?? '—'}
|
|
|
|
|
{r.project.oceanIssue && (
|
|
|
|
|
<>
|
|
|
|
|
{' · '}
|
|
|
|
|
{formatEnumLabel(r.project.oceanIssue)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell>
|
|
|
|
|
<Badge variant={badge.variant} className="text-xs">
|
|
|
|
|
{badge.label}
|
|
|
|
|
</Badge>
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell>
|
|
|
|
|
{r.mentor ? (
|
|
|
|
|
<div className="text-sm">
|
|
|
|
|
<div>{r.mentor.name ?? r.mentor.email}</div>
|
|
|
|
|
<div className="text-muted-foreground text-xs tabular-nums">
|
|
|
|
|
{r.mentor.currentLoad}
|
|
|
|
|
{r.mentor.maxAssignments != null
|
|
|
|
|
? `/${r.mentor.maxAssignments}`
|
|
|
|
|
: ''}
|
|
|
|
|
{' load'}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<span className="text-muted-foreground text-sm">—</span>
|
|
|
|
|
)}
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell className="text-right tabular-nums">
|
|
|
|
|
{r.messageCount}
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell className="text-right tabular-nums">{r.fileCount}</TableCell>
|
|
|
|
|
<TableCell className="text-sm">
|
|
|
|
|
{formatRelativePast(r.lastActivityAt as unknown as Date | null)}
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell className="text-right">
|
|
|
|
|
<Button asChild size="sm" variant="outline">
|
|
|
|
|
<Link href={`/admin/projects/${r.project.id}/mentor`}>
|
|
|
|
|
{r.mentor ? 'Open' : 'Assign'}
|
|
|
|
|
</Link>
|
|
|
|
|
</Button>
|
|
|
|
|
</TableCell>
|
|
|
|
|
</TableRow>
|
|
|
|
|
)
|
|
|
|
|
})}
|
|
|
|
|
</TableBody>
|
|
|
|
|
</Table>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function MentorsListPage() {
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<h1 className="text-2xl font-semibold tracking-tight">Mentors</h1>
|
|
|
|
|
<p className="text-muted-foreground">
|
|
|
|
|
Manage the mentor pool and track mentee teams across the program.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<Button asChild variant="outline">
|
|
|
|
|
<Link href="/admin/members">
|
|
|
|
|
<Users className="mr-2 h-4 w-4" />
|
|
|
|
|
Manage Members
|
|
|
|
|
</Link>
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Tabs defaultValue="mentors" className="space-y-6">
|
|
|
|
|
<TabsList>
|
|
|
|
|
<TabsTrigger value="mentors">
|
|
|
|
|
<GraduationCap className="mr-2 h-4 w-4" /> Mentors
|
|
|
|
|
</TabsTrigger>
|
|
|
|
|
<TabsTrigger value="mentees">
|
|
|
|
|
<Users className="mr-2 h-4 w-4" /> Mentees & Activity
|
|
|
|
|
</TabsTrigger>
|
|
|
|
|
</TabsList>
|
|
|
|
|
<TabsContent value="mentors">
|
|
|
|
|
<MentorListPanel />
|
|
|
|
|
</TabsContent>
|
|
|
|
|
<TabsContent value="mentees">
|
|
|
|
|
<MenteeActivityPanel />
|
|
|
|
|
</TabsContent>
|
|
|
|
|
</Tabs>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|