From 76037844823eb3d6896c11bc9107eea524e3339e Mon Sep 17 00:00:00 2001 From: Hannah Casey <61227037+hanaCasey@users.noreply.github.com> Date: Thu, 13 Aug 2026 14:07:53 +0000 Subject: [PATCH] fix(frontend): make the participants "View" link reach a real page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every "View" button on the participants card view was dead. The card defaults profileDetailsHref to '#', and the participants page passed "#participant-{id}" — a same-page anchor to an element that exists nowhere on the page, so every click went nowhere. "View" is meant to open that participant's registration answers (affiliation, diet, skills) — what an organizer needs for catering and check-in. GetRegistrationResponse already serves this: it takes an optional user_id and requires hackathon Write to read someone else's, so the backend already gates it. The self-service /register/[id] route already renders exactly this data (the caller's own, editable), so the fix extends it rather than adding a route: - /register/[id] takes an optional ?userId=. When present it fetches that person's answers, renders them read-only (no submit, disabled consent boxes), and titles the page with their name (read off the members already in hackathon.get). A not-yet-submitted target shows "No response yet." rather than a blank form. Permission is left to the backend: PERMISSION_DENIED -> 403, NOT_FOUND -> 404, per the usual error-translation convention. - The participants page points "View" at /register/{id} for your own row (editable, no userId) and /register/{id}?userId={id} for others, and — matching how every organizer-only action here is hidden rather than left to fail — offers the cross-participant link only to managers, since a plain member would deterministically 403. - ParticipantCard omits the link entirely when no href is given. No backend or proto change: the RPC and its permission check already existed. --- .../hackathon/ParticipantCard.svelte | 10 ++++-- .../[id]/participants/+page.server.ts | 1 + .../hackathon/[id]/participants/+page.svelte | 13 ++++++- .../(app)/register/[id]/+page.server.ts | 35 ++++++++++++++++++- .../routes/(app)/register/[id]/+page.svelte | 33 +++++++++++++++-- 5 files changed, 85 insertions(+), 7 deletions(-) diff --git a/components/frontend/src/lib/components/hackathon/ParticipantCard.svelte b/components/frontend/src/lib/components/hackathon/ParticipantCard.svelte index d9bdd303..a704baa9 100644 --- a/components/frontend/src/lib/components/hackathon/ParticipantCard.svelte +++ b/components/frontend/src/lib/components/hackathon/ParticipantCard.svelte @@ -9,7 +9,7 @@ role: roleProp, skills = [], linkedinUrl, - profileDetailsHref = '#', + profileDetailsHref, actions, }: { name: string; @@ -26,6 +26,8 @@ role?: string; skills?: string[]; linkedinUrl?: string; + /** When unset, no "View" link is rendered — the caller has decided this + * viewer has nowhere to go (e.g. a member who can't read others). */ profileDetailsHref?: string; /** * Extra controls rendered beside "View" — e.g. an owner's Approve/Remove @@ -112,8 +114,10 @@
- - View + {#if profileDetailsHref} + + View + {/if} {#if actions} {@render actions()} {/if} diff --git a/components/frontend/src/routes/(app)/my/hackathon/[id]/participants/+page.server.ts b/components/frontend/src/routes/(app)/my/hackathon/[id]/participants/+page.server.ts index f9c5050f..cd8f2c30 100644 --- a/components/frontend/src/routes/(app)/my/hackathon/[id]/participants/+page.server.ts +++ b/components/frontend/src/routes/(app)/my/hackathon/[id]/participants/+page.server.ts @@ -33,6 +33,7 @@ export const load: PageServerLoad = async (event) => { return { participants, ownerCount, + hackathonId: event.params.id, // 0 means unlimited — the page renders the fullness gauge and the // over-capacity warning only when a cap is set. maxParticipants: hackathon.maxParticipants ?? 0, diff --git a/components/frontend/src/routes/(app)/my/hackathon/[id]/participants/+page.svelte b/components/frontend/src/routes/(app)/my/hackathon/[id]/participants/+page.svelte index 032cdc3d..7fa6023c 100644 --- a/components/frontend/src/routes/(app)/my/hackathon/[id]/participants/+page.svelte +++ b/components/frontend/src/routes/(app)/my/hackathon/[id]/participants/+page.svelte @@ -76,6 +76,17 @@ function mayDemote(p: Participant): boolean { return data.mayManage && p.isOwner && !p.isSelf && data.ownerCount > 1; } + + // "View" opens this person's registration answers. Your own row goes to + // your editable form; another person's needs organizer rights, so it is a + // read-only ?userId= view offered only to managers — a plain member would + // deterministically 403, matching how every organizer-only action here is + // hidden rather than left to fail. + function viewHref(p: Participant): string | undefined { + if (p.isSelf) return `/register/${data.hackathonId}`; + if (data.mayManage) return `/register/${data.hackathonId}?userId=${p.id}`; + return undefined; + } + {#if !data.alreadySubmitted} +

No response yet.

+ {:else} +
+ {#each data.fields as f (f.key)} +
+
{f.label}
+
+ {data.answers[f.key] || '—'} +
+
+ {/each} + {#each data.consents as c (c.key)} +
+ + {c.label} +
+ {/each} +
+ {/if} + {:else if form?.submitted}

Thanks — your answers are in.