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, })