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
+8 -43
View File
@@ -1,4 +1,4 @@
import { CheckCircle2, Eye, EyeOff, XCircle } from "lucide-react";
import { Eye, EyeOff } from "lucide-react";
import { useEffect, useState } from "react";
import { Button, Field, Spinner, inputClass } from "@/components/ui";
@@ -11,6 +11,7 @@ import {
} from "@/lib/e621";
import { useAuth } from "@/store/auth";
import { useE621 } from "@/store/e621";
import { toast } from "@/store/toasts";
const BASE_URL_OPTIONS = [
{ value: "https://e621.net", label: "e621.net — main site" },
@@ -33,13 +34,7 @@ function AccountForm({ credentials }: { credentials: E621Credentials | null }) {
const [customBase, setCustomBase] = useState(savedBaseIsKnown ? "" : savedBase);
const [showKey, setShowKey] = useState(false);
const [saving, setSaving] = useState(false);
const [saveMessage, setSaveMessage] = useState<string | null>(null);
const [saveError, setSaveError] = useState<string | null>(null);
const [testing, setTesting] = useState(false);
const [testResult, setTestResult] = useState<{
ok: boolean;
message: string;
} | null>(null);
const baseUrl = (baseChoice === "custom" ? customBase.trim() : baseChoice).replace(
/\/+$/,
@@ -53,13 +48,11 @@ function AccountForm({ credentials }: { credentials: E621Credentials | null }) {
async function handleSave() {
setSaving(true);
setSaveMessage(null);
setSaveError(null);
try {
await save(values);
setSaveMessage("Credentials saved.");
toast.ok("e621 credentials saved.");
} catch (error) {
setSaveError(errorMessage(error));
toast.error(errorMessage(error));
} finally {
setSaving(false);
}
@@ -67,19 +60,17 @@ function AccountForm({ credentials }: { credentials: E621Credentials | null }) {
async function handleTest() {
setTesting(true);
setTestResult(null);
try {
const user = await verifyE621Credentials({ ...values, configured: true });
setTestResult({
ok: true,
message: `Connected as ${user.name}${
toast.ok(
`Connected as ${user.name}${
typeof user.favorite_count === "number"
? ` — ${user.favorite_count} favorites`
: ""
}.`,
});
);
} catch (error) {
setTestResult({ ok: false, message: e621ErrorMessage(error) });
toast.error(e621ErrorMessage(error));
} finally {
setTesting(false);
}
@@ -167,32 +158,6 @@ function AccountForm({ credentials }: { credentials: E621Credentials | null }) {
</div>
</Field>
{saveError ? (
<p className="rounded-md border border-ctp-red/30 bg-ctp-red/10 px-3 py-2 text-sm text-ctp-red">
{saveError}
</p>
) : null}
{saveMessage ? (
<p className="text-sm text-ctp-green">{saveMessage}</p>
) : null}
{testResult ? (
<p
className={cn(
"flex items-center gap-2 rounded-md border px-3 py-2 text-sm",
testResult.ok
? "border-ctp-green/30 bg-ctp-green/10 text-ctp-green"
: "border-ctp-red/30 bg-ctp-red/10 text-ctp-red",
)}
>
{testResult.ok ? (
<CheckCircle2 className="h-4 w-4 shrink-0" />
) : (
<XCircle className="h-4 w-4 shrink-0" />
)}
{testResult.message}
</p>
) : null}
<div className="flex items-center gap-3">
<Button onClick={() => void handleSave()} disabled={saving}>
{saving ? "Saving…" : "Save"}