From 084ee855127a3be21b0ca56458574b639c651325 Mon Sep 17 00:00:00 2001 From: Sabine Maennel <5292683+sabinem@users.noreply.github.com> Date: Fri, 9 Oct 2026 07:08:45 +0200 Subject: [PATCH 1/3] feat(frontend): keep unsaved team changes in view and ask before leaving Following an in-app link off Manage Teams dropped unsaved changes without a word, because only closing the tab was guarded. A beforeNavigate guard now asks first. A sticky bar with the change summary, Discard and Save appears while anything is unsaved, so Save stays in reach however far down the projects one scrolls, and it repeats a failed save's message. --- CHANGELOG.md | 3 + .../hackathon/[id]/teams/manage/+page.svelte | 63 ++++++++++++++----- 2 files changed, 49 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 066e6266..95edcd06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,9 @@ written while it was being built. See [RELEASING.md](RELEASING.md). ### Added +- Manage Teams makes unsaved changes hard to lose: a bar at the bottom of the + screen shows them with Save and Discard while you scroll, and following a link + away from the page asks first. - Manage Teams fixes the teams once a team has a submission or teams are published: a notice says why, and uploading, adding and deleting teams are disabled. People can still be moved between teams or to Unassigned by hand, diff --git a/components/frontend/src/routes/(app)/my/hackathon/[id]/teams/manage/+page.svelte b/components/frontend/src/routes/(app)/my/hackathon/[id]/teams/manage/+page.svelte index a48e1635..c03615df 100644 --- a/components/frontend/src/routes/(app)/my/hackathon/[id]/teams/manage/+page.svelte +++ b/components/frontend/src/routes/(app)/my/hackathon/[id]/teams/manage/+page.svelte @@ -1,5 +1,6 @@ +
{#if changes.total > 0} - Unsaved: {changes.summary} + Unsaved: {changes.summary} {:else} No unsaved changes {/if} - -
@@ -1314,4 +1312,35 @@ {/if} + + + {#if changes.total > 0} +
+ + {pending ? 'Saving…' : `Unsaved changes: ${changes.summary}`} + + {#if form?.message && !pending} + {form.message} + {/if} + + +
+ {/if} From 99a3555cd6031d7458009997314c316d0f382223 Mon Sep 17 00:00:00 2001 From: Sabine Maennel <5292683+sabinem@users.noreply.github.com> Date: Fri, 9 Oct 2026 07:29:49 +0200 Subject: [PATCH 2/3] feat(frontend): mark unsaved team changes and keep Save at the top MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The save bar moves to the top, sticks below the nav bar and is always there — quiet while everything is saved, warning-colored once anything is not — so Save is in one place. People whose team changed and teams that are new or renamed carry the same warning color until saved, so the page shows what the bar is counting. --- CHANGELOG.md | 5 +- .../hackathon/[id]/teams/manage/+page.svelte | 129 +++++++++++------- 2 files changed, 81 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95edcd06..1942eab4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,8 +18,9 @@ written while it was being built. See [RELEASING.md](RELEASING.md). ### Added -- Manage Teams makes unsaved changes hard to lose: a bar at the bottom of the - screen shows them with Save and Discard while you scroll, and following a link +- Manage Teams makes unsaved changes hard to lose: a bar at the top stays in + view while you scroll and shows what is unsaved with Save and Discard, the + people and teams that changed are marked until saved, and following a link away from the page asks first. - Manage Teams fixes the teams once a team has a submission or teams are published: a notice says why, and uploading, adding and deleting teams are diff --git a/components/frontend/src/routes/(app)/my/hackathon/[id]/teams/manage/+page.svelte b/components/frontend/src/routes/(app)/my/hackathon/[id]/teams/manage/+page.svelte index c03615df..494a7011 100644 --- a/components/frontend/src/routes/(app)/my/hackathon/[id]/teams/manage/+page.svelte +++ b/components/frontend/src/routes/(app)/my/hackathon/[id]/teams/manage/+page.svelte @@ -372,28 +372,38 @@ const kept = locked ? teams : teams.filter((t) => t.memberIds.length > 0); const alive = new Set(kept.map((t) => t.key)); - const added = kept.filter((t) => t.id === null).length; + const addedKeys = kept.filter((t) => t.id === null).map((t) => t.key); const removed = base.filter((t) => !alive.has(t.key)).length; - const renamed = kept.filter( - (t) => t.id !== null && nameBefore.get(t.key) !== t.name - ).length; + const renamedKeys = kept + .filter((t) => t.id !== null && nameBefore.get(t.key) !== t.name) + .map((t) => t.key); const teamOf = (list: WorkTeam[]) => new Map(list.flatMap((t) => t.memberIds.map((m) => [m, t.key] as const))); const before = teamOf(base); const after = teamOf(teams); - let moved = 0; - for (const id of new Set([...before.keys(), ...after.keys()])) { - if (before.get(id) !== after.get(id)) moved++; - } + const movedIds = [...new Set([...before.keys(), ...after.keys()])].filter( + (id) => before.get(id) !== after.get(id) + ); + const added = addedKeys.length; + const renamed = renamedKeys.length; + const moved = movedIds.length; const parts: string[] = []; if (added > 0) parts.push(`${added} new ${added === 1 ? 'team' : 'teams'}`); if (removed > 0) parts.push(`${removed} deleted`); if (renamed > 0) parts.push(`${renamed} renamed`); if (moved > 0) parts.push(`${moved} ${moved === 1 ? 'move' : 'moves'}`); - return { total: added + removed + renamed + moved, summary: parts.join(', ') }; + return { + total: added + removed + renamed + moved, + summary: parts.join(', '), + // What to mark on the page until it is saved: the people whose team + // changed, and the teams that are new or renamed. A deleted team is + // gone from the page and has nothing left to mark. + movedIds: new Set(movedIds), + changedTeamKeys: new Set([...addedKeys, ...renamedKeys]) + }; }); function startEdit(key: string, name: string) { @@ -611,14 +621,20 @@ )} {@const matches = projectId !== null && person.preferredProjectIds.includes(projectId)} {@const answers = answersFor(person)} + {@const unsaved = changes.movedIds.has(person.id)} +
startDrag(e, person.id, from)} ondragend={endDrag} - class="flex cursor-grab items-center gap-1.5 rounded-card border border-line bg-raised - px-2 py-1 active:cursor-grabbing" + class="flex cursor-grab items-center gap-1.5 rounded-card border px-2 py-1 + active:cursor-grabbing {unsaved + ? 'border-warning bg-warning/10' + : 'border-line bg-raised'}" class:opacity-40={draggedId === person.id} + title={unsaved ? 'Moved — not saved yet' : undefined} >
@@ -695,6 +711,50 @@

Manage Teams

+ +
+ + {#if pending} + Saving… + {:else if changes.total > 0} + Unsaved changes, marked on the page: {changes.summary} + {:else} + No unsaved changes + {/if} + + {#if form?.message && !pending} + {form.message} + {/if} + + +
+ {#if locked}
- {#if changes.total > 0} - Unsaved: {changes.summary} - {:else} - No unsaved changes - {/if} -
- {#if changes.total > 0} -
- - {pending ? 'Saving…' : `Unsaved changes: ${changes.summary}`} - - {#if form?.message && !pending} - {form.message} - {/if} - - -
- {/if} From 5e90a9e9f45d442ea4d01ce737b37e1d4cab5e09 Mon Sep 17 00:00:00 2001 From: Sabine Maennel <5292683+sabinem@users.noreply.github.com> Date: Fri, 9 Oct 2026 07:45:58 +0200 Subject: [PATCH 3/3] feat(frontend): ask Manage Teams' questions in the app's own dialog The native confirm() cannot be styled, introduces itself as "localhost:8081 says" and only offers "OK". A ConfirmDialog component, built on with showModal(), asks instead: themed, with Cancel focused and a confirm button that names its action. Manage Teams uses it for replacing teams on upload, deleting a saved team, and leaving with unsaved changes; the navigation guard cancels, asks, and resumes the navigation on a "leave". --- .../components/layout/ConfirmDialog.svelte | 73 +++++++++++++++++++ .../hackathon/[id]/teams/manage/+page.svelte | 62 +++++++++++++--- 2 files changed, 126 insertions(+), 9 deletions(-) create mode 100644 components/frontend/src/lib/components/layout/ConfirmDialog.svelte diff --git a/components/frontend/src/lib/components/layout/ConfirmDialog.svelte b/components/frontend/src/lib/components/layout/ConfirmDialog.svelte new file mode 100644 index 00000000..5138ca89 --- /dev/null +++ b/components/frontend/src/lib/components/layout/ConfirmDialog.svelte @@ -0,0 +1,73 @@ + + + + + answer(false)} + onclick={(e) => { + if (e.target === dialog) answer(false); + }} + class="m-auto w-[min(28rem,calc(100vw-2rem))] rounded-card border border-line bg-surface p-0 + text-ink shadow-lg backdrop:bg-scrim" +> + {#if current} +
+

{current.title}

+

{current.message}

+
+ + + +
+
+ {/if} +
diff --git a/components/frontend/src/routes/(app)/my/hackathon/[id]/teams/manage/+page.svelte b/components/frontend/src/routes/(app)/my/hackathon/[id]/teams/manage/+page.svelte index 494a7011..191a56e2 100644 --- a/components/frontend/src/routes/(app)/my/hackathon/[id]/teams/manage/+page.svelte +++ b/components/frontend/src/routes/(app)/my/hackathon/[id]/teams/manage/+page.svelte @@ -1,6 +1,6 @@ @@ -613,6 +655,8 @@ + + {#snippet personRow( person: Person, from: string,