yiekheng eebbcb3db2 feat(web): success toast on confirmed create/delete
Adds a small top-centered <Toast> that fires only when the Server
Action returns { ok: true } (i.e., the DB write actually succeeded).
Auto-dismisses after 3s.

Wires both create dialogs (CreateAccountDialog, CreateUserDialog) with
an onSuccess callback that the table parent uses to push the toast,
and the delete confirm-flow does the same. Inline-edit success stays
quiet (no toast) — only add/delete trigger it, per the requested
scope.
2026-05-02 21:20:25 +08:00

58 lines
1.5 KiB
TypeScript

"use client";
import { useEffect } from "react";
export type ToastMessage = {
type: "success" | "error";
message: string;
};
/**
* Simple top-centered toast. Auto-dismisses after `durationMs` (default
* 3s). Owners hold the ToastMessage state; this component reads it and
* calls onDismiss when the timer fires (or when the toast object
* changes — useEffect's cleanup clears any in-flight timer).
*/
export default function Toast({
toast,
onDismiss,
durationMs = 3000,
}: {
toast: ToastMessage | null;
onDismiss: () => void;
durationMs?: number;
}) {
useEffect(() => {
if (!toast) return;
const id = setTimeout(onDismiss, durationMs);
return () => clearTimeout(id);
}, [toast, onDismiss, durationMs]);
if (!toast) return null;
const styles =
toast.type === "success"
? "bg-emerald-50 text-emerald-800 ring-emerald-200"
: "bg-red-50 text-red-800 ring-red-200";
return (
<div
role="status"
aria-live="polite"
className="fixed left-1/2 top-4 z-50 -translate-x-1/2 transform px-4"
>
<div
className={`flex items-center gap-2 rounded-full px-4 py-2 shadow-sm ring-1 ${styles}`}
>
<span
aria-hidden="true"
className="inline-flex h-5 w-5 items-center justify-center rounded-full bg-current/10 text-xs font-bold"
>
{toast.type === "success" ? "✓" : "!"}
</span>
<span className="text-sm font-medium">{toast.message}</span>
</div>
</div>
);
}