feat(mentor): mentoring-specific Round Overview card grid (§B)
Renders above Round Details when round.roundType === 'MENTORING': - Top-line counts: requested + assigned (with awaiting badge) - Request window: countdown pill (amber <48h, red <12h) - Mentor pool: size + avg load + 'View all' link to /admin/mentors - Workspace activity: msgs / files / milestones / last activity Round Details panel now shows 'Mentor Pool: N members' (linked) instead of an always-empty 'Jury Group' row on MENTORING rounds. Plan: docs/superpowers/plans/2026-04-28-pr5-mentor-round-overview.md
This commit is contained in:
217
src/components/admin/round/mentoring-round-overview.tsx
Normal file
217
src/components/admin/round/mentoring-round-overview.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { trpc } from '@/lib/trpc/client'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import {
|
||||
ArrowRight,
|
||||
Clock,
|
||||
FileText,
|
||||
MessageCircle,
|
||||
Target,
|
||||
UserCheck,
|
||||
Users,
|
||||
} from 'lucide-react'
|
||||
|
||||
interface Props {
|
||||
roundId: string
|
||||
}
|
||||
|
||||
function formatRelativeFuture(date: Date | null): { label: string; tone: 'normal' | 'amber' | 'red' } {
|
||||
if (!date) return { label: '—', tone: 'normal' }
|
||||
const ms = date.getTime() - Date.now()
|
||||
if (ms <= 0) return { label: 'Closed', tone: 'red' }
|
||||
const hours = Math.floor(ms / 3_600_000)
|
||||
const days = Math.floor(hours / 24)
|
||||
const tone: 'normal' | 'amber' | 'red' =
|
||||
hours <= 12 ? 'red' : hours <= 48 ? 'amber' : 'normal'
|
||||
const label = days > 0 ? `Closes in ${days}d` : `Closes in ${hours}h`
|
||||
return { label, tone }
|
||||
}
|
||||
|
||||
function formatRelativePast(date: Date | null): string {
|
||||
if (!date) return '—'
|
||||
const ms = Date.now() - date.getTime()
|
||||
const minutes = Math.floor(ms / 60_000)
|
||||
const hours = Math.floor(ms / 3_600_000)
|
||||
const days = Math.floor(ms / 86_400_000)
|
||||
if (days > 0) return `${days}d ago`
|
||||
if (hours > 0) return `${hours}h ago`
|
||||
return `${Math.max(0, minutes)}m ago`
|
||||
}
|
||||
|
||||
export function MentoringRoundOverview({ roundId }: Props) {
|
||||
const { data: stats, isLoading: statsLoading } = trpc.mentor.getRoundStats.useQuery(
|
||||
{ roundId },
|
||||
{ refetchInterval: 30_000 },
|
||||
)
|
||||
const { data: pool, isLoading: poolLoading } = trpc.mentor.getMentorPool.useQuery({})
|
||||
|
||||
if (statsLoading || poolLoading) {
|
||||
return (
|
||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<Skeleton key={i} className="h-32 w-full rounded-md" />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (!stats || !pool) return null
|
||||
|
||||
const requestedPct = stats.totalProjects
|
||||
? Math.round((stats.requestedCount / stats.totalProjects) * 100)
|
||||
: 0
|
||||
const assignedPct = stats.totalProjects
|
||||
? Math.round((stats.assignedCount / stats.totalProjects) * 100)
|
||||
: 0
|
||||
|
||||
const window = formatRelativeFuture(
|
||||
stats.requestWindow.deadline ? new Date(stats.requestWindow.deadline) : null,
|
||||
)
|
||||
|
||||
const avgLoad =
|
||||
pool.poolSize > 0 ? (pool.totalCurrentAssignments / pool.poolSize).toFixed(1) : '—'
|
||||
const lastActivity = stats.workspaceActivity.lastActivityAt
|
||||
? new Date(stats.workspaceActivity.lastActivityAt)
|
||||
: null
|
||||
|
||||
return (
|
||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-muted-foreground text-xs font-medium uppercase tracking-wide">
|
||||
Requested mentoring
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold tabular-nums">
|
||||
{stats.requestedCount}
|
||||
<span className="text-muted-foreground ml-2 text-sm font-normal">
|
||||
/ {stats.totalProjects}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-muted-foreground mt-1 text-xs">{requestedPct}% of round</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-muted-foreground text-xs font-medium uppercase tracking-wide">
|
||||
Mentor assigned
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<div className="text-3xl font-bold tabular-nums">{stats.assignedCount}</div>
|
||||
<UserCheck className="text-muted-foreground h-5 w-5" />
|
||||
</div>
|
||||
<p className="text-muted-foreground mt-1 text-xs">
|
||||
{assignedPct}% of round{' '}
|
||||
{stats.awaitingAssignment > 0 && (
|
||||
<span className="text-amber-700 dark:text-amber-400">
|
||||
· {stats.awaitingAssignment} awaiting
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-muted-foreground text-xs font-medium uppercase tracking-wide">
|
||||
Request window
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<Clock className="text-muted-foreground h-5 w-5" />
|
||||
<Badge
|
||||
variant={
|
||||
window.tone === 'red'
|
||||
? 'destructive'
|
||||
: window.tone === 'amber'
|
||||
? 'secondary'
|
||||
: 'outline'
|
||||
}
|
||||
className="text-sm"
|
||||
>
|
||||
{window.label}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-muted-foreground mt-2 text-xs">
|
||||
{stats.requestWindow.deadline
|
||||
? `Closes ${new Date(stats.requestWindow.deadline).toLocaleDateString()} · ${stats.requestWindow.deadlineDays}-day window`
|
||||
: 'No window deadline set'}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-muted-foreground text-xs font-medium uppercase tracking-wide">
|
||||
Mentor pool
|
||||
</CardTitle>
|
||||
<Link
|
||||
href="/admin/mentors"
|
||||
className="text-muted-foreground hover:text-foreground inline-flex items-center text-xs"
|
||||
>
|
||||
View all
|
||||
<ArrowRight className="ml-0.5 h-3 w-3" />
|
||||
</Link>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<div className="text-3xl font-bold tabular-nums">{pool.poolSize}</div>
|
||||
<Users className="text-muted-foreground h-5 w-5" />
|
||||
</div>
|
||||
<p className="text-muted-foreground mt-1 text-xs">
|
||||
Avg load <span className="text-foreground font-medium">{avgLoad}</span> ·{' '}
|
||||
{pool.totalCurrentAssignments} active
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="md:col-span-2 xl:col-span-4">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm">Workspace activity</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-x-6 gap-y-3 text-sm md:grid-cols-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<MessageCircle className="text-muted-foreground h-4 w-4" />
|
||||
<div>
|
||||
<div className="font-bold tabular-nums">{stats.workspaceActivity.messageCount}</div>
|
||||
<div className="text-muted-foreground text-xs">messages</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="text-muted-foreground h-4 w-4" />
|
||||
<div>
|
||||
<div className="font-bold tabular-nums">{stats.workspaceActivity.fileCount}</div>
|
||||
<div className="text-muted-foreground text-xs">files</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Target className="text-muted-foreground h-4 w-4" />
|
||||
<div>
|
||||
<div className="font-bold tabular-nums">
|
||||
{stats.workspaceActivity.milestoneCount}
|
||||
</div>
|
||||
<div className="text-muted-foreground text-xs">milestones</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="text-muted-foreground h-4 w-4" />
|
||||
<div>
|
||||
<div className="font-medium">{formatRelativePast(lastActivity)}</div>
|
||||
<div className="text-muted-foreground text-xs">last activity</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user