feat: warn before leaving a form with unsaved changes

Drag-dropping page images then clicking a nav link silently discarded the
whole form. Add a reusable guardUnsavedChanges helper (SvelteKit beforeNavigate
confirm + native beforeunload) and wire it into the manga upload, manga edit,
and chapter upload forms. The dirty check returns false while submitting so the
post-save redirect isn't prompted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-11 15:16:40 +02:00
parent 3ca05dcb58
commit a47b6895c2
6 changed files with 145 additions and 1 deletions

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { ApiError, fileUrl } from '$lib/api/client';
import { guardUnsavedChanges } from '$lib/unsaved-guard';
import {
deleteMangaCover,
updateManga,
@@ -49,6 +50,25 @@
mangaTitle.trim().length > 0 && !coverError && !submitting
);
// Warn before leaving with unsaved edits. `submitting` stays true through
// the post-save redirect, so that navigation isn't prompted.
guardUnsavedChanges(
() =>
!submitting &&
(mangaTitle !== data.manga.title ||
mangaStatus !== data.manga.status ||
mangaDescription !== (data.manga.description ?? '') ||
JSON.stringify(mangaAuthors) !==
JSON.stringify(data.manga.authors.map((a) => a.name)) ||
JSON.stringify(mangaAltTitles) !== JSON.stringify([...data.manga.alt_titles]) ||
JSON.stringify(mangaGenreIds) !==
JSON.stringify(data.manga.genres.map((g) => g.id)) ||
coverFile != null ||
pendingCoverRemoval ||
authorDraft.trim() !== '' ||
altTitleDraft.trim() !== '')
);
function addAuthor() {
const name = authorDraft.trim();
if (!name) return;

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { ApiError, fileUrl } from '$lib/api/client';
import { guardUnsavedChanges } from '$lib/unsaved-guard';
import { createChapter } from '$lib/api/chapters';
import { session } from '$lib/session.svelte';
import ChapterPagesEditor, {
@@ -19,6 +20,16 @@
let submitting = $state(false);
let error: string | null = $state(null);
// Warn before leaving with staged pages / edits. `submitting` stays true
// through the post-save redirect, so that navigation isn't prompted.
guardUnsavedChanges(
() =>
!submitting &&
(pages.length > 0 ||
title.trim() !== '' ||
(number != null && number !== data.defaultNumber))
);
const allPagesValid = $derived(pages.every((p) => !p.error));
const canSubmit = $derived(
Boolean(session.user) &&

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { ApiError } from '$lib/api/client';
import { guardUnsavedChanges } from '$lib/unsaved-guard';
import { createManga, type MangaStatus } from '$lib/api/mangas';
import { createChapter } from '$lib/api/chapters';
import { session } from '$lib/session.svelte';
@@ -43,6 +44,23 @@
let mangaError = $state<string | null>(null);
let success = $state<string | null>(null);
// Warn before navigating away with an in-progress upload. Suppressed while
// submitting and after a successful save (which redirects).
guardUnsavedChanges(
() =>
!success &&
!submitting &&
(mangaTitle.trim() !== '' ||
mangaDescription.trim() !== '' ||
mangaAuthors.length > 0 ||
mangaAltTitles.length > 0 ||
mangaGenreIds.length > 0 ||
coverFile != null ||
stagedChapters.length > 0 ||
authorDraft.trim() !== '' ||
altTitleDraft.trim() !== '')
);
const allChapterPagesValid = $derived(
stagedChapters.every((c) => c.pages.every((p) => !p.error))
);