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
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:
@@ -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 to Award
|
Back
|
||||||
</Link>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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 to Awards
|
Back
|
||||||
</Link>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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 to Awards
|
Back
|
||||||
</Link>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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 to Juries
|
Back
|
||||||
</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>
|
||||||
|
|||||||
@@ -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're looking for does not exist.
|
The resource you'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 to Learning Hub
|
Back
|
||||||
</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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 to Members
|
Back
|
||||||
</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 to Members
|
Back
|
||||||
</Link>
|
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Header Hero */}
|
{/* Header Hero */}
|
||||||
|
|||||||
@@ -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 to Members
|
Back
|
||||||
</Link>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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 to Messages
|
Back
|
||||||
</Link>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 to Programs
|
Back
|
||||||
</Link>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 to Projects
|
Back
|
||||||
</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 to Project
|
Back
|
||||||
</Link>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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 to Project
|
Back
|
||||||
</Link>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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 to Projects
|
Back
|
||||||
</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 to Projects
|
Back
|
||||||
</Link>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 to Projects
|
Back
|
||||||
</Link>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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 to Projects
|
Back
|
||||||
</Link>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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">{round.specialAwardId ? 'Back to Award' : 'Back to Rounds'}</span>
|
<span className="text-xs hidden sm:inline">Back</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 */}
|
||||||
|
|||||||
@@ -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 to Settings
|
Back
|
||||||
</Link>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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 to Settings
|
Back
|
||||||
</Link>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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 to Dashboard
|
Back
|
||||||
</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>
|
||||||
|
|||||||
@@ -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't have access.
|
This resource may have been removed or you don'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 to Resources
|
Back
|
||||||
</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 to Resources
|
Back
|
||||||
</Link>
|
|
||||||
</Button>
|
</Button>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{resource.externalUrl && (
|
{resource.externalUrl && (
|
||||||
|
|||||||
@@ -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 to Awards
|
Back
|
||||||
</Link>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
<Badge variant="default" className="bg-emerald-50 text-emerald-700 border-emerald-200">
|
||||||
<CheckCircle2 className="mr-1 h-3 w-3" />
|
<CheckCircle2 className="mr-1 h-3 w-3" />
|
||||||
Completed
|
Completed
|
||||||
</Badge>
|
</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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
{isReadOnly ? (
|
||||||
|
<Button variant="ghost" size="sm" onClick={() => router.back()}>
|
||||||
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
<Button variant="ghost" size="sm" asChild>
|
<Button variant="ghost" size="sm" asChild>
|
||||||
<Link href={`/jury/competitions/${roundId}/projects/${projectId}` as Route}>
|
<Link href={`/jury/competitions/${roundId}/projects/${projectId}` as Route}>
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
Back to Project
|
Back to Project
|
||||||
</Link>
|
</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">
|
||||||
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,9 +618,24 @@ 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} />
|
||||||
|
|
||||||
|
{!isReadOnly && (
|
||||||
<Card className="border-l-4 border-l-amber-500">
|
<Card className="border-l-4 border-l-amber-500">
|
||||||
<CardContent className="flex items-start gap-3 p-4">
|
<CardContent className="flex items-start gap-3 p-4">
|
||||||
<AlertCircle className="h-5 w-5 text-amber-600 shrink-0 mt-0.5" />
|
<AlertCircle className="h-5 w-5 text-amber-600 shrink-0 mt-0.5" />
|
||||||
@@ -621,6 +648,7 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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,6 +975,17 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{isReadOnly ? (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
>
|
||||||
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div className="flex items-center justify-between flex-wrap gap-4">
|
<div className="flex items-center justify-between flex-wrap gap-4">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -957,6 +1012,7 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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 to Dashboard
|
Back
|
||||||
</Link>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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't have access.
|
This resource may have been removed or you don'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 to Learning Hub
|
Back
|
||||||
</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 to Learning Hub
|
Back
|
||||||
</Link>
|
|
||||||
</Button>
|
</Button>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{resource.externalUrl && (
|
{resource.externalUrl && (
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 to Dashboard
|
Back
|
||||||
</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 to Dashboard
|
Back
|
||||||
</Link>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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't have access.
|
This resource may have been removed or you don'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 to Resources
|
Back
|
||||||
</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 to Resources
|
Back
|
||||||
</Link>
|
|
||||||
</Button>
|
</Button>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{resource.externalUrl && (
|
{resource.externalUrl && (
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 to Dashboard
|
Back
|
||||||
</Link>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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 to My Submissions
|
Back
|
||||||
</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 to My Submissions
|
Back
|
||||||
</Link>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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 to Projects
|
Back
|
||||||
</Link>
|
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Project Header */}
|
{/* Project Header */}
|
||||||
|
|||||||
@@ -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 || ''}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
Reference in New Issue
Block a user