diff --git a/.gitignore b/.gitignore
index 5f9ee0d..e135821 100644
--- a/.gitignore
+++ b/.gitignore
@@ -61,3 +61,4 @@ build-output.txt
# Private keys and secrets
private/
+public/build-id.json
diff --git a/next.config.ts b/next.config.ts
index 228aacf..3475016 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -2,9 +2,6 @@ import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
output: 'standalone',
- env: {
- NEXT_PUBLIC_BUILD_ID: Date.now().toString(),
- },
serverExternalPackages: ['@prisma/client', 'minio'],
typescript: {
// We run tsc --noEmit separately before each push
diff --git a/package.json b/package.json
index 446d71e..6ba3266 100644
--- a/package.json
+++ b/package.json
@@ -5,6 +5,7 @@
"type": "module",
"scripts": {
"dev": "next dev --turbopack",
+ "prebuild": "node -e \"require('fs').writeFileSync('public/build-id.json', JSON.stringify({buildId: Date.now().toString()}))\"",
"build": "next build",
"start": "next start",
"lint": "next lint",
diff --git a/src/app/api/version/route.ts b/src/app/api/version/route.ts
deleted file mode 100644
index 53c4c87..0000000
--- a/src/app/api/version/route.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { NextResponse } from 'next/server'
-
-export const dynamic = 'force-static'
-
-export function GET() {
- return NextResponse.json({ buildId: process.env.NEXT_PUBLIC_BUILD_ID })
-}
diff --git a/src/components/admin/members-content.tsx b/src/components/admin/members-content.tsx
index fd15957..5b77cf5 100644
--- a/src/components/admin/members-content.tsx
+++ b/src/components/admin/members-content.tsx
@@ -428,20 +428,19 @@ export function MembersContent() {
{user.role === 'APPLICANT' ? (
(() => {
- const info = (user as unknown as { applicantRoundInfo?: { roundName: string; state: string } | null }).applicantRoundInfo
+ const info = (user as unknown as { applicantRoundInfo?: { projectName: string; roundName: string; state: string } | null }).applicantRoundInfo
if (!info) return
-
const stateColor = info.state === 'REJECTED' ? 'destructive' as const
: info.state === 'WITHDRAWN' ? 'secondary' as const
: info.state === 'PASSED' ? 'success' as const
- : 'default' as const
- const stateLabel = info.state === 'IN_PROGRESS' ? 'Active'
- : info.state === 'PENDING' ? 'Pending'
- : info.state === 'COMPLETED' ? 'Completed'
+ : 'outline' as const
+ const stateLabel = info.state === 'REJECTED' ? 'Rejected'
+ : info.state === 'WITHDRAWN' ? 'Withdrawn'
: info.state === 'PASSED' ? 'Passed'
- : info.state
+ : info.roundName
return (
- {info.roundName}
+ {info.projectName}
{stateLabel}
@@ -543,25 +542,24 @@ export function MembersContent() {
- {user.role === 'APPLICANT' ? 'Current Round' : 'Assignments'}
+ {user.role === 'APPLICANT' ? 'Project' : 'Assignments'}
{user.role === 'APPLICANT' ? (
(() => {
- const info = (user as unknown as { applicantRoundInfo?: { roundName: string; state: string } | null }).applicantRoundInfo
+ const info = (user as unknown as { applicantRoundInfo?: { projectName: string; roundName: string; state: string } | null }).applicantRoundInfo
if (!info) return -
const stateColor = info.state === 'REJECTED' ? 'destructive' as const
: info.state === 'WITHDRAWN' ? 'secondary' as const
: info.state === 'PASSED' ? 'success' as const
- : 'default' as const
- const stateLabel = info.state === 'IN_PROGRESS' ? 'Active'
- : info.state === 'PENDING' ? 'Pending'
- : info.state === 'COMPLETED' ? 'Completed'
+ : 'outline' as const
+ const stateLabel = info.state === 'REJECTED' ? 'Rejected'
+ : info.state === 'WITHDRAWN' ? 'Withdrawn'
: info.state === 'PASSED' ? 'Passed'
- : info.state
+ : info.roundName
return (
-
- {info.roundName}
+
+ {info.projectName}
{stateLabel}
diff --git a/src/components/shared/version-guard.tsx b/src/components/shared/version-guard.tsx
index a5ca618..7ab013f 100644
--- a/src/components/shared/version-guard.tsx
+++ b/src/components/shared/version-guard.tsx
@@ -3,7 +3,9 @@
import { useEffect, useRef } from 'react'
import { toast } from 'sonner'
-const CLIENT_BUILD_ID = process.env.NEXT_PUBLIC_BUILD_ID
+// Capture the build ID when this module first loads (from the current deployment's JS bundle).
+// On subsequent fetches of /build-id.json, if the value differs, a new deploy happened.
+let initialBuildId: string | null = null
export function VersionGuard() {
const notified = useRef(false)
@@ -12,10 +14,19 @@ export function VersionGuard() {
async function checkVersion() {
if (notified.current) return
try {
- const res = await fetch('/api/version', { cache: 'no-store' })
+ const res = await fetch('/build-id.json?t=' + Date.now())
if (!res.ok) return
const { buildId } = await res.json()
- if (buildId && CLIENT_BUILD_ID && buildId !== CLIENT_BUILD_ID) {
+ if (!buildId) return
+
+ // First load — capture the build ID
+ if (initialBuildId === null) {
+ initialBuildId = buildId
+ return
+ }
+
+ // Subsequent checks — compare
+ if (buildId !== initialBuildId) {
notified.current = true
toast('A new version is available', {
description: 'Refresh to get the latest updates.',
@@ -31,6 +42,9 @@ export function VersionGuard() {
}
}
+ // Initial check (captures build ID)
+ checkVersion()
+
// Check on tab focus (covers users returning to stale tabs)
window.addEventListener('focus', checkVersion)
diff --git a/src/server/routers/user.ts b/src/server/routers/user.ts
index 5f7b0a0..7b8a974 100644
--- a/src/server/routers/user.ts
+++ b/src/server/routers/user.ts
@@ -299,7 +299,7 @@ export const userRouter = router({
// For APPLICANT users, attach their project's current round info
const applicantIds = users.filter((u) => u.role === 'APPLICANT').map((u) => u.id)
- const applicantRoundMap = new Map()
+ const applicantRoundMap = new Map()
if (applicantIds.length > 0) {
// Find each applicant's project, then the latest round state
@@ -342,11 +342,13 @@ export const userRouter = router({
if (!applicantIds.includes(uid)) continue
if (latestTerminal && !latestActive) {
applicantRoundMap.set(uid, {
+ projectName: proj.title,
roundName: latestTerminal.round.name,
state: latestTerminal.state,
})
} else if (latest) {
applicantRoundMap.set(uid, {
+ projectName: proj.title,
roundName: latest.round.name,
state: latest.state,
})