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}
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)}
+
- {/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