Files
MOPC-Portal/src/components/observer/observer-edition-context.tsx
Matt 875c2e8f48
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
fix: security hardening — block self-registration, SSE auth, audit logging fixes
Security fixes:
- Block self-registration via magic link (PrismaAdapter createUser throws)
- Magic links only sent to existing ACTIVE users (prevents enumeration)
- signIn callback rejects non-existent users (defense-in-depth)
- Change schema default role from JURY_MEMBER to APPLICANT
- Add authentication to live-voting SSE stream endpoint
- Fix false FILE_OPENED/FILE_DOWNLOADED audit events on page load
  (remove purpose from eagerly pre-fetched URL queries)

Bug fixes:
- Fix impersonation skeleton screen on applicant dashboard
- Fix onboarding redirect loop in auth layout

Observer dashboard redesign (Steps 1-6):
- Clickable round pipeline with selected round highlighting
- Round-type-specific dashboard panels (intake, filtering, evaluation,
  submission, mentoring, live final, deliberation)
- Enhanced activity feed with server-side humanization
- Previous round comparison section
- New backend queries for round-specific analytics

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 20:18:50 +01:00

102 lines
2.9 KiB
TypeScript

'use client'
import { createContext, useContext, useState, useEffect, useMemo, type ReactNode } from 'react'
import { trpc } from '@/lib/trpc/client'
type RoundInfo = {
id: string
name: string
status: string
competitionId?: string
roundType?: string
sortOrder?: number
}
type Program = {
id: string
name: string | null
year?: number
rounds?: RoundInfo[]
}
type EditionContextValue = {
programs: Program[]
selectedProgramId: string
setSelectedProgramId: (id: string) => void
activeRoundId: string
/** The user-selected round (defaults to best/active round) */
selectedRoundId: string
setSelectedRoundId: (id: string) => void
/** Derived roundType for the selected round */
selectedRoundType: string
/** All rounds for the selected program (sorted by sortOrder) */
rounds: RoundInfo[]
}
const EditionContext = createContext<EditionContextValue | null>(null)
export function useEditionContext() {
const ctx = useContext(EditionContext)
if (!ctx) throw new Error('useEditionContext must be used within EditionProvider')
return ctx
}
function findBestRound(rounds: Array<{ id: string; status: string }>): string {
const active = rounds.find(r => r.status === 'ROUND_ACTIVE')
if (active) return active.id
const closed = [...rounds].filter(r => r.status === 'ROUND_CLOSED').pop()
if (closed) return closed.id
return rounds[0]?.id ?? ''
}
export function EditionProvider({ children }: { children: ReactNode }) {
const [selectedProgramId, setSelectedProgramId] = useState<string>('')
const [selectedRoundId, setSelectedRoundId] = useState<string>('')
const { data: programs } = trpc.program.list.useQuery(
{ includeStages: true },
{ refetchInterval: 30_000 },
)
const typedPrograms = (programs ?? []) as Program[]
const selectedProgram = typedPrograms.find(p => p.id === selectedProgramId)
const rounds = useMemo(
() => ((selectedProgram?.rounds ?? []) as RoundInfo[]).slice().sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0)),
[selectedProgram?.rounds],
)
const activeRoundId = findBestRound(rounds)
// Auto-select first program
useEffect(() => {
if (typedPrograms.length > 0 && !selectedProgramId) {
setSelectedProgramId(typedPrograms[0].id)
}
}, [typedPrograms, selectedProgramId])
// Auto-select best round when program changes or rounds load
useEffect(() => {
if (rounds.length > 0 && (!selectedRoundId || !rounds.some(r => r.id === selectedRoundId))) {
setSelectedRoundId(findBestRound(rounds))
}
}, [rounds, selectedRoundId])
const selectedRoundType = rounds.find(r => r.id === selectedRoundId)?.roundType ?? ''
return (
<EditionContext.Provider
value={{
programs: typedPrograms,
selectedProgramId,
setSelectedProgramId,
activeRoundId,
selectedRoundId,
setSelectedRoundId,
selectedRoundType,
rounds,
}}
>
{children}
</EditionContext.Provider>
)
}