Fix event links, the related-event lock, certificates and mobile access - #24
Merged
Conversation
Pixel-for-pixel port of FED-Frontend (React 18 + Vite) and FED-Backend (Express) into a single Next.js 16 App Router application, running against the same MongoDB. The original SCSS modules are carried over verbatim (84 stylesheets, 24 image assets) and components keep their original markup and class names, which is what makes the replica indistinguishable from the original rather than merely similar. URLs keep the original casing (/Events, /Team, /Login). Verified against both apps running side by side: - /Events and /Team render at identical total page height (6109px, 9461px) with matching element counts and computed styles - All five public API responses are byte-identical to the Express server - typecheck clean, eslint 0 errors, build 68 routes, npm audit 0 vulns Includes all 48 Express endpoints, ported as Route Handlers preserving the original JSON contracts. Security fixes (npm audit 44 -> 0): - xlsx moved to the official SheetJS distribution (prototype pollution) - react-share-social removed for a local react-share component (-530 pkgs) - postcss and sharp pinned to patched versions via overrides - JWT_SECRET was 4 characters; replaced with 64 bytes of CSPRNG entropy, which rotates existing sessions - BCRYPT_SALT_ROUNDS held a bcrypt salt string rather than a cost factor - contact endpoint now validates email format and message length See MIGRATION.md for the full record, including known issues.
The original never navigated from inside Login.jsx — it called
authCtx.login(...) and let the React Router table react:
<Route path="/Login" element={authCtx.isLoggedIn ? <LoginRedirect /> : <Login />} />
App Router routes are files, so nothing observed isLoggedIn, and proxy.ts
only runs on a server request — which a client-side sign-in never makes.
A correct login showed "Login successful" and then sat on /Login forever.
AuthRouteGuard reinstates the guard for all five auth routes, honouring
both the original sessionStorage.prevPage and the ?next= the proxy
appends. It rejects protocol-relative return URLs, and cannot ping-pong
with the proxy when localStorage holds a session the cookie no longer
backs.
The layout-level guard added in 1b17715 was the wrong shape. SignUp and CompleteProfile sign the user in and then navigate themselves to "/", and a guard reacting to isLoggedIn cancels that in-flight router.push before it commits — measured on the signup flow, the push never reached history and a new account landed on /profile instead of "/". No delay fixes that reliably, since the push only commits once its RSC payload arrives. Login.jsx, GoogleLogin.jsx and GoogleSignup.jsx already carry the shouldNavigate/navigatePath state and the effect that acts on it — dead in the original precisely because the route table did the job. Setting setShouldNavigate(true) after authCtx.login(...) brings it to life, with no cross-component race. SendOtp.jsx already did this and is unchanged. postAuthRedirect() resolves the destination the way LoginRedirect did, plus the ?next= proxy.ts appends, discarding anything that is not a plain internal path. Verified against the running server, signed out and signed in: all five auth pages, all seven /profile pages, casing redirects, a tampered cookie (rejected and cleared), and the seven auth endpoints on an empty body. Two pre-existing gaps documented in MIGRATION.md rather than silently changed: /ForgotPassword submits natively (identical in the original), and /profile/members and /profile/BlogForm are not access-gated in the UI.
A correct login showed "Login successful" and then sat on /Login forever.
App.jsx never navigated from inside Login.jsx — it called authCtx.login(...)
and let the route table react:
<Route path="/Login" element={authCtx.isLoggedIn ? <LoginRedirect /> : <Login />} />
App Router routes are files, so nothing observes isLoggedIn, and proxy.ts only
runs on a server request, which a client-side sign-in never makes.
The redirect belongs in the components rather than in a layout wrapper. A guard
in app/(auth)/layout.jsx reacting to isLoggedIn was tried first and is wrong:
SignUp and CompleteProfile sign the user in and then navigate themselves to
"/", and a layout guard cancels that in-flight router.push before it commits.
Measured on the signup flow, the push never reached history at all and a new
account landed on /profile instead of "/". No delay fixes that reliably, since
the push only commits once its RSC payload arrives.
Login.jsx, GoogleLogin.jsx and GoogleSignup.jsx already carry the
shouldNavigate/navigatePath state and the effect that acts on it — dead in the
original precisely because the route table did the job. Setting
setShouldNavigate(true) after authCtx.login(...) brings it to life, with no
cross-component race. SendOtp.jsx already did this and is unchanged.
postAuthRedirect() resolves the destination the way LoginRedirect did, plus the
?next= proxy.ts appends. That value comes off the query string and is therefore
attacker-supplied, so anything that is not a plain internal path is discarded.
Verified in the browser by driving the real forms with the API stubbed:
Login /Login -> /profile
Login /Login?next=/Events -> /Events
Login /Login?next=//example.com/phish -> /profile, origin preserved
Login blocked page -> login -> back to it, prevPage cleared
Signup /SignUp -> /, matching the original
Login stale localStorage, no cookie -> login form, one bounce, no loop
Also verified every auth route against the running server, signed out and
signed in: all five auth pages, all seven /profile pages, the lowercase casing
redirects, a tampered cookie (rejected and cleared), and the seven auth
endpoints on an empty body.
Two pre-existing gaps are documented in MIGRATION.md rather than silently
changed: /ForgotPassword submits natively (identical in the original), and
/profile/members and /profile/BlogForm are not access-gated in the UI.
typecheck clean, eslint 0 errors, build 68 routes.
The landing page shipped 2.3 MB of JavaScript. Home.jsx imported its four
sections from the `sections` barrel, which also re-exports `sections/Profile` —
the whole admin panel. All of them are client components, so the bundler pulled
that entire graph onto the landing page: certificate tooling, admin tables, the
avatar editor, event analytics. The `features` barrel did the same for
LiveEventPopup. Under Vite this cost nothing noticeable; under Next, where each
route gets its own bundle, a barrel import silently undoes the code splitting.
Importing those four components directly, leaving the barrels in place for
other call sites:
/ 2317 KB -> 1082 KB
/Events 2063 KB -> 1082 KB
/Team 2014 KB -> 1082 KB
/Login 1248 KB -> 920 KB
Over the wire the landing page is now 327 KB of JS; locally TTFB 38ms,
DOMContentLoaded 135ms, load 536ms.
EventCard wrapped its meta row in a <p> containing div.price, which contained
another <p>. Client-rendered that was invisible, because React builds the DOM
node by node and nothing reparents it. Server-rendered it is real markup, so
the parser closed the <p> early and div.price came out a sibling instead of a
child — a different layout, reported as a hydration mismatch on every card. The
wrapper is a <div className={style.meta}> and the two `.eventname p` rules list
`.eventname .meta` alongside, so computed styles are unchanged: verified in the
browser at font-size 14.4px, display flex, align-items center, margin-top 1.6px
with div.price a real child, console clean across 580 cards.
Also: darken() -> color.adjust() in VerifyCertificate.module.scss, confirmed
identical by compiling both and diffing (rgb(80%, 43.2941176471%, 0%) either
way), which clears the Dart Sass deprecation warnings; and <html> now carries
data-scroll-behavior="smooth" to acknowledge the globals.scss rule.
The <Fit /> warnings are left alone — react-fit is a transitive dependency of
react-date-picker and uses the `warning` package, a no-op in production.
typecheck clean, eslint 0 errors, build 68 routes, all pages 200, auth route
matrix unchanged.
Four components wrapped block-level content in a <p>: EventCard and EventModal (<p> -> div.price -> <p>), Hero (<p> -> <span> -> <h3>) and Social (<p> -> div.fed). Client-rendered under Vite none of it mattered, because React builds the DOM node by node and nothing reparents an existing tree. Server-rendered it is real markup, so the parser closes the <p> at the first block child and the content lands as a sibling — a different layout, reported as a hydration mismatch. EventCard was fixed in 9a1073e; this covers the other three. Each wrapper is a <div> carrying a class listed alongside the original `p` selector, so computed styles are unchanged. Verified in the browser: EventModal .meta 14.4px / flex / center / 1.6px / #fff Hero .tagline 39.2px / 700 / #fff Social .content 40px / 600 / #fff / center Hero keeps its <h3> rather than downgrading it to a span — the wrapper changed instead, so the heading still counts as a heading. Social's `styles.content` had no rule in the stylesheet at all, so the className resolved to undefined and the element was styled purely by `.text p`; `.content` now exists and carries those declarations. Fixing Social surfaced a second, unrelated mismatch on that page: react-social-media-embed mints a fresh UUID per render into both `id` and `className`, and the embed sizes come from useDimensions(), which measures 0 on the server. Both embeds now load via next/dynamic with ssr: false — nothing is lost, since the visible post is drawn by Instagram's and LinkedIn's own scripts after mount. Same four embeds, same 1674px page height, console clean. npm run audit:nesting keeps this from recurring. It walks a tag stack through every JSX file and reports anything the HTML parser would reparent, skipping comments and string/regex literals — without that a comment mentioning <p>, or `.replace(/<a\s[^>]*>/gi, '')`, is read as markup. It was validated against the pre-fix files: it reports all four real cases and nothing for the two files that produced false positives. Current tree: 153 files, zero findings. typecheck clean, eslint 0 errors, build 68 routes, no Sass deprecations.
The calendar on the admin form page had no styling at all. The original
index.scss imported three vendor stylesheets and only one was carried over, so
react-date-picker's popup — which it draws with react-calendar — came out as an
unstyled column of numbers. Confirmed by grepping the emitted CSS: zero
.react-calendar rules shipped, now 50. In the browser the popup measures
350x248 with a #a0a096 border, react-calendar's own defaults.
Re-reading every form/event controller against its port turned up more:
getFormAnalytics returned the wrong shape entirely. EventStats.jsx reads
response.data.form.formAnalytics, .form.info and .yearCounts; the port returned
{ success, message, data }, so response.data.form was undefined and the admin
analytics panel threw. It now returns { message, form, yearCounts }, rebuilds
the yearCounts histogram, and restores the 404 for a form nobody has registered
to. Access is the controller's own allowlist (ADMIN/PRESIDENT/VICEPRESIDENT/
DIRECTOR_*) plus the srex@fedkiit.com escape hatch, answering 401 "Access
Denied", not the isMember test the port had invented.
The attendance flow did not work. The QR carries a signed JWT: attendanceCode
must return { message, attendanceToken } with a 20-minute expiry, and
markAttendance takes { formId, token } and verifies it. The port returned the
raw record id under a different key, so QRCodeModal generated no QR, and
markAttendance rejected the scanned JWT for not being a 24-character id. Both
now match, including the formId binding that stops one event's QR checking
someone in at another, and the ?teamCode= branch. Verified both directions: a
token minted by the original's jsonwebtoken call verifies with jose here, and
vice versa; tampered tokens are rejected.
Access levels corrected both ways: export-attendance is ADMIN again (was any
club member, which handed every executive the full attendee list), while
markAttendance drops to signed-in, matching the Express route whose checkAccess
is commented out — the door volunteer signs in as a plain USER, so requiring
member access locked the door staff out.
Cloudinary dimensions were wrong: addForm/editForm resize FormImages at
h350.67 x w196.37 and QR media at h400 x w150 and h150 x w400 respectively, not
the 1000x1000 / 500x500 the port used. The two QR rows are transposed against
each other in the original and are reproduced per call site rather than
reconciled. addForm also returns 200 "Form created successfully" and editForm
"Form info and sections updated successfully".
One divergence kept deliberately: Express's addForm computes
Boolean(isPublic) || false over a multipart field, and Boolean("false") is
true, so every event created through the admin form was public,
registration-closed and past regardless of the toggles. editForm already used
=== "true". The port uses editForm's form in both so the switches work;
restoring byte-parity would re-break them. Documented in MIGRATION.md along
with checkAccess("USER") being stricter in Express than here.
typecheck clean, eslint 0 errors, build 68 routes, nesting audit clean.
Introduces a broad UI refresh across auth, events, team, profile, blog, chatbot, and modal surfaces using shared design tokens, updated layouts, and improved accessibility/interaction patterns (including a reusable CloseButton and new event artwork/disclosure components). Adds certificate backend support with new routes (`createOrganisationEvent`, `getEventByFormId`, `myCertificates`) and matching service logic, plus related flow fixes (team management query cleanup, event/certificate lookup behavior) and `prisma generate` on postinstall.
The password-reset email carried a 6-digit code while every OTP screen renders four boxes, so the code could not be entered at all. The reset flow and signup share components/OtpInput, so both were affected. The UI was not at fault — it is four boxes in the original too, character for character. This port had raised OTP_LENGTH from 4 to 6 as hardening, without a consumer that could accept six. Reverted to 4, matching generateOtp(4, false, false, false). The argument for 6 does not hold here: 10,000 combinations are brute-forceable against the Express backend, which had no throttling at all, but RATE_LIMITS.passwordReset allows 6 attempts per 15 minutes and a code expires after 15, so an attacker gets at most 6 guesses out of 10,000 before the code stops existing. The rest of the OTP hardening is untouched: SHA-256 digest at rest, constant-time comparison, single-use, and expiry derived from createdAt rather than a setTimeout that never fires on a serverless host. Codes issued before this change are 6 digits and cannot be entered; they expire on their own within 15 minutes. typecheck clean, eslint 0 errors, build 68 routes.
The disabled "Resend OTP" button rendered with a grey box. Measured against the
original running side by side, it had background-color rgba(19,1,1,0.3) and a
2px outset border where the original has rgba(0,0,0,0) and 0px none — the
user-agent's disabled-button chrome showing through.
The cause is structural, not a mistranslation. TeamCard.module.scss has a
top-level bare `button { }` rule; Vite does not hash element selectors in CSS
Modules and bundles every module into one stylesheet for the SPA, so that rule
was live on every page of the original. Next code-splits CSS per route, so once
ported it only loaded where TeamCard did — /Team and /profile/members — and
buttons everywhere else lost their reset.
Declared in app/globals.scss, which reproduces the original cascade. At
specificity 0-0-1 every component's own class rules still win: verified by
diffing all three buttons on /Login between the two apps, identical background,
font-size, margins and box sizes, and the resend button on /otp now matches the
original on every property (transparent, 0px none, 14.4px, 16px margin-top,
126x19).
Ten other top-level :global(...) rules sit in the same trap and are listed in
MIGRATION.md. They are left alone deliberately — three are :global(*) and would
move layout on every page, so they want reviewing one at a time rather than
hoisting wholesale.
Separately, App.jsx renders <Chatbot /> above <Routes>, so it is on every route.
The port mounted it in the (main) layout, hiding it on /Login, /SignUp, /otp,
/ForgotPassword and /completeProfile. Moved to the root layout, inside Providers
since it reads AuthContext. Confirmed in the server-rendered HTML for all six
routes, and the toggle measures 72x72 on /Login in both apps.
typecheck clean, eslint 0 errors, build 68 routes.
/otp rejected a correct code with "Email, otp and password are required".
OtpInput.jsx posts { newPassword, confirmPassword, otp, email }, matching
Express; this handler destructured `password`, so the check failed before the
code was ever looked at.
Restored the rest of that controller's contract too, which had been simplified
away: 400 "Missing fields." when any of the four is absent, 409 on a
confirm-password mismatch, 404 "User not found!" (Express's checkAccess looks
the account up by the body's email, which is also why the endpoint works without
a session), 400 when the new password equals the old one, and
200 { status: "OK", message: "Password has been changed successfully !!" }.
This was the third contract break found by report rather than by testing, so
npm run audit:contracts now walks every api.*() call in the components, resolves
it to its Route Handler and flags payload keys the handler never reads. It found
two more live ones:
renameTeam UI sends newTeamName, handler read teamName
sendJoinRequest UI sends teamRegistrationId, handler read teamCode
Every rename failed on an empty name and every join request was rejected. Both
now match Express, including sendJoinRequest identifying the target by
registration row id — the value searchTeams already returns to the UI as
teamRegistrationId — with the explicit team.formId !== formId check that a
global row id makes necessary.
The audit also surfaced that the admin certificate tooling calls five endpoints
that do not exist here (getEvent, getEventByFormId, createOrganisationEvent,
sendBatchMails, sendCertViaEmail), out of eleven missing overall. That is a gap
rather than a regression — it was never built — and is documented in
MIGRATION.md rather than fixed in passing, since the certificate flow already
carries a deliberate architectural deviation.
typecheck clean, eslint 0 errors, build 68 routes.
React Router returns [params, setParams]; Next returns the params object itself.
Two components outside the team feature kept the array destructuring:
EventForm.jsx crashed opening the event registration form
VerifyCertificate.jsx crashed opening certificate verification
ReadonlyURLSearchParams is iterable, so destructuring it as an array does not
throw — it quietly yields the first [key, value] entry, or undefined when the
URL has no query string. The next .get() then fails with "Cannot read
properties of undefined (reading 'get')".
EventForm is the one that hid: it reads searchParams.get("teamCode") during
render, but ProtectedRoute redirects signed-out visitors first, so an anonymous
smoke test returns 200 and only a signed-in user reaches the crash.
Swept the tree for the rest of the React Router surface (useNavigate,
useLocation, Outlet, Navigate, to=): only comments remain.
TeamManagement.jsx has the same bug and is fixed on feature/team-member-fix,
where the rest of the team work lives.
GET|HEAD /api/health returns 200 when every dependency answers and 503 when one does not. There was no equivalent in the Express backend, so uptime monitors had to be pointed at a real endpoint like /api/form/getAllForms — which reads the whole forms collection on every poll and still answers 200 when the database is unreachable but the response is cached. The database check uses MongoDB's `ping`, the cheapest command the driver exposes: it touches no collection and reads no documents, so polling costs nothing. It is bounded at 3s. A hung TCP connection to Atlas does not reject — it waits for the driver's own much longer timeout — and a probe that hangs reads to a monitor as a timeout rather than a clean 503. The endpoint is unauthenticated (proxy.ts already excludes /api/), so the error body carries only "unreachable" or the timeout message. Driver errors can embed the connection string, which holds the Atlas password; the detail goes to the server log instead. HEAD is exported explicitly because Next does not derive it from GET for route handlers, and a monitor configured for HEAD would otherwise get 405 and report the site as down. force-dynamic + no-store: a health check answering from build output or a cache would report "ok" for a process that is not actually serving. Verified both paths against the real database: 200 with a 36ms ping, and 503 in 3.03s with DATABASE_URL pointed at an unreachable host (HEAD likewise 503).
The five components were already ported line for line; the backend behind them
was not. It had been written against a different data model, so most of the page
did not work.
The model: a team is one formRegistration row holding every member's address in
regTeamMemEmails and every member's answers in value, with userId as the leader.
Leaving or being removed lifts a person's entries out of that row into their own
UNAFFILIATED row, so they stay registered and can join or start another team.
The port assumed a row per member sharing a teamCode, earliest row as leader.
That assumption produced most of the faults:
teamDetails looked up by userId, so only the leader could load the page.
Confirmed on live data: the ownership query returns NULL for a
real member of team "ABC"; the membership query finds it.
teamDetails returned registrationId/regTeamMemEmails/isLeader, while the UI
reads eventTitle, leaderEmail, max/minTeamSize,
isRegistrationClosed, isEventPast and data.isTeamless — and
members lacked college and year.
teamDetails had no UNAFFILIATED branch, so TeamlessState — the entire
create/join flow — could never render.
searchTeams returned a flat array of {teamName,teamCode,size,maxSize,isFull}
and read ?q=, where the picker reads data.data.teams with
teamRegistrationId/teamSize/maxTeamSize/leaderName/
spotsRemaining/hasPendingRequest and sends ?search=. The list
rendered empty and the search box filtered nothing.
leaveTeam keyed on userId with no leader/member distinction: a member
could not leave, answers were not carried across, and the
tracker's regTeamNames was never released.
removeTeamMember assumed row-per-member, so removal failed and no email went
out.
Also restored: leaveTeam blocks a leader who still has members; the
closed-registration guard compares the flags as the strings "true"/"false" the
data actually stores; joinTeam returns eventId (falling back to formId when
relatedEvent is absent or the literal "null"); joinRequestUpdates returns
pendingCount; the removed-member email is sent, following
emailTemplates/removedMember.html. Every success message matches the original,
since the components put response.data.message straight into a toast.
Verified against the live database, read paths only: teamDetails as leader and
as member return identical full payloads, the teamless branch returns
isTeamless with the event's limits, searchTeams returns data.teams with every
field the picker needs and ?search= filters correctly, inviteLink and
joinRequestUpdates match.
The mutations are deliberately not exercised: this database holds real
registrations and running them would rewrite other people's teams. They are
matched to the controllers line by line and typecheck clean, but want a
run-through on a scratch database before release.
typecheck clean, eslint 0 errors, build 68 routes, audit:contracts clean for
every team endpoint.
TeamManagement.jsx carried React Router's [params, setParams] destructuring.
Next returns the params object itself, so opening any team page failed with
"Cannot read properties of undefined (reading 'get')".
ReadonlyURLSearchParams is iterable, so destructuring it as an array does not
throw — it quietly yields the first [key, value] entry, or undefined when the
URL has no query string. The next .get() is what fails.
TeamManagement also cleaned the URL after an email-redirect toast by mutating
the params and calling the setter. Next's object is read-only with no setter,
so that is now a copy plus router.replace(..., { scroll: false }) — the toast
still does not re-fire on refresh and no history entry is added.
EventForm.jsx and VerifyCertificate.jsx had the same bug from the base
migration; they are fixed on auth-redirect-fix since neither is team work.
Verified in the browser against live data: the team page renders in full for a
non-leader member (team, code, 2/3 members with year and college, "You" marker,
Leave Team) and for the leader (invite panel, share link, per-member remove);
?toast=joined&name=... is consumed and stripped from the URL; console clean on
both.
inviteLink is built from the request rather than hardcoded, so production traffic already produced production URLs — the localhost in a dev screenshot is dev's own origin. But the origin was taken verbatim, and these URLs go into email: the team invitation, and the accept/reject buttons sent to a team leader. Origin and Host are set by the caller. Reflected unchecked, anyone able to create a team could have FED KIIT send a message, from its own address, containing a link to a domain they chose. Measured before the change, a request carrying `Origin: https://evil.example` came back with `https://evil.example/Events/.../Form?teamCode=...`. An origin is now trusted when it matches NEXT_PUBLIC_SITE_URL, or is localhost so a developer's copied link still works locally; anything else falls back to NEXT_PUBLIC_SITE_URL. After the change: Origin: https://www.fedkiit.com -> https://www.fedkiit.com/Events/... Origin: http://localhost:3999 -> http://localhost:3999/Events/... Origin: https://evil.example -> falls back to the real host Host: evil.example -> https://www.fedkiit.com/Events/... The Express controller reflected these headers (req.headers.origin || process.env.FRONTEND_URL || "https://fedkiit.com"), so this is a deliberate deviation, noted in MIGRATION.md. typecheck clean, eslint 0 errors, build 68 routes.
Clicking an invite while signed out sent you to the login page and then dropped
you on the team-finding page instead of joining the team.
The auto-join was never the problem — it was ported and works. The destination
was thrown away during authentication, in four places:
Login.jsx "Sign Up" link overwrote prevPage with /Login, discarding the
invite ProtectedRoute had just saved
SignUP.jsx router.push("/") after signing up
GoogleSignup.jsx cleared prevPage, then resolved to /profile
CompleteProfile.jsx router.push("/")
So the very case an invite exists for — someone without an account — lost it
before signing up. All four now resolve through postAuthRedirect(), which takes
a fallback argument so the signup screens still default to "/" as before.
Measured through the real UI with the login and join calls stubbed, so nothing
was written to the database:
open invite signed out -> /Login, prevPage = /Events/<id>/Form?teamCode=...
click "Sign Up" -> prevPage unchanged (previously became "/Login")
sign in -> POST /api/auth/login
POST /api/form/joinTeam {formId, teamCode}
-> /Events/<id>/team
Email and WhatsApp links carry the same ?teamCode=, so both behave identically.
A brand-new account still fills in the event's registration form first: team
membership is a formRegistration row, so there is nothing to move onto a team
until the person has registered, and that form is where required details and
payment are collected. The invite is carried through it — EventForm passes the
code to PreviewForm, which joins the team as soon as registration succeeds.
typecheck clean, eslint 0 errors, build 68 routes.
PreviewForm joins the invited team the moment registration succeeds. When that
join was rejected it only logged to the console and fell through to
router.push("/Events") — the person ended up on the events listing, registered
but teamless, with nothing on screen explaining that the invite had not been
honoured.
Two paths were silent, not one. Besides the catch, a 200 carrying
success: false also fell through untouched. Both now redirect to
/Events/<formId>/team?toast=join_failed&reason=<api message>
The team page is the right destination: they are registered by then, just
unaffiliated, so TeamManagement renders TeamlessState — the team search. They
land on the screen that lets them recover instead of the events listing.
The reason is forwarded rather than hard-coded, because the same path catches
four different rejections from joinTeam: "This team is full", "Invalid team
code", "Registration is closed for this event" and "You are already in a team".
TeamManagement appends "You can join another team below." and strips reason from
the URL alongside toast and name, so the toast does not re-fire on refresh.
Also dropped a dead `const eventId` in the success branch — it was computed and
never read; the redirect uses form.id directly.
The same silent fall-through is in FED-Frontend PreviewForm.jsx:191-194, so the
behaviour is inherited, not a port defect.
Not exercised against the database: joinTeam is a mutation and the configured
Atlas instance holds real registrations. eslint 0 errors, build clean.
The mutations had only ever been matched to the controllers by eye. Running them end to end against the database with two real accounts found four defects. joinTeam failed outright with a 500. formRegistration carries @@unique([formId, teamCode]): the model is one row per TEAM, holding the whole roster in regTeamMemEmails, not one row per member. The port stamped the team's code onto the joiner's own row, colliding with the team row on that constraint, so every join died with a Prisma P2002. Joining is a merge — the joiner's email and value entry move onto the team row and their solo row is deleted. This is what the entire invite-link flow depends on, so that flow could not have worked. respondJoinRequest failed the same way for the same reason, so a leader accepting from their email got the generic error page and the request stayed PENDING for ever. It also now handles the requester having joined elsewhere in the meantime, and a team that filled up, as expiries rather than errors. renameTeam had no leader check at all — any member could rename the team. The controller looks the row up by membership and compares its owner to the caller; this loaded the caller's own row and compared nothing. Also missing: the registration-closed check, the "name unchanged" no-op, and the tracker's name swap, so a rename left the old name reserved for ever. Team codes were built from a slug of the team name (CLAUDETE-7130) instead of the controller's <2-letter event code>-<3-digit index>-<4 digits> (AR-003-8793). Every code already in the database uses the latter and people share them by hand, so the wrong shape makes a valid code look fake. Two messages were wrong with them: createTeam used a commented-out v1 string from addRegistration.js rather than the live one, and renameTeam invented its own. Both now match the controllers. One deliberate deviation kept: the controller lets a teamless registrant rename their own UNAFFILIATED placeholder, producing a named "team" with a SOLO- code and absent from the tracker. Guarded here. Verified on the "Team Test" event with two accounts belonging to the repo owner, restored to their original state afterwards; no team containing anyone else was touched. 20/20 asserted steps pass across createTeam, duplicate name, searchTeams, joinTeam (valid/invalid/already-joined), renameTeam (leader/non-leader/unchanged), inviteTeamMember, removeTeamMember, leaveTeam (member and leader-last), sendJoinRequest and respondJoinRequest (accept and replay). Both accounts end TEAMLESS with no leftover PENDING requests. typecheck clean, eslint 0 errors, build 68 routes.
A pending join request survived the dissolution of the team it pointed at, and came back to life the moment that leader created their next team. A leader could disband "ABC", create "BCD", and the person who asked to join ABC would be pulled into BCD without ever agreeing to it. teamRegistrationId cannot tell the two apart: the registration row is reused, so its id survives a rename and a disband alike. Confirmed against the database rather than assumed — after a disband and a fresh createTeam, the request's teamRegistrationId still equals the new team's row id. teamCode is the discriminator. A rename leaves it untouched; disbanding resets it to a SOLO- code and the next createTeam mints a fresh one. teamJoinRequest now pins the code at the time of asking, and acceptance compares it: renames ABC -> BCD code unchanged request accepted, link works disbands ABC, creates BCD code new request auto-expired, link 404 The invite-link path already had the right semantics, since those links carry ?teamCode=, but it was verified rather than taken on trust. The field is optional: rows written before it existed carry no code, and a request that cannot be verified is treated as stale. There were 45 requests in the database and none pending when this shipped, so nothing was grandfathered. MongoDB needs no migration for an optional scalar with no index — prisma generate is enough, no db push against production. Verified on the live database, 9/9 assertions across all three cases, plus the 14-step lifecycle re-run to confirm nothing regressed. Both test accounts end TEAMLESS with no leftover PENDING requests. typecheck clean, eslint 0 errors, build 68 routes.
upstream/main carries PR fed-tech#3 as a squash commit (8a4b896 / f5e6387), which has no ancestry link to the 596c5f6 it was squashed from. Git therefore saw the same sign-in redirect work as two unrelated changes and conflicted on three files. Resolutions, all of them "ours supersedes upstream" rather than a merge of two live variants: src/utils/postAuthRedirect.js add/add. Ours is a superset: it takes a `fallback` argument, which the signup screens pass as "/". Upstream's is the earlier hardcoded /profile form, and keeping it would send new accounts to the wrong page. src/authentication/SignUp/ Upstream still calls GoogleSignup.jsx sessionStorage.removeItem("prevPage") before redirecting. That is the exact line that discarded a team invite when someone signed up with Google, removed in 98848cb. MIGRATION.md Both hunks are additions on our side with nothing opposite them; markers stripped so upstream's non-conflicting edits elsewhere in the file are kept, which --ours would have discarded. Checked that nothing upstream-only was lost: the only lines dropped are the four superseded ones above. app/(auth)/layout.jsx is byte-identical to upstream. typecheck clean, eslint 0 errors, build 68 routes.
beta and main each carry their own squash of PR fed-tech#3 — 8a4b896 on beta, f5e6387 on main — as separate commits with the same content and no ancestry link to the 596c5f6 they came from. So merging beta conflicted on the same three files that merging main did, and resolved the same way: src/utils/postAuthRedirect.js ours keeps the `fallback` argument the signup screens pass as "/" src/authentication/SignUp/ ours keeps the removal of GoogleSignup.jsx sessionStorage.removeItem("prevPage"), the line that discarded a team invite during Google sign-up MIGRATION.md additions on our side only; markers stripped rather than taking --ours Confirmed again that the only upstream lines dropped are those four superseded ones. The branch now descends from both upstream/beta and upstream/main, so a PR opened against either target merges without conflicts. beta is the one that matters here: work lands there first and reaches main from beta. typecheck clean, build 68 routes.
The generated client lives in node_modules/.prisma/client, which is gitignored, so it never travels with a commit. @prisma/client has its own postinstall hook, but npm only fires that during an install — and a schema change on its own does not touch package.json, so nobody has a reason to reinstall. That combination is the trap: pulling a branch that changed schema.prisma leaves a client that no longer matches it, the app starts perfectly, and the mismatch only surfaces as "Unknown argument" on whichever request happens to touch the new field. npm run build and npm run typecheck do fail loudly, but npm run dev does not type-check up front, so the usual workflow hides it. The teamCode field added for the rename-vs-disband fix is the first change with this hazard. scripts/ensure-prisma.mjs runs the check where it cannot be skipped: with-env.mjs calls it, and dev, build and start all go through that. It regenerates only when the schema actually changed, comparing a SHA-256 of schema.prisma against a stamp written beside the generated client. Generating unconditionally would add seconds to every dev start; hashing one file is imperceptible, so the common case is free. The stamp sits inside the client directory deliberately — deleting node_modules takes it along, so a wiped install cannot look up to date — and is written only after a successful generate, so a failure retries instead of marking a client that was never produced as current. postinstall runs the same script with --optional, which tolerates a missing CLI: prisma is a devDependency, so npm install --omit=dev on a deploy host legitimately has none and failing there would break the install. A dev server without one is not legitimate, so that path leaves the flag off and fails. npm run prisma:generate is the manual escape hatch. package-lock records hasInstallScript for the new hook, and npm re-resolved the Prisma engines from dev to devOptional. Verified all three paths: no stamp generates; unchanged schema is silent with no generate spawned; a changed schema is detected and regenerated, both standalone and through a real npm run dev, which then served /api/health 200. npm install still exits 0. eslint clean, typecheck clean, build 68 routes.
…l incoming changes
The merged PR read `response.data.data.attendanceId` from
/api/form/attendanceCode, but that endpoint returns
`{ message, attendanceToken }` at the top level — the same shape the
Express controller returns (markAttendance.js:99). `response.data.data`
is undefined, so reading `.attendanceId` off it threw a TypeError and QR
generation failed outright.
Its duplicate-scan branch was unreachable for the same reason: a rescan
comes back as 400 "Attendance already marked." (markAttendance.js:154),
never a 200 carrying `data.alreadyMarked`. That intent is kept, moved to
the catch block and keyed off the response the API actually sends, so a
second scan reads as information rather than a red error.
The double-scan guard from the PR is untouched — it fixed a real bug.
Also stop reporting a lapsed volunteer session as a bad QR: both are 401,
so the API's own wording is now shown.
/profile/attendance was reachable by any signed-in participant. The sidebar only ever showed the link to admins, but that is presentation: proxy.ts guards /profile by checking for a valid session, not a role, and the route entry had no check of its own. Confirmed against the live app — both test accounts (access=USER) got a working scanner by typing the path. Since a participant can also mint their own QR through /api/form/attendanceCode, which exists precisely so they can display it, that combination let anyone mark themselves present. The gate that matters is on the API: markAttendance now returns 403 to non-admins, so the page is a UI over a call that refuses. Issuing a code stays open to any signed-in user; only redeeming one is restricted. This diverges from the Express route, which has its checkAccess commented out entirely and accepts unauthenticated calls. Note: the sidebar's attendance@fedkiit.com special case is dead code — no such account exists.
Taken selectively rather than wholesale. The source branch carries merges
made with "accepting all incoming changes", which had reverted the
migration work it was branched from — a plain merge applied cleanly and
silently rolled back 79 backend files (+743/-1368), including the team
mutation fixes, the Google sign-in fix and the env schema.
Merged with -s ours and then brought his work in deliberately, so nothing
of ours could be lost by omission.
Taken:
- Home revamp: Home.jsx, Hero, About, Feedback, Sponser, Contact and a
new EventsSection, plus HeroGallery, CustomCursor, CloseButton and
the Event Artwork component
- The revamped navbar (app/components/Navbar.tsx) and its design
tokens (app/globals.css)
- SCSS restyling across ~100 existing stylesheets
- New image assets, and dark-theme overrides for the date pickers
Not taken:
- Every backend file. All 76 of his modifications there were reverts.
- app/events, app/team, app/login, app/insights and app/data — static
mock-ups reading hardcoded data, unreachable behind the existing
proxy redirects. Wiring them up would have replaced the working
Events page with a prototype.
- app/sections/*.tsx and app/components/Footer.tsx, which nothing
imports even on his own branch.
- His package.json, which dropped the audit and prisma scripts and
added "saas" — a typo for the already-present "sass"; the real
package has no repository and has not been published since 2015.
- 207 files of editor/agent configuration and committed temp
artifacts (.tmp-shots, skills-lock.json).
Adjusted on the way in:
- Root layout keeps the Chatbot. His moved it into the main layout,
which hides it on Login, SignUp and OTP — the reason it was moved out
in the first place — and replaced the metadata title template with
hardcoded strings.
- The navbar's mobile backdrop was the one element written in Tailwind
utilities. Translated to plain CSS so no second styling system is
needed; nothing else in the merged set uses one.
- The navbar reset its state inside an effect, which the lint config
rejects as an error. Moved to the adjust-during-render pattern.
- The mobile blog menu no longer offers Attendance to
SENIOR_EXECUTIVE_CREATIVE, whom the new ADMIN gate would bounce.
His navbar links to /Events, /Team and /Blog, so it drives the existing
pages rather than the mock-ups.
PR fed-tech#12 was squash-merged into upstream main, so beta's lineage was severed from it again and beta no longer merged cleanly. Nine files conflicted; all resolved in favour of this branch, which already contains everything beta has: - markAttendance: keeps the ADMIN gate added here. - audit-nesting.mjs: keeps the CodeQL ReDoS fix, which came from beta and is already merged. - The Home revamp and its stylesheets: beta predates them. Beta did contribute two things worth keeping. It restores the global `button {}` rule in globals.scss, which the incoming revamp had dropped and which exists so buttons outside /Team and /profile/members do not fall back to the user-agent appearance. The duplicated vendor @import lines the merge produced are collapsed back to one each. Deliberately not taken: beta's GoogleSignup.jsx, which lost the invite-carry fix when PR fed-tech#8 was resolved into it. That regression is the reason this branch keeps its own copy.
Taking the revamp's stylesheets while keeping our own components broke the pairing between them: a CSS Module's exported class names are part of its component's contract, and the revamp renamed them. On /Team that left 448 elements rendering with class="undefined" — the page loaded but was entirely unstyled. Reverted the 63 stylesheets whose component we kept. What remains from the revamp is the 10 stylesheets that ship alongside their own components: the Home sections (Hero, About, Feedback, Sponser, Contact, EventsSection) and the new HeroGallery, CustomCursor and CloseButton. /Team now renders 482 styled cards. All ten main routes return 200 with no unresolved module classes. Pre-existing and left alone: a handful of unresolved classes on /Login, /SignUp and in TeamCard. Those files are untouched by this merge and behave the same before it.
The site rendered `class="undefined"` on a large number of elements. Two
separate causes, both inherited from the original app:
1. `styles.X` where the module never defined `.X`. 62 such references
across 33 files. Scanning the original Vite app found the same set —
these are dead references to rules deleted at some point, so the
elements were already unstyled and removing the reference changes
nothing visually.
2. A `className` prop interpolated into a template literal with no
default. `Core/Input` and `Core/Text` both did this, so every caller
that omitted the prop contributed a literal "undefined" to the class
list. This is what produced the counts on /Login and /SignUp.
Handled the second by defaulting the props to "", and swept for the same
pattern everywhere else — no other component has it.
Three of the first group were real defects rather than dead weight:
- PreviewForm added `styles.noScroll` to <body> to lock scrolling while
the modal is open. No such rule existed, so the page behind the modal
stayed scrollable. Defined it, matching the declaration the original
uses for this class in its other modules.
- Footer set `id={styles.footer}` with no `#footer` rule, rendering
id="undefined" — invalid as soon as any other such element was on the
page. Dropped.
- EventsSection referenced `.loadingGrid` for its skeleton row, which
was never defined, so the placeholders stacked instead of sitting in
the events grid. Added alongside `.eventsGrid` so the layout does not
shift when real cards replace them.
Also removed two files the revamp brought in that nothing imports
(Event/components/Artwork and Disclosure) and the superseded
src/layouts/Navbar.
All ten main routes now render with no unresolved classes, server-side and
after hydration. /Team goes from 448 such elements to none.
GitGuardian flagged a Generic High Entropy Secret in EventsSection.jsx. It was the `apiKey` query parameter on a cdn.builder.io image URL used as the fallback banner when an event carries no image of its own — a key belonging to a third-party Builder.io account, not ours, almost certainly carried over from a design-tool export. Low severity in itself: Builder.io public keys are meant to be visible in image URLs, and the asset is read-only. But it is a third-party credential hardcoded in our source, it fails the secret scan on every PR, and it makes the home page depend on an outside host that is not even in `next.config.ts` remotePatterns. Replaced with a local asset behind a named constant. The same URL also appeared 10 times in src/data/FormData.json, the mock data several components fall back to, so those are replaced too — GitGuardian only saw the one in the PR diff. Verified no other occurrence of the key or host remains, and swept everything the revamp merge added for similar patterns: nothing else.
These were dropped by the earlier stylesheet correction, not by intent. When taking the revamp's stylesheets while keeping our own components turned out to break the class-name contract, the fix was to revert every stylesheet whose component we had kept — which caught Footer, the Events page and EventCard along with the rest, so only Home and the navbar ended up restyled. Taking these three as matched JSX+SCSS pairs instead. Checked first that nothing of ours is lost: - Footer.jsx and Event.jsx: no commit of ours has ever touched them. - EventCard.jsx: one, 9a1073e, which replaced a <p> that wrapped `div.price` because the invalid nesting broke SSR hydration. The incoming version does not have that nesting — confirmed with scripts/audit-nesting.mjs, which was written for exactly this bug and reports clean across all 160 files. Event.jsx imports Artwork and Disclosure, so those come back too. They were removed earlier as dead code, which they were under our Event.jsx; under this one they are used, and their classes resolve against the incoming Event.module.scss. Verified: /Events renders 634 cards from the live API with 33 register buttons, the footer marquee is present, and no route emits an unresolved class either server-side or after hydration. Backend still untouched.
Several screens quietly fell back to JSON fixtures when an API call
failed, so an outage did not show an error — it showed fabricated
records that looked real:
- Home events section and /Events/pastEvents fell back to
FormData.json, putting old test rows ("Test Payment 3", "QR TEST")
on the public site under an error banner.
- /Alumni listed sample people out of Team.json as though they were
real alumni.
- The admin member view fell back to Team.json for the roster.
- /Events counted a member's registrations against the sample file
rather than the events actually fetched.
All four now render an empty state and surface the error instead.
Two role lists were worse than stale fallbacks. ViewMember fetched
/api/user/fetchAccessTypes and then discarded the response in favour of
a hardcoded `testAccess` array, and AddMemberForm fell back to
Access.json. That file had drifted to 15 entries against the schema's
30 — missing every SENIOR_EXECUTIVE and DEPUTY_DIRECTOR role, and
offering DIRECTOR_SPONSORSHIP, OPERATION and SPONSORSHIP, none of which
exist. Both now use the enum the API returns.
Deleted FormData.json, Team.json, Access.json and user.json, along with
the dead imports of them in EventForm, EventModal, ViewEvent, Login,
GoogleLogin and GoogleSignup.
Kept: Sponser.json, Feedback.json, Carousel.json, SocialLink.json and
the live-event content files. Those are curated site copy, not stand-ins
for backend records.
Verified every route returns 200 and no page renders a sample record.
/Events shows 634 cards and 33 register buttons from the live API.
Event ranking. /Events and the attendance page already sorted by `eventPriority`, but the home page's featured three sorted on date alone, so the order set in the admin form had no effect in the one place it is most visible. It now ranks by priority first and falls back to date, matching /Events. Unranked events sort last rather than ahead of everything. Navbar. The scroll handler kept the previous offset in state and listed it as an effect dependency, so every scroll tick re-rendered the component and tore the listener down and re-attached it — several times per frame while scrolling. Moved to refs, throttled to one update per animation frame, and the listener is attached once. The show/hide threshold now only advances the reference point once it is crossed, which stops a slow drag from flickering between states. About section pin. Replaced the JavaScript pin with native `position: sticky`. It had been switching the element between `position: relative` and `fixed` mid-scroll and writing top/left/right every frame, which is what made it jump at both boundaries. The script now only computes progress. That change needed `overflow-x: hidden` to become `overflow-x: clip` on body/.page/main. `hidden` forces `overflow-y` to compute to `auto`, making each of them a scroll container, and a scroll container between a sticky element and the viewport stops it sticking — which is why the pin had been written in JavaScript in the first place. `clip` suppresses the same sideways overflow without that side effect. Verified no horizontal scrollbar appears and all ten routes still render. Team and Alumni headings. Both set `margin-top: 20px`, which predates the pill navbar and put the text behind it. They now use a shared `--fed-navbar-offset` token so the two values cannot drift from the navbar's own geometry. Measured: heading top moves 20px -> 120px against a navbar whose lower edge is at 82px.
Both problems came from the same place. `.page` used to carry `margin-top: 88px` in globals.scss to sit clear of the fixed navbar, but globals.css zeroes it with `margin-top: 0 !important` across body, .page and main. That is why /Team, /Alumni and /profile all rendered their first heading behind the navbar. Handled once in the layout instead of per page: everything gets a `page--nav-offset` class except Home and Omega, which open with a full-bleed hero that is meant to run up behind a transparent navbar. The two per-page margins added earlier were a symptom fix and are reverted, so there is a single source for the offset again. Measured against a navbar whose lower edge is at 82px — /Team heading 108px, /profile 123px, Home unchanged at 0 with no offset class. The navbar also had no idea whether anyone was signed in; it always rendered a Login pill. Restored what the previous navbar did: the avatar, linking to /profile, once a session is present, falling back to defaultImg when the account has no picture. The mobile menu gets the avatar with the member's name plus a Logout button, as before. The signed-out state is rendered while `isLoading` is still true, which is the case on the server and on the first client render, so hydration stays consistent and the control does not flash. Verified signed in: avatar shows the account's Google picture, no Login pill, and at 375px the mobile menu shows "Krishna Das" with Logout.
The offset added in the previous commit stacked on top of spacing the
Events page already had: `.page--nav-offset` contributed 88px and
`.shell` a further 110px, leaving a large empty band under the bar.
`.shell` no longer reserves navbar space at all — that is the layout's
job now — and keeps only breathing room, with `.group`'s own 32px margin
doing the visible spacing.
The offset itself was also short. It was set to 5.5rem from a reading of
82px taken before the avatar was added; the bar actually renders 80px
tall at `top: 1.25rem`, so its lower edge is at 100px and content was
sliding under it — /Events/pastEvents overlapped by 4px. Measured
properly and set to 8rem, with a comment recording where the number
comes from so it is not guessed again.
Gap between the navbar's lower edge and the first content, measured:
/Events 130px -> 60px
/Events/pastEvents -4px (overlapping) -> 36px
/Team 48px
/Alumni 48px
/Blog content starts at 128px; the space below it is the
page's own search bar, not a gap
/profile content starts at 128px; nothing renders behind the
navbar
Home and Omega still opt out, and their hero remains full-bleed.
Paid events could only be paid by UPI/QR. They can now use an external
payment page instead: the admin picks a mode, and Link mode collects the
URL, the button label and a message shown under the button. QR mode is
unchanged, and an event saved before this setting reads as QR.
Participants also upload a payment screenshot now. The register route was
reading only `_id` and `sections` and discarding every file part, while
the client serialised the File to `{}` — so an uploaded screenshot went
nowhere. Files are now uploaded and their URLs written into the stored
answer, which fixes it for any file field, not just this one.
Admins had no way to see any of it. EventStats was written and exported
but never mounted by a route, and EventCard has been shipping an
analytics button pointing at /profile/events/Analytics/<id> that 404'd.
That route exists now, gated to ADMIN on the server, and carries a
payment proofs panel.
Also:
- The terms and conditions field was nested inside the UTR field's
`validations` array, where nothing renders it, so participants were
never actually shown the no-refund acknowledgement. It is a sibling
field now, and a checkbox rather than a lone radio nobody could clear.
- PreviewForm's `open &&` guard was written without braces, so "open && ("
and ")" rendered as literal text on every registration form.
- Event posters are square and the card's media area was 16:9, so `cover`
cropped 44% off a poster and took the title and logos with it. The box
is 4:3 now and the poster is drawn slightly taller than it, trading a
12% stretch for a 16% crop off the bottom.
- FormImages was capped at 196.37x350.67. Cloudinary takes integer pixels,
so the transform never applied and posters were stored at full size —
one live banner is a 4320x4320 PNG of 3.9 MB. Capped at 1600.
- The favicon was still the Vercel triangle create-next-app ships, and
metadata pinned it, overriding the correct app/icon.png.
Both were fixed-position overlays rendered on top of the still-mounted event listing. A multi-step registration form with file uploads and a payment step is a destination, not a dialog, and the event detail is a shareable URL — neither wanted modal semantics. /Events/:id is now a detail page, and /Events/:id/Form a registration page with a back link. The listing is no longer rendered underneath either, so opening one no longer fetches and paints every event just to cover it. PreviewForm gains an `inline` prop. Only the three wrapper elements differ between the two modes — the form itself stays a single copy, so a change to a step cannot land in one mode and not the other. The reference implementation duplicated the whole tree instead. The scroll lock is now conditional on that prop. It exists so the page behind an overlay does not scroll while the overlay owns its own scroller; inline the form *is* the page, and locking the body left a form taller than the viewport unable to reach its own Submit button. The detail page keeps EventModal's behaviour unchanged — the countdown, the button state machine and the related-event lock are the same rules, so anything registerable before is registerable now. Only the shell and styling differ, and the styling uses the tokens in globals.scss rather than the old modal's palette. Two deliberate changes: it no longer waits three seconds before navigating to the form, which on a page reads as an unresponsive button, and the share URL is read in an effect rather than during render, where `window` does not exist. EventModal is left in the tree, still exported, but no longer routed.
The stats panel reused `.card` from EventModal.module.scss — a fixed 32rem wide with no height constraint. It carries far more than an event card does (counts, the year breakdown, the registrant list, and now a grid of payment proofs), so it simply grew past the bottom of the viewport with no way to reach what fell off. The inline `overflow-y: auto` on it could never engage: an element with no height bound never overflows. It now has its own stylesheet — the file existed but was empty — with a max-height of min(86vh, 900px) and its own scroller, and is widened to min(46rem, 94vw) so the payment grid is not squeezed into 32rem. The backdrop scrolls as a fallback below roughly 420px of viewport height, where the panel's own minimum would otherwise put its top out of reach, and `overscroll-behavior: contain` stops a flick at the end of the list from scrolling the page behind it. Verified with 2000px of injected content: the panel holds at 619px against a 720px viewport, scrolls internally, and leaves the page behind it unmoved. Also drops the skeleton block that rendered unconditionally next to the panel. It was not gated on `isLoading` — the real loading state is handled inside the panel — so it was permanently visible filler.
…nce export Three layout faults, all visible with a couple of registrants: - The overlay was `z-index: 10` while the navbar wrapper is 999, so the navbar floated over the modal and clipped its heading. - The registrant list was `height: 300px` rather than a maximum, and left `align-items` at its `stretch` default — so two entries reserved the full height and each card grew to fill all 300px of it. Cards now hug their content and the box shrinks to fit. - The close button used `.closeModal`, which is `top: -8%` and therefore positioned outside its own panel; it landed up beside the navbar. Those classes were shared from EventModal.module.scss but only ever used here, so they move into this component's own stylesheet rather than being overridden across module boundaries. The attendance export now carries `utr` and `paymentScreenshot`. Payment proof lives on the registration, not the attendance record, so it has to be joined in — without it the attendance sheet gave a volunteer at the desk no way to check a payment against what was actually uploaded. The extraction is now one shared helper used by both the export and the payments endpoint, instead of two copies that could drift. Also corrects both download filenames from .xlsx to .csv. Neither route has ever produced a workbook — they stream CSV, and Excel warns when the extension disagrees with the contents.
The crop/stretch treatment was applied to every event card. It only earns its keep on the spotlight, where the poster is large enough for the title, the logos and the subject to matter. On a 359px grid thumbnail it bought nothing and cost a 12% distortion on every image on the page. The grid goes back to what it was: a 16:9 box and a plain centred `cover`. Only `.featured` keeps the 4:3-ish box, the `fill` and the 1.19 stretch. The skeleton ratios follow the cards they stand in for, so nothing shifts on load. Measured: featured 573x429, 16% cropped, 12.3% stretched; grid 359x202 at `cover`, element exactly its box, no stretch.
The spotlight card was a flat 430px tall sitting under a 128px navbar offset, which on a short laptop ran it off the bottom of the screen — the one card the page exists to show could not be seen without scrolling. It is now sized as a share of the viewport at every width, so it occupies roughly the same portion of the screen everywhere: 53% on a 375x812 phone, 50% on a 768x1024 tablet, 46% on 1440x900, fitting above the fold in each. The navbar offset drops from 8rem to 6.5rem. It was set from an 80px reading of the bar; the bar renders 62px tall and ends at 82px, so 8rem was reserving 46px of empty band above every page on the site. Mobile fixes, all measured at 375px rather than assumed: - Every field rendered at 12px. Safari on iOS zooms the viewport in when a focused field's text is under 16px and never zooms back out, so the page magnified itself partway through signing up and stayed that way. Fields are 16px below the tablet breakpoint, with a global floor to catch react-select's own injected inputs. - `.inputTxtArea` and `.inputSelect` were a flat `width: 380px`, wider than the phone they were being viewed on. Fluid now. - The registration form's field row is a wrapping flexbox with a 3rem column gap; on a phone each field became a shrink-to-content item about 200px wide inside a 343px card. One field per row now, full width — the text input goes from 203px to 329px. - Buttons rendered 34px tall, under the comfortable touch minimum on the control every registration has to end with. 44px now. Checked for horizontal overflow on /, /Events, /Events/:id, the registration page, /Team, /Login and /SignUp: none of them scroll sideways. The marquee and glow elements report widths past the viewport but are clipped by `overflow-x: clip` on body and never scroll.
A session has two halves: the httpOnly `token` cookie proxy.ts gates routes on, and the localStorage copy AuthContext reads. They expired at different times -- 7 hours for the cookie, 2h40m for localStorage (and 3h, and 2h, depending on which of the five login call sites you came through). In the gap between the two, the app deadlocked. proxy.ts saw a live cookie and redirected /Login to /profile; the profile guard saw no localStorage and redirected back to /Login. Clicking Login did nothing at all, for the whole remainder of the cookie's life. The trigger was closing the tab over the localStorage expiry, so the logout timer never ran to clean up the cookie. The mismatched numbers came from the Vite frontend, where they were harmless -- that app gated nothing on the server, so /Login always rendered. Adding proxy.ts in the migration is what turned the mismatch into a trap. - SESSION_TTL_MS, exported and used at every login call site, matching the JWT. - clearServerSession(), so the client can drop a cookie it cannot see. Called when a stored session is found expired on mount. - Both route guards clear the cookie before redirecting to /Login, which breaks the loop for any residual disagreement. /profile needed this separately -- it has its own inline guard rather than using ProtectedRoute.
PR fed-tech#15 was squash-merged upstream, so none of the commits on this branch appear there by SHA even though all of their content does. Merging normally therefore reported conflicts on every file both sides had touched, all of them spurious. Resolved by taking upstream's tree wholesale: upstream/main is a strict superset of this branch -- it carries the squashed migration work plus the navbar, About, Sidebar and payment-UI passes that landed after it. Verified before committing that the merge result is byte-identical to upstream/main.
…e weight Seven defects reported from production, and one found while verifying them. Event links (every past event 404'd) EventCard built its detail link as `modalpath + id`, and three of the four callers passed a path with no route: "/pastEvents/", "/Events/pastEvents/" and "/profile/Events/". `modalpath` predates the modal-to-page conversion, when it named a modal and was never navigated to. The card now builds `/Events/<id>` itself and the prop is gone, so a caller cannot reintroduce this. The same "/profile/Events" string was hardcoded in the profile events table's View button. Related-event lock (gated events were open to everyone) Both event views computed one page-wide `isRegisteredInRelatedEvents`: "is the visitor registered for *any* event that gates *any* other", then applied it to every card. Registering for one prerequisite unlocked every gated event on the site. It also only ever set the flag true, never back to false. Now evaluated per event in src/utils/prerequisite.js, matching the test the register route already applies server-side. Payment step vanished when a paid event was edited `constructForPreview` drops the stored "Payment Details" section and re-adds `paymentSection` in its place. Nothing hydrated `paymentSection` when loading an event for editing, so it stayed null and the step was dropped and never rebuilt -- saving an edit removed Pay Now, the UTR field and the screenshot upload from the live form. The stored section's id is reused so earlier sections' `onNext` pointers still resolve. Certificates were invisible to participants /api/certificate/sendCertificatesAndEvents was ported as an admin-only certificate *issuing* endpoint. In Express it is a lookup: given an email, return each issued certificate with its event. Its only caller is the participant profile, which got a 403, so the list stayed empty and View led nowhere. Restored to the Express contract, authorised as "your own certificates, or anyone's if you are an admin" -- the Express route had no auth at all. The client-side lookup also called two routes that were never ported; the joined event now comes back with the certificate instead. Analytics unreachable on mobile The admin bar was gated on an `isHovered` state. No touch device sets it, so admins on a phone could not reach Edit, Delete or Analytics. The reveal now lives in CSS behind `@media (hover: hover)`. Login card cramped on laptops `.authpage` centres with `align-items: center`, which sizes its child shrink-to-fit. Login nested two unclassed wrappers inside it, so the card's `width: 100%` resolved against a collapsed 257px instead of reaching its 480px max-width. Mobile performance The home page shipped 22.09 MB of images across 22 requests, ~521 MB once decoded -- including an 8.2 MB 6000x4000 JPEG drawn in a 208px box, fetched twice. No Cloudinary URL carried a transformation. `IMAGE_SIZES` caps uploads, so it does nothing for images already stored; this caps delivery instead, via f_auto/q_auto/c_limit for Cloudinary and /_next/image for the other hosts. Measured after: 0.66 MB and ~42 MB decoded, nothing above 1200px, no broken images. Also: `parentEventCount` was initialised to [] and compared against 0.
Measured at 375px while verifying that Analytics is reachable without hover: the Edit/Delete/Analytics buttons rendered 32px tall, under the 44px minimum used elsewhere in the mobile pass. They are now the only way an admin reaches Analytics on a phone, so they are worth sizing properly.
Registering for an event, then logging out, left "Already Registered" on the
card and on the event page. The session was genuinely gone -- token cleared,
cookie cleared, navbar back to Login -- but the button still claimed the
previous user's state, and clicking it did nothing useful.
`btnTxt` had two effects writing to it. One handled the impersonal states
(Closed / countdown / Register Now), the other the personalised ones (Already
Registered / Locked). The personalised effect opened with
if (!authCtx.isLoggedIn || !authCtx.user.regForm) return;
so logging out re-ran it -- `isLoggedIn` is a dependency -- and it wrote
nothing, leaving the stale label in place. The other effect only re-runs when
the countdown or the closed flag changes, and neither of those changes on
logout. For any event whose registration window has already opened the
countdown is null and never ticks, so nothing ever corrected it.
Both components now derive the whole label in one effect, so every input --
including signing out -- produces a complete answer rather than an early
return. Verified by reproducing the stale label on the previous code and
confirming it clears on the new one, on both the listing and the detail page.
Clicking a locked event told the visitor "You need to register for Form test first" when the event in fact required Omega4.0. The card was handed a page-level `eventName`: the title of whichever event happened to be listed first with no prerequisite of its own, which has nothing to do with the card being clicked. Sending someone to register for the wrong event is worse than saying nothing. Each card now resolves its own `info.relatedEvent` id to that event's title, and the page banner names a prerequisite the gated events actually point at rather than an arbitrary ungated one. The lock itself was already correct on both surfaces; this is the wording.
|
@Krishna-Das20 is attempting to deploy a commit to the fedkiitgmailcom's projects Team on Vercel. A member of the Team first needs to authorize it. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Eight defects, all reproduced against the live site before fixing. 19 files, +386/-218, no new dependencies.
The commit count is inflated — the earlier migration work was squash-merged upstream, so the diff is the accurate picture of what changes here.
EventCardtook amodalpathprop each caller set differently; the admin view pointed at/profile/Events/<id>, which doesn't exist. The prop is gone and the card builds/Events/<id>itself.src/utils/prerequisite.jsmirrors the API check and the card showsLockedagain, naming the event you need.sendCertificatesAndEventshad been migrated as an admin issuing endpoint but is called as a lookup. Rewritten to the original contract, authorised as own-email-or-admin.align-items: centercollapsed the form's width.Each fix was reproduced on the old code first, then re-checked after.