diff --git a/job-tracker-ui/src/App.tsx b/job-tracker-ui/src/App.tsx
index 766ec90..b062b51 100644
--- a/job-tracker-ui/src/App.tsx
+++ b/job-tracker-ui/src/App.tsx
@@ -21,6 +21,7 @@ import { Navigate, Route, Routes, useLocation, useNavigate, createBrowserRouter,
import { getTheme } from "./theme";
import { ToastProvider } from "./toast";
import { ConfirmProvider } from "./confirm";
+import { PromptProvider } from "./prompt";
import JobTable, { JobTableColumns } from "./components/JobTable";
import AddJobModal from "./components/AddJobModal";
@@ -237,12 +238,16 @@ export default function App() {
return (
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/job-tracker-ui/src/components/JobDetailsDialog.tsx b/job-tracker-ui/src/components/JobDetailsDialog.tsx
index e635dfb..cb58b45 100644
--- a/job-tracker-ui/src/components/JobDetailsDialog.tsx
+++ b/job-tracker-ui/src/components/JobDetailsDialog.tsx
@@ -194,7 +194,7 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) {
disabled={refreshingAi}
onClick={async () => {
if (!jobId) return;
- if (!window.confirm("Overwrite the current summary and skills with a freshly generated version?")) return;
+ if (!(await confirmAction("Overwrite the current summary and skills with a freshly generated version?", { title: "Refresh AI summary", confirmLabel: "Refresh" }))) return;
setRefreshingAi(true);
try {
const res = await api.post(`/jobapplications/${jobId}/refresh-ai`);
diff --git a/job-tracker-ui/src/components/JobTable.tsx b/job-tracker-ui/src/components/JobTable.tsx
index bdfe023..9e58b1b 100644
--- a/job-tracker-ui/src/components/JobTable.tsx
+++ b/job-tracker-ui/src/components/JobTable.tsx
@@ -123,6 +123,7 @@ function statusTone(status: string): string {
export default function JobTable({ refreshToken, pageSize, onPageSizeChange, columns, onColumnsChange, mode = "jobs" }: Props) {
const theme = useTheme();
const { toast } = useToast();
+ const { confirmAction } = useDialogActions();
const location = useLocation();
const navigate = useNavigate();
const [jobs, setJobs] = useState([]);
@@ -400,3 +401,8 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
);
}
+s); setStatusAnchor(null); setStatusJobId(null); }}>Set {s})}
+
+
+ );
+}
diff --git a/job-tracker-ui/src/confirm.test.tsx b/job-tracker-ui/src/confirm.test.tsx
new file mode 100644
index 0000000..d40aaca
--- /dev/null
+++ b/job-tracker-ui/src/confirm.test.tsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import '@testing-library/jest-dom';
+import { render, screen, fireEvent } from '@testing-library/react';
+import { ConfirmProvider, useConfirm } from './confirm';
+
+function Demo() {
+ const { confirm } = useConfirm();
+ return (
+
+ );
+}
+
+test('renders app-owned confirmation dialog', async () => {
+ render(
+
+
+ ,
+ );
+
+ fireEvent.click(screen.getByRole('button', { name: /open confirm/i }));
+
+ expect(await screen.findByText(/are you sure/i)).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /delete/i })).toBeInTheDocument();
+});
diff --git a/job-tracker-ui/src/dialogs.ts b/job-tracker-ui/src/dialogs.ts
index 479ee2f..5b909de 100644
--- a/job-tracker-ui/src/dialogs.ts
+++ b/job-tracker-ui/src/dialogs.ts
@@ -1,7 +1,9 @@
import { useConfirm } from "./confirm";
+import { usePrompt } from "./prompt";
export function useDialogActions() {
const { confirm } = useConfirm();
+ const { prompt } = usePrompt();
return {
confirmAction: (message: string, options?: { title?: string; confirmLabel?: string; cancelLabel?: string; destructive?: boolean }) =>
@@ -12,6 +14,13 @@ export function useDialogActions() {
cancelLabel: options?.cancelLabel,
destructive: options?.destructive,
}),
- promptForValue: async (_message: string, defaultValue = "") => window.prompt(_message, defaultValue),
+ promptForValue: (message: string, defaultValue = "", options?: { title?: string; confirmLabel?: string; cancelLabel?: string }) =>
+ prompt({
+ message,
+ defaultValue,
+ title: options?.title,
+ confirmLabel: options?.confirmLabel,
+ cancelLabel: options?.cancelLabel,
+ }),
};
}
diff --git a/job-tracker-ui/src/prompt.tsx b/job-tracker-ui/src/prompt.tsx
new file mode 100644
index 0000000..c13683a
--- /dev/null
+++ b/job-tracker-ui/src/prompt.tsx
@@ -0,0 +1,80 @@
+import React, { createContext, useCallback, useContext, useMemo, useState } from "react";
+import { Button, Dialog, DialogActions, DialogContent, DialogTitle, TextField, Typography } from "@mui/material";
+
+type PromptOptions = {
+ title?: string;
+ message: string;
+ defaultValue?: string;
+ confirmLabel?: string;
+ cancelLabel?: string;
+};
+
+type PromptContextValue = {
+ prompt: (options: PromptOptions) => Promise;
+};
+
+type PromptState = PromptOptions & {
+ open: boolean;
+ resolver?: (value: string | null) => void;
+};
+
+const PromptContext = createContext(null);
+
+export function PromptProvider({ children }: { children: React.ReactNode }) {
+ const [state, setState] = useState({
+ open: false,
+ title: "Enter value",
+ message: "",
+ defaultValue: "",
+ confirmLabel: "Save",
+ cancelLabel: "Cancel",
+ });
+ const [value, setValue] = useState("");
+
+ const closeWith = useCallback((next: string | null) => {
+ setState((prev) => {
+ prev.resolver?.(next);
+ return { ...prev, open: false, resolver: undefined };
+ });
+ }, []);
+
+ const prompt = useCallback((options: PromptOptions) => {
+ return new Promise((resolve) => {
+ setValue(options.defaultValue ?? "");
+ setState({
+ open: true,
+ title: options.title ?? "Enter value",
+ message: options.message,
+ defaultValue: options.defaultValue ?? "",
+ confirmLabel: options.confirmLabel ?? "Save",
+ cancelLabel: options.cancelLabel ?? "Cancel",
+ resolver: resolve,
+ });
+ });
+ }, []);
+
+ const contextValue = useMemo(() => ({ prompt }), [prompt]);
+
+ return (
+
+ {children}
+
+
+ );
+}
+
+export function usePrompt() {
+ const ctx = useContext(PromptContext);
+ if (!ctx) throw new Error("usePrompt must be used within PromptProvider");
+ return ctx;
+}