Add jury assignment transfer, cap redistribution, and learning hub overhaul
All checks were successful
Build and Push Docker Image / build (push) Successful in 12m19s
All checks were successful
Build and Push Docker Image / build (push) Successful in 12m19s
- Add getTransferCandidates/transferAssignments procedures for targeted assignment moves between jurors with TOCTOU guards and audit logging - Add getOverCapPreview/redistributeOverCap for auto-redistributing assignments when a juror's cap is lowered below their current load - Add TransferAssignmentsDialog (2-step: select projects, pick destinations) - Extend InlineMemberCap with over-cap detection and redistribute banner - Extend getReassignmentHistory to show ASSIGNMENT_TRANSFER and CAP_REDISTRIBUTE events - Learning hub: replace ResourceType/CohortLevel enums with accessJson JSONB, add coverImageKey, resource detail pages for jury/mentor, shared renderer - Migration: 20260221200000_learning_hub_overhaul Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
122
src/app/(jury)/jury/learning/[id]/page.tsx
Normal file
122
src/app/(jury)/jury/learning/[id]/page.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { trpc } from '@/lib/trpc/client'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import {
|
||||
ArrowLeft,
|
||||
Download,
|
||||
ExternalLink,
|
||||
AlertCircle,
|
||||
} from 'lucide-react'
|
||||
|
||||
const ResourceRenderer = dynamic(
|
||||
() => import('@/components/shared/resource-renderer').then((mod) => mod.ResourceRenderer),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<div className="mx-auto max-w-3xl min-h-[200px] rounded-lg border bg-muted/20 animate-pulse" />
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
export default function JuryResourceDetailPage() {
|
||||
const params = useParams()
|
||||
const resourceId = params.id as string
|
||||
|
||||
const { data: resource, isLoading, error } = trpc.learningResource.get.useQuery({ id: resourceId })
|
||||
const logAccess = trpc.learningResource.logAccess.useMutation()
|
||||
const utils = trpc.useUtils()
|
||||
|
||||
// Log access on mount
|
||||
useEffect(() => {
|
||||
if (resourceId) {
|
||||
logAccess.mutate({ id: resourceId })
|
||||
}
|
||||
}, [resourceId])
|
||||
|
||||
const handleDownload = async () => {
|
||||
try {
|
||||
const { url } = await utils.learningResource.getDownloadUrl.fetch({ id: resourceId })
|
||||
window.open(url, '_blank')
|
||||
} catch {
|
||||
// silently fail
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Skeleton className="h-8 w-20" />
|
||||
<div className="mx-auto max-w-3xl space-y-4">
|
||||
<Skeleton className="h-10 w-2/3" />
|
||||
<Skeleton className="h-6 w-1/3" />
|
||||
<Skeleton className="h-px w-full" />
|
||||
<Skeleton className="h-64 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error || !resource) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Resource not found</AlertTitle>
|
||||
<AlertDescription>
|
||||
This resource may have been removed or you don't have access.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<Button asChild>
|
||||
<Link href="/jury/learning">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back to Learning Hub
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Button variant="ghost" asChild className="-ml-4">
|
||||
<Link href="/jury/learning">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back to Learning Hub
|
||||
</Link>
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
{resource.externalUrl && (
|
||||
<a href={resource.externalUrl} target="_blank" rel="noopener noreferrer">
|
||||
<Button variant="outline" size="sm">
|
||||
<ExternalLink className="mr-2 h-4 w-4" />
|
||||
Open Link
|
||||
</Button>
|
||||
</a>
|
||||
)}
|
||||
{resource.objectKey && (
|
||||
<Button variant="outline" size="sm" onClick={handleDownload}>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Download
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<ResourceRenderer
|
||||
title={resource.title}
|
||||
description={resource.description}
|
||||
contentJson={resource.contentJson}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user