feat: router.back() navigation, read-only evaluation view, auth audit logging
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m53s

- Convert all Back buttons platform-wide (38 files) to use router.back()
  for natural browser-back behavior regardless of entry point
- Add read-only view for submitted evaluations in closed rounds with
  blue banner, disabled inputs, and contextual back navigation
- Add auth audit logs: MAGIC_LINK_SENT, PASSWORD_RESET_LINK_CLICKED,
  PASSWORD_RESET_LINK_EXPIRED, PASSWORD_RESET_LINK_INVALID
- Learning Hub links navigate in same window for all roles
- Update settings descriptions to reflect all-user scope

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 14:25:56 +01:00
parent a556732b46
commit a1e758bc39
44 changed files with 398 additions and 384 deletions

View File

@@ -122,11 +122,9 @@ export default function EditAwardPage({
return (
<div className="space-y-6">
<div className="flex items-center gap-4">
<Button variant="ghost" asChild className="-ml-4">
<Link href={`/admin/awards/${awardId}`}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Award
</Link>
<Button variant="ghost" className="-ml-4" onClick={() => router.back()}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back
</Button>
</div>

View File

@@ -663,11 +663,9 @@ export default function AwardDetailPage({
<div className="space-y-6">
{/* Header */}
<div className="flex items-center gap-4">
<Button variant="ghost" asChild className="-ml-4">
<Link href="/admin/awards">
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Awards
</Link>
<Button variant="ghost" className="-ml-4" onClick={() => router.back()}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back
</Button>
</div>

View File

@@ -69,11 +69,9 @@ export default function CreateAwardPage() {
return (
<div className="space-y-6">
<div className="flex items-center gap-4">
<Button variant="ghost" asChild className="-ml-4">
<Link href="/admin/awards">
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Awards
</Link>
<Button variant="ghost" className="-ml-4" onClick={() => router.back()}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back
</Button>
</div>

View File

@@ -194,10 +194,8 @@ export default function JuryGroupDetailPage({ params }: JuryGroupDetailPageProps
<Card className="border-dashed">
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
<p className="text-muted-foreground">The requested jury group could not be found.</p>
<Button asChild className="mt-4" variant="outline">
<Link href={'/admin/juries' as Route}>
Back to Juries
</Link>
<Button className="mt-4" variant="outline" onClick={() => router.back()}>
Back
</Button>
</CardContent>
</Card>
@@ -212,13 +210,11 @@ export default function JuryGroupDetailPage({ params }: JuryGroupDetailPageProps
<Button
variant="ghost"
size="sm"
asChild
className="mb-2"
onClick={() => router.back()}
>
<Link href={'/admin/juries' as Route}>
<ArrowLeft className="h-4 w-4 mr-1" />
Back to Juries
</Link>
<ArrowLeft className="h-4 w-4 mr-1" />
Back
</Button>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>

View File

@@ -2,7 +2,6 @@
import { useCallback, useEffect, useState } from 'react'
import { useParams, useRouter } 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'
@@ -257,11 +256,9 @@ export default function EditLearningResourcePage() {
The resource you&apos;re looking for does not exist.
</AlertDescription>
</Alert>
<Button asChild>
<Link href="/admin/learning">
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Learning Hub
</Link>
<Button onClick={() => router.back()}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back
</Button>
</div>
)
@@ -271,11 +268,9 @@ export default function EditLearningResourcePage() {
<div className="flex min-h-screen flex-col">
{/* Sticky toolbar */}
<div className="sticky top-0 z-30 flex items-center justify-between border-b bg-background/95 px-4 py-2 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<Button variant="ghost" size="sm" asChild>
<Link href="/admin/learning">
<ArrowLeft className="mr-2 h-4 w-4" />
Back
</Link>
<Button variant="ghost" size="sm" onClick={() => router.back()}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back
</Button>
<div className="flex items-center gap-2">

View File

@@ -2,7 +2,6 @@
import { useCallback, useEffect, useState } from 'react'
import { useRouter } 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'
@@ -165,11 +164,9 @@ export default function NewLearningResourcePage() {
<div className="flex min-h-screen flex-col">
{/* Sticky toolbar */}
<div className="sticky top-0 z-30 flex items-center justify-between border-b bg-background/95 px-4 py-2 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<Button variant="ghost" size="sm" asChild>
<Link href="/admin/learning">
<ArrowLeft className="mr-2 h-4 w-4" />
Back
</Link>
<Button variant="ghost" size="sm" onClick={() => router.back()}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back
</Button>
<div className="flex items-center gap-2">

View File

@@ -224,11 +224,9 @@ export default function MemberDetailPage() {
{error?.message || 'The member you\'re looking for does not exist.'}
</AlertDescription>
</Alert>
<Button asChild>
<Link href="/admin/members">
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Members
</Link>
<Button onClick={() => router.back()}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back
</Button>
</div>
)
@@ -239,11 +237,9 @@ export default function MemberDetailPage() {
return (
<div className="space-y-6">
{/* Back nav */}
<Button variant="ghost" asChild className="-ml-4">
<Link href="/admin/members">
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Members
</Link>
<Button variant="ghost" className="-ml-4" onClick={() => router.back()}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back
</Button>
{/* Header Hero */}

View File

@@ -2,6 +2,7 @@
import { useState, useCallback, useMemo } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import Papa from 'papaparse'
import { trpc } from '@/lib/trpc/client'
import { Button } from '@/components/ui/button'
@@ -257,6 +258,7 @@ function TagPicker({
}
export default function MemberInvitePage() {
const router = useRouter()
const [step, setStep] = useState<Step>('input')
const [inputMethod, setInputMethod] = useState<'manual' | 'csv'>('manual')
const [rows, setRows] = useState<MemberRow[]>([createEmptyRow()])
@@ -1044,11 +1046,9 @@ export default function MemberInvitePage() {
return (
<div className="space-y-6">
<div className="flex items-center gap-4">
<Button variant="ghost" asChild className="-ml-4">
<Link href="/admin/members">
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Members
</Link>
<Button variant="ghost" className="-ml-4" onClick={() => router.back()}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back
</Button>
</div>

View File

@@ -1,7 +1,7 @@
'use client'
import { useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { trpc } from '@/lib/trpc/client'
import {
Card,
@@ -83,6 +83,7 @@ const defaultForm: TemplateFormData = {
}
export default function MessageTemplatesPage() {
const router = useRouter()
const [dialogOpen, setDialogOpen] = useState(false)
const [editingId, setEditingId] = useState<string | null>(null)
const [deleteId, setDeleteId] = useState<string | null>(null)
@@ -183,11 +184,9 @@ export default function MessageTemplatesPage() {
<div className="space-y-6">
{/* Header */}
<div className="flex items-center gap-4">
<Button variant="ghost" asChild className="-ml-4">
<Link href="/admin/messages">
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Messages
</Link>
<Button variant="ghost" className="-ml-4" onClick={() => router.back()}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back
</Button>
</div>

View File

@@ -135,11 +135,9 @@ export default function EditPartnerPage() {
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Link href="/admin/partners">
<Button variant="ghost" size="icon">
<ArrowLeft className="h-4 w-4" />
</Button>
</Link>
<Button variant="ghost" size="icon" onClick={() => router.back()}>
<ArrowLeft className="h-4 w-4" />
</Button>
<div>
<h1 className="text-2xl font-bold">Edit Partner</h1>
<p className="text-muted-foreground">

View File

@@ -66,11 +66,9 @@ export default function NewPartnerPage() {
return (
<div className="space-y-6">
<div className="flex items-center gap-4">
<Link href="/admin/partners">
<Button variant="ghost" size="icon">
<ArrowLeft className="h-4 w-4" />
</Button>
</Link>
<Button variant="ghost" size="icon" onClick={() => router.back()}>
<ArrowLeft className="h-4 w-4" />
</Button>
<div>
<h1 className="text-2xl font-bold">Add Partner</h1>
<p className="text-muted-foreground">

View File

@@ -134,11 +134,9 @@ export default function EditProgramPage() {
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Link href={`/admin/programs/${id}`}>
<Button variant="ghost" size="icon">
<ArrowLeft className="h-4 w-4" />
</Button>
</Link>
<Button variant="ghost" size="icon" onClick={() => router.back()}>
<ArrowLeft className="h-4 w-4" />
</Button>
<div>
<h1 className="text-2xl font-bold">Edit Program</h1>
<p className="text-muted-foreground">

View File

@@ -1,8 +1,7 @@
'use client'
import { useState } from 'react'
import { useParams } from 'next/navigation'
import Link from 'next/link'
import { useParams, useRouter } from 'next/navigation'
import { trpc } from '@/lib/trpc/client'
import {
Card,
@@ -68,6 +67,7 @@ const defaultMilestoneForm: MilestoneFormData = {
export default function MentorshipMilestonesPage() {
const params = useParams()
const router = useRouter()
const programId = params.id as string
const [dialogOpen, setDialogOpen] = useState(false)
@@ -184,11 +184,9 @@ export default function MentorshipMilestonesPage() {
<div className="space-y-6">
{/* Header */}
<div className="flex items-center gap-4">
<Button variant="ghost" asChild className="-ml-4">
<Link href="/admin/programs">
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Programs
</Link>
<Button variant="ghost" className="-ml-4" onClick={() => router.back()}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back
</Button>
</div>

View File

@@ -56,11 +56,9 @@ export default function NewProgramPage() {
return (
<div className="space-y-6">
<div className="flex items-center gap-4">
<Link href="/admin/programs">
<Button variant="ghost" size="icon">
<ArrowLeft className="h-4 w-4" />
</Button>
</Link>
<Button variant="ghost" size="icon" onClick={() => router.back()}>
<ArrowLeft className="h-4 w-4" />
</Button>
<div>
<h1 className="text-2xl font-bold">Create Program</h1>
<p className="text-muted-foreground">

View File

@@ -300,19 +300,17 @@ function EditProjectContent({ projectId }: { projectId: string }) {
if (!project) {
return (
<div className="space-y-6">
<Button variant="ghost" asChild className="-ml-4">
<Link href="/admin/projects">
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Projects
</Link>
<Button variant="ghost" className="-ml-4" onClick={() => router.back()}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back
</Button>
<Card>
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
<AlertCircle className="h-12 w-12 text-destructive/50" />
<p className="mt-2 font-medium">Project Not Found</p>
<Button asChild className="mt-4">
<Link href="/admin/projects">Back to Projects</Link>
<Button className="mt-4" onClick={() => router.back()}>
Back
</Button>
</CardContent>
</Card>
@@ -330,11 +328,9 @@ function EditProjectContent({ projectId }: { projectId: string }) {
<div className="space-y-6">
{/* Header */}
<div className="flex items-center gap-4">
<Button variant="ghost" asChild className="-ml-4">
<Link href={`/admin/projects/${projectId}`}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Project
</Link>
<Button variant="ghost" className="-ml-4" onClick={() => router.back()}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back
</Button>
</div>

View File

@@ -1,7 +1,7 @@
'use client'
import { Suspense, use, useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { trpc } from '@/lib/trpc/client'
import { toast } from 'sonner'
import {
@@ -46,6 +46,7 @@ interface MentorSuggestion {
}
function MentorAssignmentContent({ projectId }: { projectId: string }) {
const router = useRouter()
const [selectedMentorId, setSelectedMentorId] = useState<string | null>(null)
const utils = trpc.useUtils()
@@ -128,11 +129,9 @@ function MentorAssignmentContent({ projectId }: { projectId: string }) {
<div className="space-y-6">
{/* Header */}
<div className="flex items-center gap-4">
<Button variant="ghost" asChild className="-ml-4">
<Link href={`/admin/projects/${projectId}`}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Project
</Link>
<Button variant="ghost" className="-ml-4" onClick={() => router.back()}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back
</Button>
</div>

View File

@@ -3,6 +3,7 @@
import { Suspense, use, useState } from 'react'
import Link from 'next/link'
import type { Route } from 'next'
import { useRouter } from 'next/navigation'
import { trpc } from '@/lib/trpc/client'
import {
Card,
@@ -102,6 +103,7 @@ const evalStatusColors: Record<string, 'default' | 'secondary' | 'destructive' |
}
function ProjectDetailContent({ projectId }: { projectId: string }) {
const router = useRouter()
// Fetch project + assignments + stats in a single combined query
const { data: fullDetail, isLoading } = trpc.project.getFullDetail.useQuery(
{ id: projectId },
@@ -199,19 +201,17 @@ function ProjectDetailContent({ projectId }: { projectId: string }) {
if (!project) {
return (
<div className="space-y-6">
<Button variant="ghost" asChild className="-ml-4">
<Link href="/admin/projects">
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Projects
</Link>
<Button variant="ghost" className="-ml-4" onClick={() => router.back()}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back
</Button>
<Card>
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
<AlertCircle className="h-12 w-12 text-destructive/50" />
<p className="mt-2 font-medium">Project Not Found</p>
<Button asChild className="mt-4">
<Link href="/admin/projects">Back to Projects</Link>
<Button className="mt-4" onClick={() => router.back()}>
Back
</Button>
</CardContent>
</Card>
@@ -223,11 +223,9 @@ function ProjectDetailContent({ projectId }: { projectId: string }) {
<div className="space-y-6">
{/* Header */}
<div className="flex items-center gap-4">
<Button variant="ghost" asChild className="-ml-4">
<Link href="/admin/projects">
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Projects
</Link>
<Button variant="ghost" className="-ml-4" onClick={() => router.back()}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back
</Button>
</div>

View File

@@ -1,7 +1,7 @@
'use client'
import { useState, useCallback, useRef, useEffect, useMemo } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { trpc } from '@/lib/trpc/client'
import { toast } from 'sonner'
import {
@@ -62,6 +62,7 @@ type UploadState = {
type UploadMap = Record<string, UploadState>
export default function BulkUploadPage() {
const router = useRouter()
const [roundId, setRoundId] = useState('')
const [search, setSearch] = useState('')
const [debouncedSearch, setDebouncedSearch] = useState('')
@@ -296,10 +297,8 @@ export default function BulkUploadPage() {
<div className="space-y-6">
{/* Header */}
<div className="flex items-center gap-4">
<Button variant="ghost" size="icon" asChild>
<Link href="/admin/projects">
<ArrowLeft className="h-4 w-4" />
</Link>
<Button variant="ghost" size="icon" onClick={() => router.back()}>
<ArrowLeft className="h-4 w-4" />
</Button>
<div>
<h1 className="text-2xl font-semibold tracking-tight">Bulk Document Upload</h1>

View File

@@ -59,11 +59,9 @@ function ImportPageContent() {
<div className="space-y-6">
{/* Header */}
<div className="flex items-center gap-4">
<Button variant="ghost" asChild className="-ml-4">
<Link href="/admin/projects">
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Projects
</Link>
<Button variant="ghost" className="-ml-4" onClick={() => router.back()}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back
</Button>
</div>

View File

@@ -246,11 +246,9 @@ function NewProjectPageContent() {
<div className="space-y-6">
{/* Header */}
<div className="flex items-center gap-4">
<Button variant="ghost" asChild className="-ml-4">
<Link href="/admin/projects">
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Projects
</Link>
<Button variant="ghost" className="-ml-4" onClick={() => router.back()}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back
</Button>
</div>

View File

@@ -1,7 +1,7 @@
'use client'
import { useState, useMemo, useCallback, useRef, useEffect } from 'react'
import { useParams, useSearchParams } from 'next/navigation'
import { useParams, useRouter } from 'next/navigation'
import Link from 'next/link'
import type { Route } from 'next'
import { trpc } from '@/lib/trpc/client'
@@ -152,8 +152,7 @@ const stateColors: Record<string, string> = Object.fromEntries(
export default function RoundDetailPage() {
const params = useParams()
const roundId = params.roundId as string
const searchParams = useSearchParams()
const backUrl = searchParams.get('from')
const router = useRouter()
const [config, setConfig] = useState<Record<string, unknown>>({})
const [autosaveStatus, setAutosaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle')
@@ -546,11 +545,9 @@ export default function RoundDetailPage() {
return (
<div className="space-y-6">
<div className="flex items-center gap-3">
<Link href={'/admin/rounds' as Route}>
<Button variant="ghost" size="icon" className="h-8 w-8" aria-label="Back">
<ArrowLeft className="h-4 w-4" />
</Button>
</Link>
<Button variant="ghost" size="icon" className="h-8 w-8" aria-label="Back" onClick={() => router.back()}>
<ArrowLeft className="h-4 w-4" />
</Button>
<div>
<h1 className="text-xl font-bold">Round Not Found</h1>
<p className="text-sm text-muted-foreground">This round does not exist.</p>
@@ -622,12 +619,10 @@ export default function RoundDetailPage() {
>
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div className="flex items-start gap-3 min-w-0">
<Link href={(backUrl ?? (round.specialAwardId ? `/admin/awards/${round.specialAwardId}` : '/admin/rounds')) as Route} className="mt-0.5 shrink-0">
<Button variant="ghost" size="sm" className="h-8 text-white/80 hover:text-white hover:bg-white/10 gap-1.5" aria-label={round.specialAwardId ? 'Back to Award' : 'Back to rounds'}>
<ArrowLeft className="h-4 w-4" />
<span className="text-xs hidden sm:inline">{round.specialAwardId ? 'Back to Award' : 'Back to Rounds'}</span>
</Button>
</Link>
<Button variant="ghost" size="sm" className="mt-0.5 shrink-0 h-8 text-white/80 hover:text-white hover:bg-white/10 gap-1.5" aria-label="Back" onClick={() => router.back()}>
<ArrowLeft className="h-4 w-4" />
<span className="text-xs hidden sm:inline">Back</span>
</Button>
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2.5">
{/* 4.6 Inline-editable round name */}

View File

@@ -1,7 +1,7 @@
'use client'
import { useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { trpc } from '@/lib/trpc/client'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
@@ -212,6 +212,7 @@ function SortableTagRow({
}
export default function TagsSettingsPage() {
const router = useRouter()
const utils = trpc.useUtils()
const [isCreateOpen, setIsCreateOpen] = useState(false)
const [editingTag, setEditingTag] = useState<Tag | null>(null)
@@ -384,11 +385,9 @@ export default function TagsSettingsPage() {
<div className="space-y-6">
{/* Header */}
<div className="flex items-center gap-4">
<Button variant="ghost" asChild className="-ml-4">
<Link href="/admin/settings">
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Settings
</Link>
<Button variant="ghost" className="-ml-4" onClick={() => router.back()}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back
</Button>
</div>

View File

@@ -1,7 +1,7 @@
'use client'
import { useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { trpc } from '@/lib/trpc/client'
import {
Card,
@@ -86,6 +86,7 @@ const defaultForm: WebhookFormData = {
}
export default function WebhooksPage() {
const router = useRouter()
const [dialogOpen, setDialogOpen] = useState(false)
const [editingId, setEditingId] = useState<string | null>(null)
const [deleteId, setDeleteId] = useState<string | null>(null)
@@ -254,11 +255,9 @@ export default function WebhooksPage() {
<div className="space-y-6">
{/* Header */}
<div className="flex items-center gap-4">
<Button variant="ghost" asChild className="-ml-4">
<Link href="/admin/settings">
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Settings
</Link>
<Button variant="ghost" className="-ml-4" onClick={() => router.back()}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back
</Button>
</div>