diff --git a/job-tracker-ui/public/index.html b/job-tracker-ui/public/index.html
index ffb2312..2c49c04 100644
--- a/job-tracker-ui/public/index.html
+++ b/job-tracker-ui/public/index.html
@@ -2,6 +2,7 @@
+
diff --git a/job-tracker-ui/src/App.tsx b/job-tracker-ui/src/App.tsx
index 8367157..766ec90 100644
--- a/job-tracker-ui/src/App.tsx
+++ b/job-tracker-ui/src/App.tsx
@@ -20,6 +20,7 @@ import { Navigate, Route, Routes, useLocation, useNavigate, createBrowserRouter,
import { getTheme } from "./theme";
import { ToastProvider } from "./toast";
+import { ConfirmProvider } from "./confirm";
import JobTable, { JobTableColumns } from "./components/JobTable";
import AddJobModal from "./components/AddJobModal";
@@ -245,3 +246,6 @@ export default function App() {
);
}
+vider>
+ );
+}
diff --git a/job-tracker-ui/src/components/Attachments.tsx b/job-tracker-ui/src/components/Attachments.tsx
index 6325a12..118c4a7 100644
--- a/job-tracker-ui/src/components/Attachments.tsx
+++ b/job-tracker-ui/src/components/Attachments.tsx
@@ -95,7 +95,7 @@ export default function Attachments({ jobId }: { jobId: number }) {
};
const remove = async (a: AttachmentItem) => {
- if (!confirmAction(`Delete attachment "${a.fileName}"?`)) return;
+ if (!(await confirmAction(`Delete attachment "${a.fileName}"?`, { title: "Delete attachment", confirmLabel: "Delete", destructive: true }))) return;
try {
await api.delete(`/attachments/${a.id}`);
toast("Deleted attachment.", "success");
diff --git a/job-tracker-ui/src/components/Correspondence.tsx b/job-tracker-ui/src/components/Correspondence.tsx
index d60ab65..a841a10 100644
--- a/job-tracker-ui/src/components/Correspondence.tsx
+++ b/job-tracker-ui/src/components/Correspondence.tsx
@@ -28,6 +28,7 @@ import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
import { api } from "../api";
import { useToast } from "../toast";
import { CorrespondenceMessage, GmailMessageSummary, GmailStatus } from "../types";
+import { useDialogActions } from "../dialogs";
function parseRawEmail(raw: string): {
subject?: string;
@@ -73,6 +74,7 @@ function parseRawEmail(raw: string): {
export default function Correspondence({ jobId }: { jobId: number }) {
const theme = useTheme();
const { toast } = useToast();
+ const { confirmAction } = useDialogActions();
const [messages, setMessages] = useState([]);
const [from, setFrom] = useState<"Me" | "Company">("Me");
const [text, setText] = useState("");
@@ -233,9 +235,8 @@ export default function Correspondence({ jobId }: { jobId: number }) {
}
};
-
const deleteMessage = async (messageId: number) => {
- if (!confirmAction("Remove this correspondence message?")) return;
+ if (!(await confirmAction("Remove this correspondence message?", { title: "Delete message", confirmLabel: "Delete", destructive: true }))) return;
try {
await api.delete(`/correspondence/${messageId}`);
await load();
@@ -527,8 +528,3 @@ export default function Correspondence({ jobId }: { jobId: number }) {
);
}
-tions>
-
-
- );
-}
diff --git a/job-tracker-ui/src/components/UserManagementCard.tsx b/job-tracker-ui/src/components/UserManagementCard.tsx
index fc4e8fd..673eb38 100644
--- a/job-tracker-ui/src/components/UserManagementCard.tsx
+++ b/job-tracker-ui/src/components/UserManagementCard.tsx
@@ -17,6 +17,7 @@ type UserDto = {
export default function UserManagementCard() {
const { toast } = useToast();
+ const { confirmAction } = useDialogActions();
const token = getAuthToken();
const [supported, setSupported] = useState(null);
@@ -150,7 +151,7 @@ export default function UserManagementCard() {
variant="outlined"
color="error"
onClick={async () => {
- if (!window.confirm("Delete this user?")) return;
+ if (!(await confirmAction("Delete this user?", { title: "Delete user", confirmLabel: "Delete", destructive: true }))) return;
try {
await api.delete(`/users/${u.id}`);
toast("User deleted.", "info");
diff --git a/job-tracker-ui/src/confirm.tsx b/job-tracker-ui/src/confirm.tsx
new file mode 100644
index 0000000..6d50f1a
--- /dev/null
+++ b/job-tracker-ui/src/confirm.tsx
@@ -0,0 +1,79 @@
+import React, { createContext, useCallback, useContext, useMemo, useState } from "react";
+import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Typography } from "@mui/material";
+
+type ConfirmOptions = {
+ title?: string;
+ message: string;
+ confirmLabel?: string;
+ cancelLabel?: string;
+ destructive?: boolean;
+};
+
+type ConfirmContextValue = {
+ confirm: (options: ConfirmOptions) => Promise;
+};
+
+type ConfirmState = ConfirmOptions & {
+ open: boolean;
+ resolver?: (value: boolean) => void;
+};
+
+const ConfirmContext = createContext(null);
+
+export function ConfirmProvider({ children }: { children: React.ReactNode }) {
+ const [state, setState] = useState({
+ open: false,
+ message: "",
+ title: "Confirm action",
+ confirmLabel: "Confirm",
+ cancelLabel: "Cancel",
+ destructive: false,
+ });
+
+ const closeWith = useCallback((value: boolean) => {
+ setState((prev) => {
+ prev.resolver?.(value);
+ return { ...prev, open: false, resolver: undefined };
+ });
+ }, []);
+
+ const confirm = useCallback((options: ConfirmOptions) => {
+ return new Promise((resolve) => {
+ setState({
+ open: true,
+ title: options.title ?? "Confirm action",
+ message: options.message,
+ confirmLabel: options.confirmLabel ?? "Confirm",
+ cancelLabel: options.cancelLabel ?? "Cancel",
+ destructive: options.destructive ?? false,
+ resolver: resolve,
+ });
+ });
+ }, []);
+
+ const value = useMemo(() => ({ confirm }), [confirm]);
+
+ return (
+
+ {children}
+
+
+ );
+}
+
+export function useConfirm() {
+ const ctx = useContext(ConfirmContext);
+ if (!ctx) throw new Error("useConfirm must be used within ConfirmProvider");
+ return ctx;
+}
diff --git a/job-tracker-ui/src/dialogs.ts b/job-tracker-ui/src/dialogs.ts
index f91983c..479ee2f 100644
--- a/job-tracker-ui/src/dialogs.ts
+++ b/job-tracker-ui/src/dialogs.ts
@@ -1,7 +1,17 @@
-export function confirmAction(message: string): boolean {
- return window.confirm(message);
-}
+import { useConfirm } from "./confirm";
-export function promptForValue(message: string, defaultValue = ""): string | null {
- return window.prompt(message, defaultValue);
+export function useDialogActions() {
+ const { confirm } = useConfirm();
+
+ return {
+ confirmAction: (message: string, options?: { title?: string; confirmLabel?: string; cancelLabel?: string; destructive?: boolean }) =>
+ confirm({
+ message,
+ title: options?.title,
+ confirmLabel: options?.confirmLabel,
+ cancelLabel: options?.cancelLabel,
+ destructive: options?.destructive,
+ }),
+ promptForValue: async (_message: string, defaultValue = "") => window.prompt(_message, defaultValue),
+ };
}
diff --git a/job-tracker-ui/src/pages/AdminUsersPage.tsx b/job-tracker-ui/src/pages/AdminUsersPage.tsx
index 4ba8771..eabcca7 100644
--- a/job-tracker-ui/src/pages/AdminUsersPage.tsx
+++ b/job-tracker-ui/src/pages/AdminUsersPage.tsx
@@ -18,6 +18,7 @@ import {
import { api } from "../api";
import { useToast } from "../toast";
+import { useDialogActions } from "../dialogs";
type UserDto = {
id: string;
@@ -98,7 +99,7 @@ export default function AdminUsersPage() {
};
const remove = async (u: UserDto) => {
- if (!confirmAction(`Delete user ${u.email || u.userName || u.id}?`)) return;
+ if (!(await confirmAction(`Delete user ${u.email || u.userName || u.id}?`, { title: "Delete user", confirmLabel: "Delete", destructive: true }))) return;
try {
await api.delete(`/users/${u.id}`);
toast("User deleted.", "info");