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 ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Button variant="ghost" asChild className="-ml-4"> <Button variant="ghost" className="-ml-4" onClick={() => router.back()}>
<Link href={`/admin/awards/${awardId}`}> <ArrowLeft className="mr-2 h-4 w-4" />
<ArrowLeft className="mr-2 h-4 w-4" /> Back
Back to Award
</Link>
</Button> </Button>
</div> </div>

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,6 @@
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { useParams, useRouter } from 'next/navigation' import { useParams, useRouter } from 'next/navigation'
import Link from 'next/link'
import dynamic from 'next/dynamic' import dynamic from 'next/dynamic'
import { trpc } from '@/lib/trpc/client' import { trpc } from '@/lib/trpc/client'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
@@ -257,11 +256,9 @@ export default function EditLearningResourcePage() {
The resource you&apos;re looking for does not exist. The resource you&apos;re looking for does not exist.
</AlertDescription> </AlertDescription>
</Alert> </Alert>
<Button asChild> <Button onClick={() => router.back()}>
<Link href="/admin/learning"> <ArrowLeft className="mr-2 h-4 w-4" />
<ArrowLeft className="mr-2 h-4 w-4" /> Back
Back to Learning Hub
</Link>
</Button> </Button>
</div> </div>
) )
@@ -271,11 +268,9 @@ export default function EditLearningResourcePage() {
<div className="flex min-h-screen flex-col"> <div className="flex min-h-screen flex-col">
{/* Sticky toolbar */} {/* 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"> <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> <Button variant="ghost" size="sm" onClick={() => router.back()}>
<Link href="/admin/learning"> <ArrowLeft className="mr-2 h-4 w-4" />
<ArrowLeft className="mr-2 h-4 w-4" /> Back
Back
</Link>
</Button> </Button>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">

View File

@@ -2,7 +2,6 @@
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import Link from 'next/link'
import dynamic from 'next/dynamic' import dynamic from 'next/dynamic'
import { trpc } from '@/lib/trpc/client' import { trpc } from '@/lib/trpc/client'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
@@ -165,11 +164,9 @@ export default function NewLearningResourcePage() {
<div className="flex min-h-screen flex-col"> <div className="flex min-h-screen flex-col">
{/* Sticky toolbar */} {/* 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"> <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> <Button variant="ghost" size="sm" onClick={() => router.back()}>
<Link href="/admin/learning"> <ArrowLeft className="mr-2 h-4 w-4" />
<ArrowLeft className="mr-2 h-4 w-4" /> Back
Back
</Link>
</Button> </Button>
<div className="flex items-center gap-2"> <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.'} {error?.message || 'The member you\'re looking for does not exist.'}
</AlertDescription> </AlertDescription>
</Alert> </Alert>
<Button asChild> <Button onClick={() => router.back()}>
<Link href="/admin/members"> <ArrowLeft className="mr-2 h-4 w-4" />
<ArrowLeft className="mr-2 h-4 w-4" /> Back
Back to Members
</Link>
</Button> </Button>
</div> </div>
) )
@@ -239,11 +237,9 @@ export default function MemberDetailPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Back nav */} {/* Back nav */}
<Button variant="ghost" asChild className="-ml-4"> <Button variant="ghost" className="-ml-4" onClick={() => router.back()}>
<Link href="/admin/members"> <ArrowLeft className="mr-2 h-4 w-4" />
<ArrowLeft className="mr-2 h-4 w-4" /> Back
Back to Members
</Link>
</Button> </Button>
{/* Header Hero */} {/* Header Hero */}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,16 @@
'use client' 'use client'
import { useSession } from 'next-auth/react' import { useSession } from 'next-auth/react'
import Link from 'next/link' import { useRouter } from 'next/navigation'
import type { Route } from 'next'
import { trpc } from '@/lib/trpc/client' import { trpc } from '@/lib/trpc/client'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { ApplicantCompetitionTimeline } from '@/components/applicant/competition-timeline' import { ApplicantCompetitionTimeline } from '@/components/applicant/competition-timeline'
import { ArrowLeft, FileText, Calendar } from 'lucide-react' import { ArrowLeft, FileText } from 'lucide-react'
export default function ApplicantCompetitionPage() { export default function ApplicantCompetitionPage() {
const router = useRouter()
const { data: session } = useSession() const { data: session } = useSession()
const { data: myProject, isLoading } = trpc.applicant.getMyDashboard.useQuery(undefined, { const { data: myProject, isLoading } = trpc.applicant.getMyDashboard.useQuery(undefined, {
enabled: !!session, enabled: !!session,
@@ -36,11 +36,9 @@ export default function ApplicantCompetitionPage() {
Track your progress through competition rounds Track your progress through competition rounds
</p> </p>
</div> </div>
<Button variant="ghost" size="sm" asChild> <Button variant="ghost" size="sm" onClick={() => router.back()}>
<Link href={'/applicant' as Route} aria-label="Back to applicant dashboard"> <ArrowLeft className="mr-2 h-4 w-4" />
<ArrowLeft className="mr-2 h-4 w-4" /> Back
Back to Dashboard
</Link>
</Button> </Button>
</div> </div>
@@ -61,29 +59,6 @@ export default function ApplicantCompetitionPage() {
<ApplicantCompetitionTimeline /> <ApplicantCompetitionTimeline />
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Calendar className="h-5 w-5" />
Quick Actions
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<Button variant="outline" className="w-full justify-start" asChild>
<Link href={'/applicant/documents' as Route}>
<FileText className="mr-2 h-4 w-4" />
View Documents
</Link>
</Button>
{myProject?.openRounds && myProject.openRounds.length > 0 && (
<p className="text-sm text-muted-foreground px-3 py-2 bg-muted/50 rounded-md">
{myProject.openRounds.length} submission window
{myProject.openRounds.length !== 1 ? 's' : ''} currently open
</p>
)}
</CardContent>
</Card>
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Timeline Info</CardTitle> <CardTitle>Timeline Info</CardTitle>

View File

@@ -1,8 +1,7 @@
'use client' 'use client'
import { useEffect } from 'react' import { useEffect } from 'react'
import { useParams } from 'next/navigation' import { useParams, useRouter } from 'next/navigation'
import Link from 'next/link'
import dynamic from 'next/dynamic' import dynamic from 'next/dynamic'
import { trpc } from '@/lib/trpc/client' import { trpc } from '@/lib/trpc/client'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
@@ -27,6 +26,7 @@ const ResourceRenderer = dynamic(
export default function ApplicantResourceDetailPage() { export default function ApplicantResourceDetailPage() {
const params = useParams() const params = useParams()
const router = useRouter()
const resourceId = params.id as string const resourceId = params.id as string
const { data: resource, isLoading, error } = trpc.learningResource.get.useQuery({ id: resourceId }) const { data: resource, isLoading, error } = trpc.learningResource.get.useQuery({ id: resourceId })
@@ -73,11 +73,9 @@ export default function ApplicantResourceDetailPage() {
This resource may have been removed or you don&apos;t have access. This resource may have been removed or you don&apos;t have access.
</AlertDescription> </AlertDescription>
</Alert> </Alert>
<Button asChild> <Button onClick={() => router.back()}>
<Link href="/applicant/resources"> <ArrowLeft className="mr-2 h-4 w-4" />
<ArrowLeft className="mr-2 h-4 w-4" /> Back
Back to Resources
</Link>
</Button> </Button>
</div> </div>
) )
@@ -87,11 +85,9 @@ export default function ApplicantResourceDetailPage() {
<div className="space-y-6"> <div className="space-y-6">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Button variant="ghost" asChild className="-ml-4"> <Button variant="ghost" onClick={() => router.back()} className="-ml-4">
<Link href="/applicant/resources"> <ArrowLeft className="mr-2 h-4 w-4" />
<ArrowLeft className="mr-2 h-4 w-4" /> Back
Back to Resources
</Link>
</Button> </Button>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{resource.externalUrl && ( {resource.externalUrl && (

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import { use, useState } from 'react' import { use, useState } from 'react'
import Link from 'next/link' import { useRouter } from 'next/navigation'
import { trpc } from '@/lib/trpc/client' import { trpc } from '@/lib/trpc/client'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
@@ -29,6 +29,7 @@ export default function JuryAwardVotingPage({
params: Promise<{ id: string }> params: Promise<{ id: string }>
}) { }) {
const { id: awardId } = use(params) const { id: awardId } = use(params)
const router = useRouter()
const utils = trpc.useUtils() const utils = trpc.useUtils()
const { data, isLoading, refetch } = const { data, isLoading, refetch } =
@@ -120,11 +121,9 @@ export default function JuryAwardVotingPage({
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Button variant="ghost" asChild className="-ml-4"> <Button variant="ghost" onClick={() => router.back()} className="-ml-4">
<Link href="/jury/awards"> <ArrowLeft className="mr-2 h-4 w-4" />
<ArrowLeft className="mr-2 h-4 w-4" /> Back
Back to Awards
</Link>
</Button> </Button>
</div> </div>

View File

@@ -1,6 +1,6 @@
'use client' 'use client'
import { useParams } from 'next/navigation' import { useParams, useRouter } from 'next/navigation'
import Link from 'next/link' import Link from 'next/link'
import type { Route } from 'next' import type { Route } from 'next'
import { trpc } from '@/lib/trpc/client' import { trpc } from '@/lib/trpc/client'
@@ -8,12 +8,13 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { ArrowLeft, CheckCircle2, Clock, Circle } from 'lucide-react' import { ArrowLeft, CheckCircle2, Clock, Circle, Eye } from 'lucide-react'
import { toast } from 'sonner' import { toast } from 'sonner'
import { JurorProgressDashboard } from '@/components/jury/juror-progress-dashboard' import { JurorProgressDashboard } from '@/components/jury/juror-progress-dashboard'
export default function JuryRoundDetailPage() { export default function JuryRoundDetailPage() {
const params = useParams() const params = useParams()
const router = useRouter()
const roundId = params.roundId as string const roundId = params.roundId as string
const { data: assignments, isLoading } = trpc.roundAssignment.getMyAssignments.useQuery( const { data: assignments, isLoading } = trpc.roundAssignment.getMyAssignments.useQuery(
@@ -38,11 +39,9 @@ export default function JuryRoundDetailPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Button variant="ghost" size="sm" asChild> <Button variant="ghost" size="sm" onClick={() => router.back()}>
<Link href={'/jury/competitions' as Route} aria-label="Back to competitions list"> <ArrowLeft className="mr-2 h-4 w-4" />
<ArrowLeft className="mr-2 h-4 w-4" /> Back
Back
</Link>
</Button> </Button>
<div> <div>
<h1 className="text-2xl font-bold tracking-tight text-brand-blue dark:text-foreground"> <h1 className="text-2xl font-bold tracking-tight text-brand-blue dark:text-foreground">
@@ -82,10 +81,13 @@ export default function JuryRoundDetailPage() {
const isDraft = assignment.evaluation?.status === 'DRAFT' const isDraft = assignment.evaluation?.status === 'DRAFT'
return ( return (
<Link <div
key={assignment.id} key={assignment.id}
href={`/jury/competitions/${roundId}/projects/${assignment.projectId}` as Route} role="button"
className="flex items-center justify-between p-4 rounded-lg border border-border/60 hover:border-brand-blue/30 hover:bg-brand-blue/5 transition-all" tabIndex={0}
onClick={() => router.push(`/jury/competitions/${roundId}/projects/${assignment.projectId}`)}
onKeyDown={(e) => { if (e.key === 'Enter') router.push(`/jury/competitions/${roundId}/projects/${assignment.projectId}`) }}
className="flex items-center justify-between p-4 rounded-lg border border-border/60 hover:border-brand-blue/30 hover:bg-brand-blue/5 transition-all cursor-pointer"
> >
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="font-medium truncate">{assignment.project.title}</p> <p className="font-medium truncate">{assignment.project.title}</p>
@@ -97,12 +99,26 @@ export default function JuryRoundDetailPage() {
)} )}
</div> </div>
</div> </div>
<div className="flex items-center gap-3 ml-4"> <div className="flex items-center gap-2 ml-4">
{isCompleted ? ( {isCompleted ? (
<Badge variant="default" className="bg-emerald-50 text-emerald-700 border-emerald-200"> <>
<CheckCircle2 className="mr-1 h-3 w-3" /> <Badge variant="default" className="bg-emerald-50 text-emerald-700 border-emerald-200">
Completed <CheckCircle2 className="mr-1 h-3 w-3" />
</Badge> Completed
</Badge>
<Button
variant="outline"
size="sm"
className="h-7 text-xs"
asChild
onClick={(e) => e.stopPropagation()}
>
<Link href={`/jury/competitions/${roundId}/projects/${assignment.projectId}/evaluate` as Route}>
<Eye className="mr-1 h-3 w-3" />
View
</Link>
</Button>
</>
) : isDraft ? ( ) : isDraft ? (
<Badge variant="secondary" className="bg-amber-50 text-amber-700 border-amber-200"> <Badge variant="secondary" className="bg-amber-50 text-amber-700 border-amber-200">
<Clock className="mr-1 h-3 w-3" /> <Clock className="mr-1 h-3 w-3" />
@@ -115,7 +131,7 @@ export default function JuryRoundDetailPage() {
</Badge> </Badge>
)} )}
</div> </div>
</Link> </div>
) )
})} })}
</div> </div>

View File

@@ -16,7 +16,7 @@ import { cn } from '@/lib/utils'
import { MultiWindowDocViewer } from '@/components/jury/multi-window-doc-viewer' import { MultiWindowDocViewer } from '@/components/jury/multi-window-doc-viewer'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { COIDeclarationDialog } from '@/components/forms/coi-declaration-dialog' import { COIDeclarationDialog } from '@/components/forms/coi-declaration-dialog'
import { ArrowLeft, Save, Send, AlertCircle, ThumbsUp, ThumbsDown, Clock, CheckCircle2, ShieldAlert } from 'lucide-react' import { ArrowLeft, Save, Send, AlertCircle, ThumbsUp, ThumbsDown, Clock, CheckCircle2, ShieldAlert, Lock } from 'lucide-react'
import { toast } from 'sonner' import { toast } from 'sonner'
import type { EvaluationConfig } from '@/types/competition-configs' import type { EvaluationConfig } from '@/types/competition-configs'
@@ -468,8 +468,10 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
// Check if round is active // Check if round is active
const isRoundActive = round.status === 'ROUND_ACTIVE' const isRoundActive = round.status === 'ROUND_ACTIVE'
const isSubmittedEvaluation = existingEvaluation?.status === 'SUBMITTED'
if (!isRoundActive) { // If round is not active and no submitted evaluation to view, block access
if (!isRoundActive && !isSubmittedEvaluation) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
@@ -502,8 +504,11 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
) )
} }
// COI gate: if COI is required, not yet declared, and we have an assignment // Read-only view for submitted evaluations in closed rounds
if (coiRequired && myAssignment && !coiLoading && !coiDeclared) { const isReadOnly = !isRoundActive && isSubmittedEvaluation
// COI gate: if COI is required, not yet declared, and we have an assignment (skip for read-only views)
if (coiRequired && !isReadOnly && myAssignment && !coiLoading && !coiDeclared) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
@@ -533,8 +538,8 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
) )
} }
// COI conflict declared — block evaluation // COI conflict declared — block evaluation (skip for read-only views)
if (coiRequired && coiConflict) { if (coiRequired && !isReadOnly && coiConflict) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
@@ -578,15 +583,22 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Button variant="ghost" size="sm" asChild> {isReadOnly ? (
<Link href={`/jury/competitions/${roundId}/projects/${projectId}` as Route}> <Button variant="ghost" size="sm" onClick={() => router.back()}>
<ArrowLeft className="mr-2 h-4 w-4" /> <ArrowLeft className="mr-2 h-4 w-4" />
Back to Project Back
</Link> </Button>
</Button> ) : (
<Button variant="ghost" size="sm" asChild>
<Link href={`/jury/competitions/${roundId}/projects/${projectId}` as Route}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Project
</Link>
</Button>
)}
<div> <div>
<h1 className="text-2xl font-bold tracking-tight text-brand-blue dark:text-foreground"> <h1 className="text-2xl font-bold tracking-tight text-brand-blue dark:text-foreground">
Evaluate Project {isReadOnly ? 'Submitted Evaluation' : 'Evaluate Project'}
</h1> </h1>
<div className="flex items-center gap-2 mt-1"> <div className="flex items-center gap-2 mt-1">
<p className="text-muted-foreground">{project.title}</p> <p className="text-muted-foreground">{project.title}</p>
@@ -606,21 +618,37 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
</div> </div>
</div> </div>
{isReadOnly && (
<Card className="border-l-4 border-l-blue-500 bg-blue-50/50 dark:bg-blue-950/20">
<CardContent className="flex items-start gap-3 p-4">
<Lock className="h-5 w-5 text-blue-600 shrink-0 mt-0.5" />
<div className="flex-1">
<p className="font-medium text-sm">View-Only</p>
<p className="text-sm text-muted-foreground mt-1">
This evaluation has been submitted and the round is now closed. You are viewing a read-only copy of your submission.
</p>
</div>
</CardContent>
</Card>
)}
{/* Project Documents */} {/* Project Documents */}
<MultiWindowDocViewer roundId={roundId} projectId={projectId} /> <MultiWindowDocViewer roundId={roundId} projectId={projectId} />
<Card className="border-l-4 border-l-amber-500"> {!isReadOnly && (
<CardContent className="flex items-start gap-3 p-4"> <Card className="border-l-4 border-l-amber-500">
<AlertCircle className="h-5 w-5 text-amber-600 shrink-0 mt-0.5" /> <CardContent className="flex items-start gap-3 p-4">
<div className="flex-1"> <AlertCircle className="h-5 w-5 text-amber-600 shrink-0 mt-0.5" />
<p className="font-medium text-sm">Important Reminder</p> <div className="flex-1">
<p className="text-sm text-muted-foreground mt-1"> <p className="font-medium text-sm">Important Reminder</p>
Your evaluation will be used to assess this project. Please provide thoughtful and <p className="text-sm text-muted-foreground mt-1">
constructive feedback. Your progress is automatically saved as a draft. Your evaluation will be used to assess this project. Please provide thoughtful and
</p> constructive feedback. Your progress is automatically saved as a draft.
</div> </p>
</CardContent> </div>
</Card> </CardContent>
</Card>
)}
<Card> <Card>
<CardHeader> <CardHeader>
@@ -673,12 +701,14 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
<div className="flex gap-4"> <div className="flex gap-4">
<button <button
type="button" type="button"
disabled={isReadOnly}
onClick={() => handleCriterionChange(criterion.id, true)} onClick={() => handleCriterionChange(criterion.id, true)}
className={cn( className={cn(
'flex-1 h-14 rounded-xl border-2 flex items-center justify-center text-base font-semibold transition-all', 'flex-1 h-14 rounded-xl border-2 flex items-center justify-center text-base font-semibold transition-all',
currentValue === true currentValue === true
? 'border-emerald-500 bg-emerald-50 text-emerald-700 shadow-sm ring-2 ring-emerald-200' ? 'border-emerald-500 bg-emerald-50 text-emerald-700 shadow-sm ring-2 ring-emerald-200'
: 'border-border hover:border-emerald-300 hover:bg-emerald-50/50' : 'border-border hover:border-emerald-300 hover:bg-emerald-50/50',
isReadOnly && 'opacity-60 cursor-default'
)} )}
> >
<ThumbsUp className="mr-2 h-5 w-5" /> <ThumbsUp className="mr-2 h-5 w-5" />
@@ -686,12 +716,14 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
</button> </button>
<button <button
type="button" type="button"
disabled={isReadOnly}
onClick={() => handleCriterionChange(criterion.id, false)} onClick={() => handleCriterionChange(criterion.id, false)}
className={cn( className={cn(
'flex-1 h-14 rounded-xl border-2 flex items-center justify-center text-base font-semibold transition-all', 'flex-1 h-14 rounded-xl border-2 flex items-center justify-center text-base font-semibold transition-all',
currentValue === false currentValue === false
? 'border-red-500 bg-red-50 text-red-700 shadow-sm ring-2 ring-red-200' ? 'border-red-500 bg-red-50 text-red-700 shadow-sm ring-2 ring-red-200'
: 'border-border hover:border-red-300 hover:bg-red-50/50' : 'border-border hover:border-red-300 hover:bg-red-50/50',
isReadOnly && 'opacity-60 cursor-default'
)} )}
> >
<ThumbsDown className="mr-2 h-5 w-5" /> <ThumbsDown className="mr-2 h-5 w-5" />
@@ -718,12 +750,14 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
<div className="flex gap-3"> <div className="flex gap-3">
<button <button
type="button" type="button"
disabled={isReadOnly}
onClick={() => handleCriterionChange(criterion.id, true)} onClick={() => handleCriterionChange(criterion.id, true)}
className={cn( className={cn(
'flex-1 h-12 rounded-lg border-2 flex items-center justify-center text-sm font-medium transition-all', 'flex-1 h-12 rounded-lg border-2 flex items-center justify-center text-sm font-medium transition-all',
currentValue === true currentValue === true
? 'border-emerald-500 bg-emerald-50 text-emerald-700 dark:bg-emerald-950/40 dark:text-emerald-400' ? 'border-emerald-500 bg-emerald-50 text-emerald-700 dark:bg-emerald-950/40 dark:text-emerald-400'
: 'border-border hover:border-emerald-300 hover:bg-emerald-50/50' : 'border-border hover:border-emerald-300 hover:bg-emerald-50/50',
isReadOnly && 'opacity-60 cursor-default'
)} )}
> >
<ThumbsUp className="mr-2 h-4 w-4" /> <ThumbsUp className="mr-2 h-4 w-4" />
@@ -731,12 +765,14 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
</button> </button>
<button <button
type="button" type="button"
disabled={isReadOnly}
onClick={() => handleCriterionChange(criterion.id, false)} onClick={() => handleCriterionChange(criterion.id, false)}
className={cn( className={cn(
'flex-1 h-12 rounded-lg border-2 flex items-center justify-center text-sm font-medium transition-all', 'flex-1 h-12 rounded-lg border-2 flex items-center justify-center text-sm font-medium transition-all',
currentValue === false currentValue === false
? 'border-red-500 bg-red-50 text-red-700 dark:bg-red-950/40 dark:text-red-400' ? 'border-red-500 bg-red-50 text-red-700 dark:bg-red-950/40 dark:text-red-400'
: 'border-border hover:border-red-300 hover:bg-red-50/50' : 'border-border hover:border-red-300 hover:bg-red-50/50',
isReadOnly && 'opacity-60 cursor-default'
)} )}
> >
<ThumbsDown className="mr-2 h-4 w-4" /> <ThumbsDown className="mr-2 h-4 w-4" />
@@ -766,6 +802,7 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
placeholder={criterion.placeholder || 'Enter your response...'} placeholder={criterion.placeholder || 'Enter your response...'}
rows={4} rows={4}
maxLength={criterion.maxLength} maxLength={criterion.maxLength}
disabled={isReadOnly}
/> />
<p className="text-xs text-muted-foreground text-right"> <p className="text-xs text-muted-foreground text-right">
{currentValue.length}/{criterion.maxLength} {currentValue.length}/{criterion.maxLength}
@@ -807,6 +844,7 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
value={[sliderValue]} value={[sliderValue]}
onValueChange={(v) => handleCriterionChange(criterion.id, v[0])} onValueChange={(v) => handleCriterionChange(criterion.id, v[0])}
className="flex-1" className="flex-1"
disabled={isReadOnly}
/> />
<span className="text-xs text-muted-foreground w-4">{max}</span> <span className="text-xs text-muted-foreground w-4">{max}</span>
</div> </div>
@@ -816,6 +854,7 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
<button <button
key={num} key={num}
type="button" type="button"
disabled={isReadOnly}
onClick={() => handleCriterionChange(criterion.id, num)} onClick={() => handleCriterionChange(criterion.id, num)}
className={cn( className={cn(
'w-9 h-9 rounded-md text-sm font-medium transition-colors', 'w-9 h-9 rounded-md text-sm font-medium transition-colors',
@@ -823,7 +862,8 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
? 'bg-primary text-primary-foreground' ? 'bg-primary text-primary-foreground'
: displayValue !== undefined && displayValue > num : displayValue !== undefined && displayValue > num
? 'bg-primary/20 text-primary' ? 'bg-primary/20 text-primary'
: 'bg-muted hover:bg-muted/80' : 'bg-muted hover:bg-muted/80',
isReadOnly && 'cursor-default'
)} )}
> >
{num} {num}
@@ -856,6 +896,7 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
value={[globalScore ? parseInt(globalScore, 10) : 5]} value={[globalScore ? parseInt(globalScore, 10) : 5]}
onValueChange={(v) => handleGlobalScoreChange(v[0].toString())} onValueChange={(v) => handleGlobalScoreChange(v[0].toString())}
className="flex-1" className="flex-1"
disabled={isReadOnly}
/> />
<span className="text-xs text-muted-foreground">10</span> <span className="text-xs text-muted-foreground">10</span>
</div> </div>
@@ -866,6 +907,7 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
<button <button
key={num} key={num}
type="button" type="button"
disabled={isReadOnly}
onClick={() => handleGlobalScoreChange(num.toString())} onClick={() => handleGlobalScoreChange(num.toString())}
className={cn( className={cn(
'w-9 h-9 rounded-md text-sm font-medium transition-colors', 'w-9 h-9 rounded-md text-sm font-medium transition-colors',
@@ -873,7 +915,8 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
? 'bg-primary text-primary-foreground' ? 'bg-primary text-primary-foreground'
: current > num : current > num
? 'bg-primary/20 text-primary' ? 'bg-primary/20 text-primary'
: 'bg-muted hover:bg-muted/80' : 'bg-muted hover:bg-muted/80',
isReadOnly && 'cursor-default'
)} )}
> >
{num} {num}
@@ -890,7 +933,7 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
<Label> <Label>
Decision <span className="text-destructive">*</span> Decision <span className="text-destructive">*</span>
</Label> </Label>
<RadioGroup value={binaryDecision} onValueChange={(v) => handleBinaryChange(v as 'accept' | 'reject')}> <RadioGroup value={binaryDecision} onValueChange={(v) => handleBinaryChange(v as 'accept' | 'reject')} disabled={isReadOnly}>
<div className="flex items-center space-x-2 p-4 border rounded-lg hover:bg-emerald-50/50"> <div className="flex items-center space-x-2 p-4 border rounded-lg hover:bg-emerald-50/50">
<RadioGroupItem value="accept" id="accept" /> <RadioGroupItem value="accept" id="accept" />
<Label htmlFor="accept" className="flex items-center gap-2 cursor-pointer flex-1"> <Label htmlFor="accept" className="flex items-center gap-2 cursor-pointer flex-1">
@@ -921,6 +964,7 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
onChange={(e) => handleFeedbackChange(e.target.value)} onChange={(e) => handleFeedbackChange(e.target.value)}
placeholder="Provide your feedback on the project..." placeholder="Provide your feedback on the project..."
rows={8} rows={8}
disabled={isReadOnly}
/> />
{requireFeedback && ( {requireFeedback && (
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
@@ -931,32 +975,44 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
</CardContent> </CardContent>
</Card> </Card>
<div className="flex items-center justify-between flex-wrap gap-4"> {isReadOnly ? (
<Button <div className="flex items-center">
variant="outline"
onClick={() => router.push(`/jury/competitions/${roundId}/projects/${projectId}` as Route)}
>
Cancel
</Button>
<div className="flex gap-3">
<Button <Button
variant="outline" variant="outline"
onClick={handleSaveDraft} onClick={() => router.back()}
disabled={autosaveMutation.isPending || submitMutation.isPending}
> >
<Save className="mr-2 h-4 w-4" /> <ArrowLeft className="mr-2 h-4 w-4" />
{autosaveMutation.isPending ? 'Saving...' : 'Save Draft'} Back
</Button>
<Button
onClick={handleSubmit}
disabled={submitMutation.isPending || isSubmitting}
className="bg-brand-blue hover:bg-brand-blue-light"
>
<Send className="mr-2 h-4 w-4" />
{submitMutation.isPending ? 'Submitting...' : 'Submit Evaluation'}
</Button> </Button>
</div> </div>
</div> ) : (
<div className="flex items-center justify-between flex-wrap gap-4">
<Button
variant="outline"
onClick={() => router.push(`/jury/competitions/${roundId}/projects/${projectId}` as Route)}
>
Cancel
</Button>
<div className="flex gap-3">
<Button
variant="outline"
onClick={handleSaveDraft}
disabled={autosaveMutation.isPending || submitMutation.isPending}
>
<Save className="mr-2 h-4 w-4" />
{autosaveMutation.isPending ? 'Saving...' : 'Save Draft'}
</Button>
<Button
onClick={handleSubmit}
disabled={submitMutation.isPending || isSubmitting}
className="bg-brand-blue hover:bg-brand-blue-light"
>
<Send className="mr-2 h-4 w-4" />
{submitMutation.isPending ? 'Submitting...' : 'Submit Evaluation'}
</Button>
</div>
</div>
)}
</div> </div>
) )
} }

View File

@@ -1,6 +1,6 @@
'use client' 'use client'
import { useParams } from 'next/navigation' import { useParams, useRouter } from 'next/navigation'
import Link from 'next/link' import Link from 'next/link'
import type { Route } from 'next' import type { Route } from 'next'
import { trpc } from '@/lib/trpc/client' import { trpc } from '@/lib/trpc/client'
@@ -13,6 +13,7 @@ import { ArrowLeft, FileText, Users, MapPin, Target, Tag } from 'lucide-react'
export default function JuryProjectDetailPage() { export default function JuryProjectDetailPage() {
const params = useParams() const params = useParams()
const router = useRouter()
const roundId = params.roundId as string const roundId = params.roundId as string
const projectId = params.projectId as string const projectId = params.projectId as string
@@ -42,11 +43,9 @@ export default function JuryProjectDetailPage() {
if (!project) { if (!project) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<Button variant="ghost" size="sm" asChild> <Button variant="ghost" size="sm" onClick={() => router.back()}>
<Link href={`/jury/competitions/${roundId}` as Route}> <ArrowLeft className="mr-2 h-4 w-4" />
<ArrowLeft className="mr-2 h-4 w-4" /> Back
Back
</Link>
</Button> </Button>
<Card> <Card>
<CardContent className="flex flex-col items-center justify-center py-12"> <CardContent className="flex flex-col items-center justify-center py-12">
@@ -61,11 +60,9 @@ export default function JuryProjectDetailPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Button variant="ghost" size="sm" asChild> <Button variant="ghost" size="sm" onClick={() => router.back()}>
<Link href={`/jury/competitions/${roundId}` as Route}> <ArrowLeft className="mr-2 h-4 w-4" />
<ArrowLeft className="mr-2 h-4 w-4" /> Back
Back
</Link>
</Button> </Button>
</div> </div>

View File

@@ -2,6 +2,7 @@
import Link from 'next/link' import Link from 'next/link'
import type { Route } from 'next' import type { Route } from 'next'
import { useRouter } from 'next/navigation'
import { trpc } from '@/lib/trpc/client' import { trpc } from '@/lib/trpc/client'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
@@ -20,6 +21,7 @@ import {
import { formatDateOnly, formatEnumLabel } from '@/lib/utils' import { formatDateOnly, formatEnumLabel } from '@/lib/utils'
export default function JuryAssignmentsPage() { export default function JuryAssignmentsPage() {
const router = useRouter()
const { data: assignments, isLoading } = trpc.assignment.myAssignments.useQuery({}) const { data: assignments, isLoading } = trpc.assignment.myAssignments.useQuery({})
if (isLoading) { if (isLoading) {
@@ -58,11 +60,9 @@ export default function JuryAssignmentsPage() {
Projects assigned to you for evaluation Projects assigned to you for evaluation
</p> </p>
</div> </div>
<Button variant="ghost" size="sm" asChild className="hidden md:inline-flex"> <Button variant="ghost" size="sm" onClick={() => router.back()} className="hidden md:inline-flex">
<Link href={'/jury' as Route}> <ArrowLeft className="mr-2 h-4 w-4" />
<ArrowLeft className="mr-2 h-4 w-4" /> Back
Back to Dashboard
</Link>
</Button> </Button>
</div> </div>

View File

@@ -1,8 +1,7 @@
'use client' 'use client'
import { useEffect } from 'react' import { useEffect } from 'react'
import { useParams } from 'next/navigation' import { useParams, useRouter } from 'next/navigation'
import Link from 'next/link'
import dynamic from 'next/dynamic' import dynamic from 'next/dynamic'
import { trpc } from '@/lib/trpc/client' import { trpc } from '@/lib/trpc/client'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
@@ -27,6 +26,7 @@ const ResourceRenderer = dynamic(
export default function JuryResourceDetailPage() { export default function JuryResourceDetailPage() {
const params = useParams() const params = useParams()
const router = useRouter()
const resourceId = params.id as string const resourceId = params.id as string
const { data: resource, isLoading, error } = trpc.learningResource.get.useQuery({ id: resourceId }) const { data: resource, isLoading, error } = trpc.learningResource.get.useQuery({ id: resourceId })
@@ -73,11 +73,9 @@ export default function JuryResourceDetailPage() {
This resource may have been removed or you don&apos;t have access. This resource may have been removed or you don&apos;t have access.
</AlertDescription> </AlertDescription>
</Alert> </Alert>
<Button asChild> <Button onClick={() => router.back()}>
<Link href="/jury/learning"> <ArrowLeft className="mr-2 h-4 w-4" />
<ArrowLeft className="mr-2 h-4 w-4" /> Back
Back to Learning Hub
</Link>
</Button> </Button>
</div> </div>
) )
@@ -87,11 +85,9 @@ export default function JuryResourceDetailPage() {
<div className="space-y-6"> <div className="space-y-6">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Button variant="ghost" asChild className="-ml-4"> <Button variant="ghost" onClick={() => router.back()} className="-ml-4">
<Link href="/jury/learning"> <ArrowLeft className="mr-2 h-4 w-4" />
<ArrowLeft className="mr-2 h-4 w-4" /> Back
Back to Learning Hub
</Link>
</Button> </Button>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{resource.externalUrl && ( {resource.externalUrl && (

View File

@@ -623,7 +623,7 @@ async function JuryDashboardContent() {
<div className="rounded-lg bg-brand-teal/10 p-1.5 dark:bg-brand-teal/20"> <div className="rounded-lg bg-brand-teal/10 p-1.5 dark:bg-brand-teal/20">
<BarChart3 className="h-4 w-4 text-brand-teal" /> <BarChart3 className="h-4 w-4 text-brand-teal" />
</div> </div>
<CardTitle className="text-lg">Stage Summary</CardTitle> <CardTitle className="text-lg">Round Summary</CardTitle>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">

View File

@@ -1,8 +1,7 @@
'use client' 'use client'
import { Suspense, use, useState, useEffect } from 'react' import { Suspense, use, useState, useEffect } from 'react'
import Link from 'next/link' import { useRouter } from 'next/navigation'
import type { Route } from 'next'
import { trpc } from '@/lib/trpc/client' import { trpc } from '@/lib/trpc/client'
import { import {
Card, Card,
@@ -75,6 +74,7 @@ const statusColors: Record<string, 'default' | 'secondary' | 'destructive' | 'ou
} }
function ProjectDetailContent({ projectId }: { projectId: string }) { function ProjectDetailContent({ projectId }: { projectId: string }) {
const router = useRouter()
const { data: project, isLoading, error } = trpc.mentor.getProjectDetail.useQuery({ const { data: project, isLoading, error } = trpc.mentor.getProjectDetail.useQuery({
projectId, projectId,
}) })
@@ -106,11 +106,9 @@ function ProjectDetailContent({ projectId }: { projectId: string }) {
if (error || !project) { if (error || !project) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<Button variant="ghost" asChild className="-ml-4"> <Button variant="ghost" className="-ml-4" onClick={() => router.back()}>
<Link href={'/mentor' as Route}> <ArrowLeft className="mr-2 h-4 w-4" />
<ArrowLeft className="mr-2 h-4 w-4" /> Back
Back to Dashboard
</Link>
</Button> </Button>
<Card> <Card>
@@ -122,8 +120,8 @@ function ProjectDetailContent({ projectId }: { projectId: string }) {
<p className="text-sm text-muted-foreground mt-1"> <p className="text-sm text-muted-foreground mt-1">
You may not have access to view this project. You may not have access to view this project.
</p> </p>
<Button asChild className="mt-4"> <Button className="mt-4" onClick={() => router.back()}>
<Link href={'/mentor' as Route}>Back to Dashboard</Link> Back
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>
@@ -140,11 +138,9 @@ function ProjectDetailContent({ projectId }: { projectId: string }) {
<div className="space-y-6"> <div className="space-y-6">
{/* Header */} {/* Header */}
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Button variant="ghost" asChild className="-ml-4"> <Button variant="ghost" className="-ml-4" onClick={() => router.back()}>
<Link href={'/mentor' as Route}> <ArrowLeft className="mr-2 h-4 w-4" />
<ArrowLeft className="mr-2 h-4 w-4" /> Back
Back to Dashboard
</Link>
</Button> </Button>
</div> </div>

View File

@@ -1,8 +1,7 @@
'use client' 'use client'
import { useEffect } from 'react' import { useEffect } from 'react'
import { useParams } from 'next/navigation' import { useParams, useRouter } from 'next/navigation'
import Link from 'next/link'
import dynamic from 'next/dynamic' import dynamic from 'next/dynamic'
import { trpc } from '@/lib/trpc/client' import { trpc } from '@/lib/trpc/client'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
@@ -27,6 +26,7 @@ const ResourceRenderer = dynamic(
export default function MentorResourceDetailPage() { export default function MentorResourceDetailPage() {
const params = useParams() const params = useParams()
const router = useRouter()
const resourceId = params.id as string const resourceId = params.id as string
const { data: resource, isLoading, error } = trpc.learningResource.get.useQuery({ id: resourceId }) const { data: resource, isLoading, error } = trpc.learningResource.get.useQuery({ id: resourceId })
@@ -73,11 +73,9 @@ export default function MentorResourceDetailPage() {
This resource may have been removed or you don&apos;t have access. This resource may have been removed or you don&apos;t have access.
</AlertDescription> </AlertDescription>
</Alert> </Alert>
<Button asChild> <Button onClick={() => router.back()}>
<Link href="/mentor/resources"> <ArrowLeft className="mr-2 h-4 w-4" />
<ArrowLeft className="mr-2 h-4 w-4" /> Back
Back to Resources
</Link>
</Button> </Button>
</div> </div>
) )
@@ -87,11 +85,9 @@ export default function MentorResourceDetailPage() {
<div className="space-y-6"> <div className="space-y-6">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Button variant="ghost" asChild className="-ml-4"> <Button variant="ghost" className="-ml-4" onClick={() => router.back()}>
<Link href="/mentor/resources"> <ArrowLeft className="mr-2 h-4 w-4" />
<ArrowLeft className="mr-2 h-4 w-4" /> Back
Back to Resources
</Link>
</Button> </Button>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{resource.externalUrl && ( {resource.externalUrl && (

View File

@@ -1,8 +1,6 @@
'use client' 'use client'
import { useParams } 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' import { trpc } from '@/lib/trpc/client'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
@@ -16,6 +14,7 @@ import { toast } from 'sonner'
export default function MentorWorkspaceDetailPage() { export default function MentorWorkspaceDetailPage() {
const params = useParams() const params = useParams()
const router = useRouter()
const projectId = params.projectId as string const projectId = params.projectId as string
// Get mentor assignment for this project // Get mentor assignment for this project
@@ -39,11 +38,9 @@ export default function MentorWorkspaceDetailPage() {
if (!project) { if (!project) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<Button variant="ghost" size="sm" asChild> <Button variant="ghost" size="sm" onClick={() => router.back()}>
<Link href={'/mentor/workspace' as Route}> <ArrowLeft className="mr-2 h-4 w-4" />
<ArrowLeft className="mr-2 h-4 w-4" /> Back
Back
</Link>
</Button> </Button>
<Card> <Card>
<CardContent className="flex flex-col items-center justify-center py-12"> <CardContent className="flex flex-col items-center justify-center py-12">
@@ -58,11 +55,9 @@ export default function MentorWorkspaceDetailPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Button variant="ghost" size="sm" asChild> <Button variant="ghost" size="sm" onClick={() => router.back()}>
<Link href={'/mentor/workspace' as Route}> <ArrowLeft className="mr-2 h-4 w-4" />
<ArrowLeft className="mr-2 h-4 w-4" /> Back
Back
</Link>
</Button> </Button>
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-3 flex-wrap"> <div className="flex items-center gap-3 flex-wrap">

View File

@@ -2,6 +2,7 @@
import Link from 'next/link' import Link from 'next/link'
import type { Route } from 'next' import type { Route } from 'next'
import { useRouter } from 'next/navigation'
import { trpc } from '@/lib/trpc/client' import { trpc } from '@/lib/trpc/client'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
@@ -20,6 +21,7 @@ const statusColors: Record<string, 'default' | 'secondary' | 'destructive' | 'ou
} }
export default function MentorWorkspacePage() { export default function MentorWorkspacePage() {
const router = useRouter()
const { data: assignments, isLoading } = trpc.mentor.getMyProjects.useQuery() const { data: assignments, isLoading } = trpc.mentor.getMyProjects.useQuery()
if (isLoading) { if (isLoading) {
@@ -46,11 +48,9 @@ export default function MentorWorkspacePage() {
Collaborate with your assigned mentee projects Collaborate with your assigned mentee projects
</p> </p>
</div> </div>
<Button variant="ghost" size="sm" asChild> <Button variant="ghost" size="sm" onClick={() => router.back()}>
<Link href={'/mentor' as Route}> <ArrowLeft className="mr-2 h-4 w-4" />
<ArrowLeft className="mr-2 h-4 w-4" /> Back
Back to Dashboard
</Link>
</Button> </Button>
</div> </div>

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import { useState } from 'react' import { useState } from 'react'
import { useParams } from 'next/navigation' import { useParams, useRouter } from 'next/navigation'
import Link from 'next/link' import Link from 'next/link'
import type { Route } from 'next' import type { Route } from 'next'
import { useSession } from 'next-auth/react' import { useSession } from 'next-auth/react'
@@ -67,6 +67,7 @@ const fileTypeLabels: Record<string, string> = {
export function SubmissionDetailClient() { export function SubmissionDetailClient() {
const params = useParams() const params = useParams()
const router = useRouter()
const { data: session } = useSession() const { data: session } = useSession()
const projectId = params.id as string const projectId = params.id as string
const [activeTab, setActiveTab] = useState('details') const [activeTab, setActiveTab] = useState('details')
@@ -116,11 +117,9 @@ export function SubmissionDetailClient() {
{error?.message || 'Submission not found'} {error?.message || 'Submission not found'}
</AlertDescription> </AlertDescription>
</Alert> </Alert>
<Button asChild className="mt-4"> <Button className="mt-4" onClick={() => router.back()}>
<Link href="/my-submission"> <ArrowLeft className="mr-2 h-4 w-4" />
<ArrowLeft className="mr-2 h-4 w-4" /> Back
Back to My Submissions
</Link>
</Button> </Button>
</div> </div>
) )
@@ -133,11 +132,9 @@ export function SubmissionDetailClient() {
<div className="max-w-4xl mx-auto space-y-6"> <div className="max-w-4xl mx-auto space-y-6">
{/* Header */} {/* Header */}
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Button variant="ghost" asChild className="-ml-4"> <Button variant="ghost" className="-ml-4" onClick={() => router.back()}>
<Link href="/my-submission"> <ArrowLeft className="mr-2 h-4 w-4" />
<ArrowLeft className="mr-2 h-4 w-4" /> Back
Back to My Submissions
</Link>
</Button> </Button>
</div> </div>

View File

@@ -203,10 +203,8 @@ export default function TeamManagementPage() {
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Button variant="ghost" size="icon" asChild> <Button variant="ghost" size="icon" onClick={() => router.back()}>
<Link href={`/my-submission/${projectId}`}> <ArrowLeft className="h-5 w-5" />
<ArrowLeft className="h-5 w-5" />
</Link>
</Button> </Button>
<div> <div>
<h1 className="text-2xl font-semibold tracking-tight flex items-center gap-2"> <h1 className="text-2xl font-semibold tracking-tight flex items-center gap-2">

View File

@@ -3,6 +3,7 @@
import { useState, useMemo } from 'react' import { useState, useMemo } from 'react'
import Link from 'next/link' import Link from 'next/link'
import type { Route } from 'next' import type { Route } from 'next'
import { useRouter } from 'next/navigation'
import { trpc } from '@/lib/trpc/client' import { trpc } from '@/lib/trpc/client'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
@@ -55,6 +56,7 @@ type SemiFinalistsContentProps = {
} }
export function SemiFinalistsContent({ editionId }: SemiFinalistsContentProps) { export function SemiFinalistsContent({ editionId }: SemiFinalistsContentProps) {
const router = useRouter()
const { data, isLoading } = trpc.dashboard.getSemiFinalistDetail.useQuery( const { data, isLoading } = trpc.dashboard.getSemiFinalistDetail.useQuery(
{ editionId }, { editionId },
{ enabled: !!editionId } { enabled: !!editionId }
@@ -116,11 +118,9 @@ export function SemiFinalistsContent({ editionId }: SemiFinalistsContentProps) {
{/* Header */} {/* Header */}
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href={'/admin' as Route}> <Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => router.back()}>
<Button variant="ghost" size="icon" className="h-8 w-8"> <ArrowLeft className="h-4 w-4" />
<ArrowLeft className="h-4 w-4" /> </Button>
</Button>
</Link>
<div> <div>
<h1 className="text-xl font-bold tracking-tight md:text-2xl"> <h1 className="text-xl font-bold tracking-tight md:text-2xl">
Semi-Finalists Semi-Finalists

View File

@@ -142,8 +142,6 @@ export function RoleNav({ navigation, roleName, user, basePath, statusBadge, edi
<a <a
key={item.name} key={item.name}
href={item.href} href={item.href}
target="_blank"
rel="noopener noreferrer"
className={className} className={className}
onClick={() => logNavClick.mutate({ url: item.href })} onClick={() => logNavClick.mutate({ url: item.href })}
> >
@@ -291,8 +289,6 @@ export function RoleNav({ navigation, roleName, user, basePath, statusBadge, edi
<a <a
key={item.name} key={item.name}
href={item.href} href={item.href}
target="_blank"
rel="noopener noreferrer"
onClick={() => { logNavClick.mutate({ url: item.href }); setIsMobileMenuOpen(false) }} onClick={() => { logNavClick.mutate({ url: item.href }); setIsMobileMenuOpen(false) }}
className={className} className={className}
> >

View File

@@ -2,6 +2,7 @@
import Link from 'next/link' import Link from 'next/link'
import type { Route } from 'next' import type { Route } from 'next'
import { useRouter } from 'next/navigation'
import { trpc } from '@/lib/trpc/client' import { trpc } from '@/lib/trpc/client'
import { import {
Card, Card,
@@ -43,6 +44,7 @@ import {
import { cn, formatDate, formatDateOnly } from '@/lib/utils' import { cn, formatDate, formatDateOnly } from '@/lib/utils'
export function ObserverProjectDetail({ projectId }: { projectId: string }) { export function ObserverProjectDetail({ projectId }: { projectId: string }) {
const router = useRouter()
const { data, isLoading } = trpc.analytics.getProjectDetail.useQuery( const { data, isLoading } = trpc.analytics.getProjectDetail.useQuery(
{ id: projectId }, { id: projectId },
{ refetchInterval: 30_000 }, { refetchInterval: 30_000 },
@@ -78,8 +80,8 @@ export function ObserverProjectDetail({ projectId }: { projectId: string }) {
<CardContent className="flex flex-col items-center justify-center py-12 text-center"> <CardContent className="flex flex-col items-center justify-center py-12 text-center">
<AlertCircle className="h-12 w-12 text-destructive/50" /> <AlertCircle className="h-12 w-12 text-destructive/50" />
<p className="mt-2 font-medium">Project Not Found</p> <p className="mt-2 font-medium">Project Not Found</p>
<Button asChild className="mt-4"> <Button className="mt-4" onClick={() => router.back()}>
<Link href={'/observer' as Route}>Back to Dashboard</Link> Back
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>
@@ -152,11 +154,9 @@ export function ObserverProjectDetail({ projectId }: { projectId: string }) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Back button */} {/* Back button */}
<Button variant="ghost" size="sm" className="gap-1.5 -ml-2 text-muted-foreground" asChild> <Button variant="ghost" size="sm" className="gap-1.5 -ml-2 text-muted-foreground" onClick={() => router.back()}>
<Link href={'/observer/projects' as Route}> <ArrowLeft className="h-3.5 w-3.5" />
<ArrowLeft className="h-3.5 w-3.5" /> Back
Back to Projects
</Link>
</Button> </Button>
{/* Project Header */} {/* Project Header */}

View File

@@ -927,13 +927,13 @@ function PlatformFeaturesSection({ settings }: { settings: Record<string, string
<Label className="text-sm font-medium">Learning Hub</Label> <Label className="text-sm font-medium">Learning Hub</Label>
<SettingToggle <SettingToggle
label="Use External Learning Hub" label="Use External Learning Hub"
description="When enabled, jury and mentor navigation links will open the external URL instead of the built-in Learning Hub" description="When enabled, all user navigation links (jury, mentor, applicant) will redirect to the external URL instead of the built-in Learning Hub"
settingKey="learning_hub_external" settingKey="learning_hub_external"
value={settings.learning_hub_external || 'false'} value={settings.learning_hub_external || 'false'}
/> />
<SettingInput <SettingInput
label="External URL" label="External URL"
description="The URL to redirect jury and mentor users to (e.g. Google Drive, Notion, etc.)" description="The URL to redirect users to (e.g. Google Drive, Notion, etc.)"
settingKey="learning_hub_external_url" settingKey="learning_hub_external_url"
value={settings.learning_hub_external_url || ''} value={settings.learning_hub_external_url || ''}
/> />

View File

@@ -64,7 +64,7 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
// (status NONE) who have no password yet. // (status NONE) who have no password yet.
const existingUser = await prisma.user.findUnique({ const existingUser = await prisma.user.findUnique({
where: { email: email.toLowerCase().trim() }, where: { email: email.toLowerCase().trim() },
select: { status: true }, select: { id: true, status: true },
}) })
if (!existingUser || existingUser.status === 'SUSPENDED') { if (!existingUser || existingUser.status === 'SUSPENDED') {
// Silently skip — don't reveal whether the email exists (prevents enumeration) // Silently skip — don't reveal whether the email exists (prevents enumeration)
@@ -72,6 +72,17 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
return return
} }
await sendMagicLinkEmail(email, url) await sendMagicLinkEmail(email, url)
// Audit: magic link sent
await prisma.auditLog.create({
data: {
userId: existingUser.id,
action: 'MAGIC_LINK_SENT',
entityType: 'User',
entityId: existingUser.id,
detailsJson: { email, timestamp: new Date().toISOString() },
},
}).catch(() => {})
}, },
}), }),
// Credentials provider for email/password login and invite token auth // Credentials provider for email/password login and invite token auth

View File

@@ -1628,14 +1628,44 @@ export const userRouter = router({
}) })
if (!user) { if (!user) {
await logAudit({
prisma: ctx.prisma,
userId: null,
action: 'PASSWORD_RESET_LINK_INVALID',
entityType: 'User',
entityId: 'unknown',
detailsJson: { reason: 'token_not_found', timestamp: new Date().toISOString() },
ipAddress: ctx.ip,
userAgent: ctx.userAgent,
}).catch(() => {})
throw new TRPCError({ code: 'BAD_REQUEST', message: 'Invalid or expired reset link. Please request a new one.' }) throw new TRPCError({ code: 'BAD_REQUEST', message: 'Invalid or expired reset link. Please request a new one.' })
} }
if (user.status === 'SUSPENDED') { if (user.status === 'SUSPENDED') {
await logAudit({
prisma: ctx.prisma,
userId: user.id,
action: 'PASSWORD_RESET_LINK_INVALID',
entityType: 'User',
entityId: user.id,
detailsJson: { reason: 'account_suspended', email: user.email, timestamp: new Date().toISOString() },
ipAddress: ctx.ip,
userAgent: ctx.userAgent,
}).catch(() => {})
throw new TRPCError({ code: 'FORBIDDEN', message: 'This account has been suspended.' }) throw new TRPCError({ code: 'FORBIDDEN', message: 'This account has been suspended.' })
} }
if (user.passwordResetExpiresAt && user.passwordResetExpiresAt < new Date()) { if (user.passwordResetExpiresAt && user.passwordResetExpiresAt < new Date()) {
await logAudit({
prisma: ctx.prisma,
userId: user.id,
action: 'PASSWORD_RESET_LINK_EXPIRED',
entityType: 'User',
entityId: user.id,
detailsJson: { email: user.email, expiredAt: user.passwordResetExpiresAt.toISOString(), timestamp: new Date().toISOString() },
ipAddress: ctx.ip,
userAgent: ctx.userAgent,
}).catch(() => {})
// Clear expired token // Clear expired token
await ctx.prisma.user.update({ await ctx.prisma.user.update({
where: { id: user.id }, where: { id: user.id },
@@ -1644,6 +1674,18 @@ export const userRouter = router({
throw new TRPCError({ code: 'BAD_REQUEST', message: 'This reset link has expired. Please request a new one.' }) throw new TRPCError({ code: 'BAD_REQUEST', message: 'This reset link has expired. Please request a new one.' })
} }
// Audit: reset link clicked and valid
await logAudit({
prisma: ctx.prisma,
userId: user.id,
action: 'PASSWORD_RESET_LINK_CLICKED',
entityType: 'User',
entityId: user.id,
detailsJson: { email: user.email, timestamp: new Date().toISOString() },
ipAddress: ctx.ip,
userAgent: ctx.userAgent,
}).catch(() => {})
// Hash and save new password, clear reset token // Hash and save new password, clear reset token
const passwordHash = await hashPassword(input.password) const passwordHash = await hashPassword(input.password)
await ctx.prisma.user.update({ await ctx.prisma.user.update({