diff --git a/CHANGELOG.md b/CHANGELOG.md index f34707f0..3f0c80e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,11 @@ written while it was being built. See [RELEASING.md](RELEASING.md). ### Added +- Participants can leave a hackathon from their own profile in it. They leave + their team and give up the projects they proposed, and their team keeps its + projects and submission. Somebody still on the waitlist can cancel their + registration from the hackathon's public page instead. + - Every page inside a hackathon starts with a breadcrumb: the hackathon's full name and the section you are in. It stays visible when the menu is folded away, and replaces the cut-off name at the top of the menu. diff --git a/components/frontend/src/lib/components/hackathon/JoinCta.svelte b/components/frontend/src/lib/components/hackathon/JoinCta.svelte index 947d500a..2ea7ef95 100644 --- a/components/frontend/src/lib/components/hackathon/JoinCta.svelte +++ b/components/frontend/src/lib/components/hackathon/JoinCta.svelte @@ -4,7 +4,9 @@ import { UserPlus } from 'lucide-svelte'; import ArrowRight from 'lucide-svelte/icons/arrow-right'; import { signIn } from '@auth/sveltekit/client'; + import { enhance } from '$app/forms'; import { resolve } from '$app/paths'; + import ConfirmDialog from '$lib/components/layout/ConfirmDialog.svelte'; import { isFinished } from '$lib/utils/hackathonStatus'; import MembershipBadge from './MembershipBadge.svelte'; @@ -42,8 +44,19 @@ // their hackathon does not stop being theirs when it ends. const canGoIn = $derived(standing === 'member' || standing === 'admin'); const show = $derived(canGoIn || !isFinished(status)); + + // Kept here rather than read off the page's `form`: this bar is the only + // thing on the page that posts, and threading `form` through + // PublicHackathonView would hand the organiser's preview a prop it never uses. + let confirmDialog: ReturnType; + let cancelError: string | null = $state(null); + // Once the load reruns the standing is `none` and the bar offers Register, + // which on its own reads as though nothing had happened. + let cancelled = $state(false); + + +
{ + const ok = await confirmDialog.ask({ + title: 'Cancel your registration?', + message: + "You come off the organizers' list and your answers are deleted. You can register again while registration is open.", + confirmLabel: 'Cancel registration', + cancelLabel: 'Keep registration', + danger: true, + }); + if (!ok) return cancel(); + cancelError = null; + return async ({ result, update }) => { + if (result.type === 'failure') { + cancelError = + typeof result.data?.message === 'string' + ? result.data.message + : 'Your registration could not be cancelled. Try again.'; + } + if (result.type === 'success') cancelled = true; + await update(); + }; + }} + > + +
+ {#if cancelError} + + {/if} {:else if signedIn}

- Registering puts you on the organizers' list. + {cancelled + ? 'Your registration is cancelled. Registering again puts you back on the list.' + : "Registering puts you on the organizers' list."}

+ + {#if form?.leaveError} + + {/if} + + {/if} + + diff --git a/components/frontend/src/routes/(public)/hackathon/[id]/+page.server.ts b/components/frontend/src/routes/(public)/hackathon/[id]/+page.server.ts index c6146cba..538322f2 100644 --- a/components/frontend/src/routes/(public)/hackathon/[id]/+page.server.ts +++ b/components/frontend/src/routes/(public)/hackathon/[id]/+page.server.ts @@ -1,5 +1,6 @@ -import { error } from "@sveltejs/kit" -import type { PageServerLoad } from "./$types" +import { error, fail } from "@sveltejs/kit" +import type { Actions, PageServerLoad } from "./$types" +import { leaveHackathon } from "$lib/server/hackathon/leave" import { createAuthorizedGrpc, publicHackathonClient, @@ -166,3 +167,33 @@ export const load: PageServerLoad = async (event) => { }, } } + +export const actions: Actions = { + /** + * Takes somebody off the waitlist at their own request. Offered only to the + * `waiting` standing — a confirmed participant leaves from their profile + * inside the hackathon, where the page can say what leaving costs them. + * + * Its own authorized client, for the reason `membership` builds one: the hook + * leaves `locals.grpc` unset on public routes. + * + * No redirect: the load reruns, the standing comes back `none`, and the bar + * offers Register again, which is the proof that it worked. + */ + cancel: async (event) => { + const session = (await event.locals.auth()) as CustomSession | null + if (!usableSession(session) || !session?.accessToken) { + return fail(401, { + message: "Your session has expired. Sign in again and try once more.", + }) + } + + const refused = await leaveHackathon( + createAuthorizedGrpc(session.accessToken).hackathon, + event.params.id, + ) + if (refused) return fail(refused.status, { message: refused.message }) + + return { cancelled: true } + }, +} diff --git a/docs/src/participant/index.md b/docs/src/participant/index.md index 381d5602..f51bcf59 100644 --- a/docs/src/participant/index.md +++ b/docs/src/participant/index.md @@ -6,6 +6,15 @@ project and a team, handing in work, and voting. What you see in a hackathon's menu depends on what its owners have switched on, so some of these pages appear only at certain stages. +## Registering + +- Click {guilabel}`Register` on the hackathon's public page. +- On a public hackathon you wait until an owner approves you. +- While you wait, click {guilabel}`Cancel registration` on the public page to + come off the list. Your answers are deleted. +- Once you are in, you leave from your own profile. See + [Leave the hackathon](member-participants.md#leave-the-hackathon). + ```{toctree} :maxdepth: 1 :caption: Pages diff --git a/docs/src/participant/member-participants.md b/docs/src/participant/member-participants.md index 603d1342..2ee7c304 100644 --- a/docs/src/participant/member-participants.md +++ b/docs/src/participant/member-participants.md @@ -8,6 +8,10 @@ Everyone taking part in the hackathon. It shows: - [A participant's profile](#a-participants-profile): links, shared answers and teams +It might also show: + +- [Leave the hackathon](#leave-the-hackathon): on your own profile + ::: ![The Participants page of a hackathon, as a participant sees it](../static/screenshots/member-participants.png) @@ -33,3 +37,13 @@ On your own profile: - Click {guilabel}`Edit answers` to change your registration answers. - Click {guilabel}`Add your GitHub, LinkedIn or Renku link` to go to your account page. +- Click {guilabel}`Leave hackathon` to stop taking part. See + [Leave the hackathon](#leave-the-hackathon). + +## Leave the hackathon + +- You leave your team and give up the projects you proposed. +- Your project preferences and registration answers are deleted. +- Your team keeps its projects and its submission. +- To come back, you have to register again. +- Not offered to owners, and not once the hackathon has finished.