Add jury assignment transfer, cap redistribution, and learning hub overhaul
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:
2026-02-21 18:50:29 +01:00
parent f42b452899
commit ee2f10e080
16 changed files with 2643 additions and 945 deletions

View File

@@ -2,9 +2,9 @@
import { useEffect, useMemo, useState } from 'react'
import { useCreateBlockNote } from '@blocknote/react'
import { BlockNoteView } from '@blocknote/mantine'
import { BlockNoteView } from '@blocknote/shadcn'
import '@blocknote/core/fonts/inter.css'
import '@blocknote/mantine/style.css'
import '@blocknote/shadcn/style.css'
import type { PartialBlock } from '@blocknote/core'

View File

@@ -0,0 +1,71 @@
'use client'
import dynamic from 'next/dynamic'
const BlockViewer = dynamic(
() => import('@/components/shared/block-editor').then((mod) => mod.BlockViewer),
{
ssr: false,
loading: () => (
<div className="min-h-[200px] rounded-lg border bg-muted/20 animate-pulse" />
),
}
)
interface ResourceRendererProps {
title: string
description?: string | null
contentJson: unknown // BlockNote PartialBlock[] stored as JSON
coverImageUrl?: string | null
className?: string
}
export function ResourceRenderer({
title,
description,
contentJson,
coverImageUrl,
className,
}: ResourceRendererProps) {
const contentString =
typeof contentJson === 'string' ? contentJson : JSON.stringify(contentJson)
return (
<article className={`mx-auto max-w-3xl ${className ?? ''}`}>
{/* Cover image */}
{coverImageUrl && (
<div className="mb-8 overflow-hidden rounded-lg">
<img
src={coverImageUrl}
alt=""
className="h-auto w-full object-cover"
/>
</div>
)}
{/* Title */}
<h1 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
{title}
</h1>
{/* Description */}
{description && (
<p className="mt-3 text-lg text-muted-foreground leading-relaxed">
{description}
</p>
)}
{/* Divider */}
<hr className="my-6 border-border" />
{/* Content */}
{contentJson ? (
<div className="prose-renderer">
<BlockViewer content={contentString} />
</div>
) : (
<p className="text-muted-foreground italic">No content</p>
)}
</article>
)
}