Toast action results and confirm destructive actions in one dialog

Inline banners and per-row status text reported action results all over
the app; they are replaced by a small toast stack (bottom-right, Level 3
floating well styling) that only speaks for actions: successes fade,
errors stay until dismissed, and form-field validation stays inline.

Destructive actions no longer use bespoke inline confirm steps (the
library detail's Confirm delete button) or fire immediately (duplicate
copies/items, delete page selections, temp cleanup, upload discard, job
cancellation): they all go through one promise-based confirm dialog
(confirmAction) with a danger variant, Escape/backdrop to cancel.
This commit is contained in:
2026-09-17 22:19:54 -05:00
parent 1053e3ee55
commit e97c3b9da0
21 changed files with 430 additions and 287 deletions
+5 -12
View File
@@ -21,6 +21,7 @@ import {
import { formatBytes, formatDate } from "@/lib/format";
import type { SimilarityCheck } from "@/lib/types";
import { useE621 } from "@/store/e621";
import { toast } from "@/store/toasts";
const VIDEO_RE = /\.(mp4|webm)$/i;
@@ -39,12 +40,10 @@ export default function SimilarPage() {
const [check, setCheck] = useState<SimilarityCheck | null>(null);
const [uploading, setUploading] = useState(false);
const [percent, setPercent] = useState(0);
const [error, setError] = useState<string | null>(null);
const [dragging, setDragging] = useState(false);
const [iqdb, setIqdb] = useState<E621IqdbResult[] | null>(null);
const [iqdbBusy, setIqdbBusy] = useState(false);
const [iqdbError, setIqdbError] = useState<string | null>(null);
const [selected, setSelected] = useState<E621IqdbResult | null>(null);
const configured = Boolean(credentials?.configured);
@@ -57,10 +56,10 @@ export default function SimilarPage() {
setFile(null);
setIqdb(null);
setSelected(null);
setError(null);
setIqdbError(null);
if (inputRef.current) inputRef.current.value = "";
toast.ok("Temporary copy deleted.");
},
onError: (error) => toast.error(errorMessage(error)),
});
async function startCheck(selectedFile: File) {
@@ -68,8 +67,6 @@ export default function SimilarPage() {
setCheck(null);
setIqdb(null);
setSelected(null);
setError(null);
setIqdbError(null);
setPercent(0);
setUploading(true);
try {
@@ -82,7 +79,7 @@ export default function SimilarPage() {
setCheck(data);
void queryClient.invalidateQueries({ queryKey: ["files"] });
} catch (err) {
setError(errorMessage(err));
toast.error(errorMessage(err));
} finally {
setUploading(false);
}
@@ -98,7 +95,7 @@ export default function SimilarPage() {
);
setIqdb(results);
} catch (err) {
setIqdbError(e621ErrorMessage(err));
toast.error(e621ErrorMessage(err));
} finally {
setIqdbBusy(false);
}
@@ -183,8 +180,6 @@ export default function SimilarPage() {
</div>
) : null}
{error ? <p className="text-sm text-ctp-red">{error}</p> : null}
{check ? (
<div className="grid gap-6 lg:grid-cols-[320px_minmax(0,1fr)]">
<aside className="flex flex-col gap-4">
@@ -359,8 +354,6 @@ export default function SimilarPage() {
</Link>{" "}
to reverse-search this image on e621.
</p>
) : iqdbError ? (
<p className="mt-3 text-xs text-ctp-red">{iqdbError}</p>
) : iqdbBusy && !iqdb ? (
<div className="mt-3 flex justify-center py-4">
<Spinner className="h-4 w-4" />