From 9faa5fc02f927438e477905a183030d47d2800d2 Mon Sep 17 00:00:00 2001 From: Yash Yadav Date: Thu, 4 Jun 2026 14:12:52 -0400 Subject: [PATCH 1/3] feat(recruiter): add advance candidate confirmation dialog --- client/src/components/ui/ConfirmDialog.tsx | 50 ++++++++++++++++--- .../applications/ApplicationsList.tsx | 30 ++++++++++- 2 files changed, 71 insertions(+), 9 deletions(-) diff --git a/client/src/components/ui/ConfirmDialog.tsx b/client/src/components/ui/ConfirmDialog.tsx index fd6f59ff7..24459ab72 100644 --- a/client/src/components/ui/ConfirmDialog.tsx +++ b/client/src/components/ui/ConfirmDialog.tsx @@ -1,13 +1,17 @@ -import { useEffect, useRef } from "react"; +import { useEffect, useRef, ReactNode } from "react"; +import { Loader2 } from "lucide-react"; interface ConfirmDialogProps { open: boolean; title: string; - description: string; + description?: string; confirmLabel?: string; cancelLabel?: string; onConfirm: () => void; onCancel: () => void; + confirmVariant?: "danger" | "primary"; + loading?: boolean; + children?: ReactNode; } export function ConfirmDialog({ @@ -18,18 +22,34 @@ export function ConfirmDialog({ cancelLabel = "Cancel", onConfirm, onCancel, + confirmVariant = "danger", + loading = false, + children, }: ConfirmDialogProps) { const cancelRef = useRef(null); + // Focus management useEffect(() => { if (open) cancelRef.current?.focus(); }, [open]); + // Keyboard navigation & Escape key dismiss + useEffect(() => { + if (!open) return; + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape" && !loading) { + onCancel(); + } + }; + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + }, [open, loading, onCancel]); + if (!open) return null; return (
-
+
{title} -

- {description} -

+
+ {children ? ( + children + ) : ( +

+ {description} +

+ )} +
@@ -62,3 +95,4 @@ export function ConfirmDialog({
); } + diff --git a/client/src/module/recruiter/applications/ApplicationsList.tsx b/client/src/module/recruiter/applications/ApplicationsList.tsx index 4bc40d136..57e075dd3 100644 --- a/client/src/module/recruiter/applications/ApplicationsList.tsx +++ b/client/src/module/recruiter/applications/ApplicationsList.tsx @@ -9,6 +9,7 @@ import api from "../../../lib/axios"; import type { Application, Pagination } from "../../../lib/types"; import { SEO } from "../../../components/SEO"; import { useDebounce } from "../../../hooks/useDebounce"; +import { ConfirmDialog } from "../../../components/ui/ConfirmDialog"; export default function ApplicationsList() { const { id: jobId } = useParams(); @@ -20,6 +21,7 @@ export default function ApplicationsList() { const [page, setPage] = useState(1); const [updatingId, setUpdatingId] = useState(null); const [advancingIds, setAdvancingIds] = useState>(() => new Set()); + const [pendingAdvanceApp, setPendingAdvanceApp] = useState(null); // Reset to page 1 when search or filter changes useEffect(() => { @@ -61,6 +63,7 @@ export default function ApplicationsList() { await api.patch(`/recruiter/applications/${appId}/advance`); queryClient.invalidateQueries({ queryKey: ["applications"] }); toast.success("Application advanced"); + setPendingAdvanceApp(null); } catch { toast.error("Failed to advance application"); } finally { @@ -75,6 +78,31 @@ export default function ApplicationsList() { return (
+ setPendingAdvanceApp(null)} + onConfirm={() => { + if (pendingAdvanceApp && !advancingIds.has(pendingAdvanceApp.id)) { + handleAdvance(pendingAdvanceApp.id); + } + }} + > + {pendingAdvanceApp && ( +
+

+ Are you sure you want to advance {pendingAdvanceApp.student?.name} to the next hiring stage? +

+

+ Warning: Advancing this candidate will update their hiring stage and create a new round submission. +

+
+ )} +
Back to Jobs @@ -191,7 +219,7 @@ export default function ApplicationsList() {
-