diff --git a/web/components/accounts-table.tsx b/web/components/accounts-table.tsx
index 7dccdb2..9994393 100644
--- a/web/components/accounts-table.tsx
+++ b/web/components/accounts-table.tsx
@@ -7,6 +7,7 @@ import { deleteAccount, updateAccount } from "@/app/actions";
import EditableCell from "./editable-cell";
import ConfirmDialog from "./confirm-dialog";
import CreateAccountDialog from "./create-account-dialog";
+import Toast, { type ToastMessage } from "./toast";
type Props = { initial: Acc[]; prefixPattern: string };
type SortDir = "asc" | "desc";
@@ -71,6 +72,7 @@ export default function AccountsTable({ initial, prefixPattern }: Props) {
const [deleting, setDeleting] = useState(false);
const [deleteError, setDeleteError] = useState(null);
const [createOpen, setCreateOpen] = useState(false);
+ const [toast, setToast] = useState(null);
const [optimistic, applyOptimistic] = useOptimistic(
initial,
@@ -113,7 +115,9 @@ export default function AccountsTable({ initial, prefixPattern }: Props) {
const result = await deleteAccount(deleteTarget);
setDeleting(false);
if (result.ok) {
+ const deleted = deleteTarget;
setDeleteTarget(null);
+ setToast({ type: "success", message: `Account ${deleted} deleted` });
} else {
setDeleteError(result.error);
}
@@ -290,9 +294,14 @@ export default function AccountsTable({ initial, prefixPattern }: Props) {
setCreateOpen(false)}
+ onSuccess={(name) =>
+ setToast({ type: "success", message: `Account ${name} created` })
+ }
prefixPattern={prefixPattern}
/>
+ setToast(null)} />
+
{
diff --git a/web/components/create-account-dialog.tsx b/web/components/create-account-dialog.tsx
index 7cbcf9d..7915c4b 100644
--- a/web/components/create-account-dialog.tsx
+++ b/web/components/create-account-dialog.tsx
@@ -7,10 +7,11 @@ import FormDialogShell, { Field, inputClass } from "./form-dialog-shell";
type Props = {
open: boolean;
onClose: () => void;
+ onSuccess?: (username: string) => void;
prefixPattern?: string;
};
-export default function CreateAccountDialog({ open, onClose, prefixPattern }: Props) {
+export default function CreateAccountDialog({ open, onClose, onSuccess, prefixPattern }: Props) {
const [pending, startTransition] = useTransition();
const [error, setError] = useState(null);
const [username, setUsername] = useState("");
@@ -35,14 +36,16 @@ export default function CreateAccountDialog({ open, onClose, prefixPattern }: Pr
return;
}
setError(null);
+ const trimmedUsername = username.trim();
startTransition(async () => {
const result = await createAccount({
- username: username.trim(),
+ username: trimmedUsername,
password,
status,
link,
});
if (result.ok) {
+ onSuccess?.(trimmedUsername);
onClose();
} else {
setError(result.error);
diff --git a/web/components/create-user-dialog.tsx b/web/components/create-user-dialog.tsx
index be82c92..9cff32d 100644
--- a/web/components/create-user-dialog.tsx
+++ b/web/components/create-user-dialog.tsx
@@ -7,9 +7,10 @@ import FormDialogShell, { Field, inputClass } from "./form-dialog-shell";
type Props = {
open: boolean;
onClose: () => void;
+ onSuccess?: (fUsername: string) => void;
};
-export default function CreateUserDialog({ open, onClose }: Props) {
+export default function CreateUserDialog({ open, onClose, onSuccess }: Props) {
const [pending, startTransition] = useTransition();
const [error, setError] = useState(null);
const [fUsername, setFUsername] = useState("");
@@ -33,14 +34,16 @@ export default function CreateUserDialog({ open, onClose }: Props) {
return;
}
setError(null);
+ const trimmedFUsername = fUsername.trim();
startTransition(async () => {
const result = await createUser({
- f_username: fUsername.trim(),
+ f_username: trimmedFUsername,
f_password: fPassword,
t_username: tUsername.trim(),
t_password: tPassword,
});
if (result.ok) {
+ onSuccess?.(trimmedFUsername);
onClose();
} else {
setError(result.error);
diff --git a/web/components/toast.tsx b/web/components/toast.tsx
new file mode 100644
index 0000000..2d8d3d3
--- /dev/null
+++ b/web/components/toast.tsx
@@ -0,0 +1,57 @@
+"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 (
+
+
+
+ {toast.type === "success" ? "✓" : "!"}
+
+ {toast.message}
+
+
+ );
+}
diff --git a/web/components/users-table.tsx b/web/components/users-table.tsx
index bd3316c..e26816f 100644
--- a/web/components/users-table.tsx
+++ b/web/components/users-table.tsx
@@ -7,6 +7,7 @@ import { deleteUser, updateUser } from "@/app/actions";
import EditableCell from "./editable-cell";
import ConfirmDialog from "./confirm-dialog";
import CreateUserDialog from "./create-user-dialog";
+import Toast, { type ToastMessage } from "./toast";
type Props = { initial: User[]; prefixPattern: string };
type SortDir = "asc" | "desc";
@@ -84,6 +85,7 @@ export default function UsersTable({ initial, prefixPattern }: Props) {
const [deleting, setDeleting] = useState(false);
const [deleteError, setDeleteError] = useState(null);
const [createOpen, setCreateOpen] = useState(false);
+ const [toast, setToast] = useState(null);
const [optimistic, applyOptimistic] = useOptimistic(
initial,
@@ -144,7 +146,9 @@ export default function UsersTable({ initial, prefixPattern }: Props) {
const result = await deleteUser(deleteTarget);
setDeleting(false);
if (result.ok) {
+ const deleted = deleteTarget;
setDeleteTarget(null);
+ setToast({ type: "success", message: `User ${deleted} deleted` });
} else {
setDeleteError(result.error);
}
@@ -180,7 +184,15 @@ export default function UsersTable({ initial, prefixPattern }: Props) {
No users yet. Click Add to create one manually.
- setCreateOpen(false)} />
+ setCreateOpen(false)}
+ onSuccess={(name) =>
+ setToast({ type: "success", message: `User ${name} created` })
+ }
+ />
+
+ setToast(null)} />
);
}
@@ -332,7 +344,15 @@ export default function UsersTable({ initial, prefixPattern }: Props) {
})}
- setCreateOpen(false)} />
+ setCreateOpen(false)}
+ onSuccess={(name) =>
+ setToast({ type: "success", message: `User ${name} created` })
+ }
+ />
+
+ setToast(null)} />