From 9102b25a96af4e6eed5761e3de9951f861c020e6 Mon Sep 17 00:00:00 2001 From: Sabine Maennel <5292683+sabinem@users.noreply.github.com> Date: Thu, 22 Oct 2026 08:23:07 +0200 Subject: [PATCH 1/2] feat(settings): turn the participant switches into a guide for organizers - Eight steps in the order a hackathon runs: register, projects, preferences, team assignments, hacking, voting setup, voting, results - Each switch sits at its step, labelled for the organizer ("Open registration", "Publish results") - Links to the pages in between: registration form, approving participants, invitation links (private only), Manage Projects, Manage Teams, Manage Voting - "Set team preferences" is now "Choose preferred projects" on the participant pages too - A switch no step names still shows, under "Other" --- CHANGELOG.md | 7 + .../hackathon/CapabilitiesPanel.svelte | 289 +++++++++++++++--- .../hackathon/CapabilitiesPanel.test.ts | 132 ++++++++ components/frontend/src/lib/utils/phase.ts | 10 +- .../my/hackathon/[id]/manage/+page.svelte | 2 + 5 files changed, 387 insertions(+), 53 deletions(-) create mode 100644 components/frontend/src/lib/components/hackathon/CapabilitiesPanel.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a4e04eb..e71a0487 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,6 +77,13 @@ written while it was being built. See [RELEASING.md](RELEASING.md). - For organizers, the "Participant View" section of a hackathon's sidebar is now folded every time you open the hackathon. Open it with its arrow when you need it. +- A hackathon's Settings page now walks organizers through the event step by + step: registration, projects, preferences, teams, hacking, voting and results. + Each switch sits at the step where it is turned on, with links to the pages in + between, such as approving participants, forming teams and setting up voting. + A private hackathon links to its invitation links. +- "Set team preferences" is now called "Choose preferred projects", since + participants pick projects, not teammates. - Manage Teams no longer has a "Suggest teams" button. Build teams by dragging people onto them, or by downloading the spreadsheet, filling in the team column and uploading it again. diff --git a/components/frontend/src/lib/components/hackathon/CapabilitiesPanel.svelte b/components/frontend/src/lib/components/hackathon/CapabilitiesPanel.svelte index 5b2f6612..db925a0f 100644 --- a/components/frontend/src/lib/components/hackathon/CapabilitiesPanel.svelte +++ b/components/frontend/src/lib/components/hackathon/CapabilitiesPanel.svelte @@ -1,12 +1,18 @@ +{#snippet toggle(capability: Capability, label: string, description?: string)} + + +{/snippet} +

- Each one is a permission held by every participant, for the whole - hackathon. Moving between phases never changes them. + These switches decide what participants can do, and this is the only + place to change them. Moving between phases never does.

- -
- {#each capabilities as capability (capability.value)} - -
+ + {#if unplaced.length > 0} +
  • +

    Other

    + {#each unplaced as capability (capability.value)} + {@render toggle( + capability, + capabilityLabel(capability.value) ?? 'Unknown', + capabilityDescription(capability.value), + )} + {/each} +
  • + {/if} + {#if message} @@ -115,7 +307,6 @@ Save changes - {:else}

    What participants can do diff --git a/components/frontend/src/lib/components/hackathon/CapabilitiesPanel.test.ts b/components/frontend/src/lib/components/hackathon/CapabilitiesPanel.test.ts new file mode 100644 index 00000000..90549e3c --- /dev/null +++ b/components/frontend/src/lib/components/hackathon/CapabilitiesPanel.test.ts @@ -0,0 +1,132 @@ +import { render, screen } from "@testing-library/svelte" +import { describe, expect, it } from "vitest" + +import CapabilitiesPanel from "./CapabilitiesPanel.svelte" + +/* + * The Settings switches as the steps of running a hackathon, with the screens + * between them. Raw enum numbers: REGISTER=1, PROPOSE_PROJECTS=2, + * SET_TEAM_PREFERENCES=3, CREATE_PROJECT_SUBMISSIONS=4, VOTE=5, + * VIEW_RESULTS=6, VIEW_TEAMS=7. + */ + +const all = [1, 2, 3, 7, 4, 5, 6].map((value) => ({ value, enabled: false })) + +function renderPanel(overrides = {}) { + return render(CapabilitiesPanel, { + hackathonId: "h1", + isPrivate: false, + hasState: true, + capabilities: all, + ...overrides, + }) +} + +const link = (name: string) => screen.queryByRole("link", { name }) +const box = (name: string) => screen.getByRole("checkbox", { name }) + +describe("CapabilitiesPanel", () => { + it("lists the steps in the order a hackathon runs", () => { + renderPanel() + + const titles = screen + .getAllByRole("heading", { level: 4 }) + .map((h) => h.textContent?.replace(/^\s*\d+\.\s*/, "").trim()) + expect(titles).toEqual([ + "Register", + "Projects", + "Project preferences", + "Team assignments", + "Hacking starts", + "Set up voting", + "Voting starts", + "Publish results", + ]) + expect(screen.getAllByRole("checkbox")).toHaveLength(7) + }) + + it("names each switch for the organizer, on the right capability", () => { + renderPanel() + + expect(box("Open registration")).toHaveAttribute("value", "1") + expect(box("Participants can propose projects")).toHaveAttribute( + "value", + "2", + ) + expect(box("Participants can voice preferences")).toHaveAttribute( + "value", + "3", + ) + expect(box("Participants can see teams")).toHaveAttribute("value", "7") + expect(box("Participants can submit for their team")).toHaveAttribute( + "value", + "4", + ) + expect(box("Participants can vote")).toHaveAttribute("value", "5") + expect(box("Publish results")).toHaveAttribute("value", "6") + }) + + it("links the steps that are screens, not switches", () => { + renderPanel() + + expect(link("Registration form →")).toHaveAttribute( + "href", + "/my/hackathon/h1/manage/forms/registration", + ) + expect(link("Manage Projects →")).toHaveAttribute( + "href", + "/my/hackathon/h1/projects/manage", + ) + expect(link("Manage Teams →")).toHaveAttribute( + "href", + "/my/hackathon/h1/teams/manage", + ) + expect(link("Set up voting →")).toHaveAttribute( + "href", + "/my/hackathon/h1/voting/manage", + ) + // Publishing is locked until the results are final, made so there. + expect(link("Manage Voting →")).toHaveAttribute( + "href", + "/my/hackathon/h1/voting/manage", + ) + }) + + // Every join waits for approval, public or private. + it("sends registration to approving people, either way", () => { + for (const isPrivate of [false, true]) { + const { unmount } = renderPanel({ isPrivate }) + + expect(link("Manage Participants →")).toHaveAttribute( + "href", + "/my/hackathon/h1/participants/manage", + ) + unmount() + } + }) + + it("says anyone can join a public hackathon, with no invitation link", () => { + renderPanel() + + expect(screen.getByText(/anyone can find it and join/)).toBeInTheDocument() + expect(link("Invitation links →")).toBeNull() + }) + + it("points a private hackathon's registration at its invitation links", () => { + renderPanel({ isPrivate: true }) + + expect(screen.getByText(/send invitation links/)).toBeInTheDocument() + expect(link("Invitation links →")).toHaveAttribute( + "href", + "/my/hackathon/h1/manage/invites", + ) + }) + + // A switch the backend adds before a step names it must still be settable. + it("keeps a capability no step names, under Other", () => { + renderPanel({ capabilities: [...all, { value: 99, enabled: false }] }) + + expect(screen.getByText("Other")).toBeInTheDocument() + expect(screen.getAllByRole("checkbox")).toHaveLength(8) + }) +}) diff --git a/components/frontend/src/lib/utils/phase.ts b/components/frontend/src/lib/utils/phase.ts index afcaca6d..2726df21 100644 --- a/components/frontend/src/lib/utils/phase.ts +++ b/components/frontend/src/lib/utils/phase.ts @@ -209,8 +209,10 @@ export function currentAndNextPhase< * the frontend no longer reads or writes it. See * `mydocs/docs/backend-tickets/phase-capabilities-unused.md`. * - * The description is what the label cannot carry: "Set team preferences" reads - * as a setting rather than as a permission handed to everyone in the hackathon. + * The description is what the label cannot carry: "Choose preferred projects" + * reads as a setting rather than as a permission handed to everyone in the + * hackathon. Its label used to be "Set team preferences", which organizers read + * as choosing teammates; the preference is for a project. * * **Pronoun-free throughout**, which is stronger than third person and is what * lets one string serve two surfaces. Capabilities grant to the `Member` role @@ -239,8 +241,8 @@ const CAPABILITIES: { }, { value: 3, - label: "Set team preferences", - description: "Mark preferred projects on the project list.", + label: "Choose preferred projects", + description: "Mark preferred projects on the project list, not teammates.", }, // Out of enum order, and deliberately: this is what the preferences above it // lead to and what the submissions below it are filed against, so it reads diff --git a/components/frontend/src/routes/(app)/my/hackathon/[id]/manage/+page.svelte b/components/frontend/src/routes/(app)/my/hackathon/[id]/manage/+page.svelte index f41d477d..271ddfab 100644 --- a/components/frontend/src/routes/(app)/my/hackathon/[id]/manage/+page.svelte +++ b/components/frontend/src/routes/(app)/my/hackathon/[id]/manage/+page.svelte @@ -238,6 +238,8 @@ Date: Thu, 22 Oct 2026 08:41:54 +0200 Subject: [PATCH 2/2] feat(settings): warn about unsaved switch changes, as on Manage Teams - A bar at the top lists what is unsaved, with Save and Discard, and sticks below the nav bar while scrolling - Changed switches are tinted until saved - Leaving through a link asks first; closing the tab, or pressing a phase button, gets the browser's own prompt - Saving stays on the page; a failed save keeps the ticks --- CHANGELOG.md | 3 + .../hackathon/CapabilitiesPanel.svelte | 184 +++++++++++++++--- .../hackathon/CapabilitiesPanel.test.ts | 71 ++++++- 3 files changed, 231 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e71a0487..26dd35f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,9 @@ written while it was being built. See [RELEASING.md](RELEASING.md). ### Added +- The Settings page shows when you have changed a switch without saving, as + Manage Teams does: a bar at the top lists what is unsaved, with Save and + Discard, and leaving the page asks first. Saving keeps you on the page. - Changing your registration answers is easier to find. On a hackathon's Participants page your own entry is now at the top, marked "You", and its button reads "View & edit". Your profile there has an "Edit answers" button diff --git a/components/frontend/src/lib/components/hackathon/CapabilitiesPanel.svelte b/components/frontend/src/lib/components/hackathon/CapabilitiesPanel.svelte index db925a0f..587230e7 100644 --- a/components/frontend/src/lib/components/hackathon/CapabilitiesPanel.svelte +++ b/components/frontend/src/lib/components/hackathon/CapabilitiesPanel.svelte @@ -1,5 +1,8 @@ + { + if (dirty && !leaving) e.preventDefault(); + }} +/> + + + {#snippet toggle(capability: Capability, label: string, description?: string)} + + {@const unsaved = draft[capability.value] !== capability.enabled}