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.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)} />