feat(admin): recruitment console (PR A of 3) - #205
Conversation
Adds /admin/recruitment where an admin opens/closes the recruitment
drive and sets the Google Form URL /join embeds, with no redeploy.
- src/app/api/admin/recruitment/route.ts — PATCH, admin-gated, backed
by updateRecruitmentSettings() from PR 0. Returns the settings as
actually persisted (not an echo of the request), so the client syncs
to server truth.
- src/app/admin/recruitment/page.tsx + RecruitmentPanel.tsx — server
page + client form, mirroring admin/users + UserManagementPanel.
Save sends only the fields the admin changed — the backend does a
partial merge, so sending untouched fields too would silently
overwrite whatever another admin (or another tab) saved to them
since this panel loaded.
- src/app/admin/layout.tsx + AdminNav.tsx — lifts the nav shared by
all five admin pages into one place. Presentational only: every page
keeps its own getAdminUser()/redirect("/") gate and its own
`export const dynamic = "force-dynamic"`, and the route handler is
gated independently of the page.
Visible side effect: admin/teachers, admin/problems/new, and
admin/problems/[slug]/edit now show the nav too (they had none, or in
teachers' case were accidentally missing it).
Depends on PR 0 (codechefPesuecc#203, feat/recruitment-foundation) — branched off it
since codechefPesuecc#203 hasn't merged yet; rebase onto main once it does.
Verified locally (npm run dev): admin/non-admin/anonymous gating on
both the page and the route, bad-URL rejection with no write, real-URL
persistence across reload, partial-PATCH doesn't clobber other fields,
identical nav on all five admin pages. `npm run test`, `npx tsc
--noEmit`, `npm run lint`, `npm run build` all clean.
|
@ishanshaurya is attempting to deploy a commit to the barunaniket-9400's projects Team on Vercel. A member of the Team first needs to authorize it. |
There was a problem hiding this comment.
Approving. The part that mattered most here is right, and it's the part that was easiest to get wrong.
Security review of the nav extraction
The risk in this PR was the layout swallowing the auth gate. It doesn't:
- All six
/admin/*pages keep their owngetAdminUser()+redirect("/")and their ownexport const dynamic = "force-dynamic". Verified individually, including the three that only inherit the nav (teachers,problems/new,problems/[slug]/edit) — the diff removed markup and nothing else. PATCH /api/admin/recruitmentgates independently of the page, which is what matters for a handler reachable by direct POST.- The comment in
layout.tsxexplaining why the layout can't be the boundary is the right thing to leave behind for whoever touches this next.
Things you did that weren't asked for and should stay
- The panel sends only changed fields. Two admins in two tabs won't silently overwrite each other's untouched values. The comment explaining it is accurate.
- Syncing state from the server's persisted response instead of echoing the request. Correct for
updatedAt/updatedByand for any server-side normalization. - Disabling Save when
!dirty, and keeping the form dirty after a failed save so a retry is possible.
Checked and deliberately not flagged
No enforceRateLimits on the new route — but no existing route under src/app/api/admin/** uses it either, so this matches the codebase rather than deviating from it. Fine as is.
Cosmetic, not blocking — I'll take these in a follow-up
- Double top padding on the three inherited pages.
admin/page.tsxandadmin/users/page.tsxhadpt-6dropped from their section (correctly, since the layout now supplies it), butteachers,problems/newandproblems/[slug]/editstill carrypt-6— so those three get the gap twice. - Nav width mismatch on the problem pages. The layout wrapper is
max-w-4xl;problems/newandproblems/[slug]/editaremax-w-3xl, so the nav sits wider than the content it heads.
Neither is worth another round trip on your branch — merging this and fixing both in a follow-up.
Optional, from the plan, if you'd rather pick them up than have me: a preview link that opens the saved form in a new tab (so an admin can confirm the paste before opening the drive), and a plain-language status line — "Recruitment is open. /join is showing the form."
Follow-up to #205. Extracting the nav into admin/layout.tsx left three pages that only inherit it out of step with the two that were edited. - teachers, problems/new and problems/[slug]/edit still carried `pt-6` on their own section while the layout now supplies it, so those three rendered the gap twice. - The two problem pages were `max-w-3xl` against the layout's `max-w-4xl`, so the nav sat wider than the content it heads. Widened them rather than narrowing the nav: all six console pages now share one measure, which is the point of having a shared layout. It does make the problem-authoring form slightly wider — deliberate, and admin-only. Also picks up the two panel touches from the plan that #205 left optional: - A "Preview the saved form" link, so an admin can confirm the URL they pasted is the right form *before* opening the drive to applicants. - A plain-language line saying what /join is actually serving right now. Derived from the saved row rather than the editor fields on purpose — reading the unsaved state would announce a drive as open the moment someone ticked the box, before anything was written.
Admin console for the recruitment drive — open/close it and set the Google
Form /join embeds, no redeploy needed. Depends on #203 (merged) — already
rebased onto main.
What's here
PATCH /api/admin/recruitment— admin-gated, backed byupdateRecruitmentSettings()/admin/recruitmentpage +RecruitmentPanel— mirrorsadmin/users+UserManagementPaneladmin/layout.tsx+AdminNav— shared nav, extracted from the duplicated blocks inadmin/page.tsxandadmin/users/page.tsx. Visible side effect:admin/teachers,admin/problems/new, andadmin/problems/[slug]/editnow show this nav too (they hadnone, or in teachers' case were missing it).
The layout is presentational only — every page keeps its own
getAdminUser()+redirect("/")and its ownexport const dynamic = "force-dynamic"; the route handleris gated independently of the page.
Verified
npm run test/tsc --noEmit/lint/buildall cleanpersistence, bad-URL rejected with inline error and no write, partial save (toggle
alone) doesn't clobber the URL field, gate bounces both signed-out and signed-in
non-admin users to
/