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..536c2b5ae 100644 --- a/client/src/module/recruiter/applications/ApplicationsList.tsx +++ b/client/src/module/recruiter/applications/ApplicationsList.tsx @@ -9,17 +9,18 @@ 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(); const queryClient = useQueryClient(); - const [pagination, setPagination] = useState(null); const [search, setSearch] = useState(""); const debouncedSearch = useDebounce(search, 300); const [statusFilter, setStatusFilter] = useState(""); 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(() => { @@ -30,15 +31,18 @@ export default function ApplicationsList() { queryKey: ["applications", jobId, page, statusFilter, debouncedSearch], queryFn: async () => { const params = new URLSearchParams({ page: String(page), limit: "10" }); - if (debouncedSearch) params.set("search", debouncedSearch); + if (debouncedSearch.trim()) params.set("search", debouncedSearch.trim()); if (statusFilter) params.set("status", statusFilter); const res = await api.get(`/recruiter/jobs/${jobId}/applications?${params}`); - setPagination(res.data.pagination); - return res.data.applications as Application[]; + return { + applications: res.data.applications as Application[], + pagination: res.data.pagination as Pagination, + }; }, }); - const applications = data ?? []; + const applications = data?.applications ?? []; + const pagination = data?.pagination ?? null; const handleStatusChange = async (appId: number, status: string) => { if (updatingId === appId) return; @@ -61,6 +65,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 +80,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 +221,7 @@ export default function ApplicationsList() {
-