Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 42 additions & 8 deletions client/src/components/ui/ConfirmDialog.tsx
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -18,18 +22,34 @@ export function ConfirmDialog({
cancelLabel = "Cancel",
onConfirm,
onCancel,
confirmVariant = "danger",
loading = false,
children,
}: ConfirmDialogProps) {
const cancelRef = useRef<HTMLButtonElement>(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 (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="fixed inset-0 bg-black/50" onClick={onCancel} />
<div className="fixed inset-0 bg-black/50" onClick={loading ? undefined : onCancel} />
<div
role="alertdialog"
aria-modal="true"
Expand All @@ -40,25 +60,39 @@ export function ConfirmDialog({
<h2 id="confirm-title" className="text-base font-semibold text-stone-900 dark:text-white">
{title}
</h2>
<p id="confirm-desc" className="mt-2 text-sm text-stone-600 dark:text-stone-400">
{description}
</p>
<div id="confirm-desc" className="mt-2 text-sm text-stone-600 dark:text-stone-400">
{children ? (
children
) : (
<p className="leading-relaxed">
{description}
</p>
)}
</div>
<div className="mt-5 flex justify-end gap-3">
<button
ref={cancelRef}
onClick={onCancel}
className="px-4 py-2 text-sm rounded-lg border border-stone-200 dark:border-white/10 text-stone-700 dark:text-stone-300 hover:bg-stone-100 dark:hover:bg-white/5 transition-colors"
disabled={loading}
className="px-4 py-2 text-sm rounded-lg border border-stone-200 dark:border-white/10 text-stone-700 dark:text-stone-300 hover:bg-stone-100 dark:hover:bg-white/5 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{cancelLabel}
</button>
<button
onClick={onConfirm}
className="px-4 py-2 text-sm rounded-lg bg-red-600 hover:bg-red-700 text-white font-medium transition-colors"
disabled={loading}
className={`inline-flex items-center gap-1.5 px-4 py-2 text-sm rounded-lg font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${
confirmVariant === "primary"
? "bg-black dark:bg-white text-white dark:text-stone-950 hover:bg-stone-800 dark:hover:bg-stone-200"
: "bg-red-600 hover:bg-red-700 text-white"
}`}
>
{loading && <Loader2 className="h-3.5 w-3.5 animate-spin" />}
{confirmLabel}
</button>
</div>
</div>
</div>
);
}

30 changes: 29 additions & 1 deletion client/src/module/recruiter/applications/ApplicationsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -20,6 +21,7 @@ export default function ApplicationsList() {
const [page, setPage] = useState(1);
const [updatingId, setUpdatingId] = useState<number | null>(null);
const [advancingIds, setAdvancingIds] = useState<Set<number>>(() => new Set());
const [pendingAdvanceApp, setPendingAdvanceApp] = useState<Application | null>(null);

// Reset to page 1 when search or filter changes
useEffect(() => {
Expand Down Expand Up @@ -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 {
Expand All @@ -75,6 +78,31 @@ export default function ApplicationsList() {
return (
<div>
<SEO title="Applications" noIndex />
<ConfirmDialog
open={pendingAdvanceApp !== null}
title="Advance Candidate?"
confirmLabel="Confirm Advance"
cancelLabel="Cancel"
confirmVariant="primary"
loading={pendingAdvanceApp ? advancingIds.has(pendingAdvanceApp.id) : false}
onCancel={() => setPendingAdvanceApp(null)}
onConfirm={() => {
if (pendingAdvanceApp && !advancingIds.has(pendingAdvanceApp.id)) {
handleAdvance(pendingAdvanceApp.id);
}
}}
>
{pendingAdvanceApp && (
<div className="space-y-4">
<p className="text-sm text-stone-600 dark:text-stone-400">
Are you sure you want to advance <strong className="font-semibold text-stone-900 dark:text-white">{pendingAdvanceApp.student?.name}</strong> to the next hiring stage?
</p>
<p className="text-xs text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-950/20 p-2.5 rounded-lg border border-amber-200/30 dark:border-amber-900/30 leading-normal">
Warning: Advancing this candidate will update their hiring stage and create a new round submission.
</p>
</div>
)}
</ConfirmDialog>
<Link to="/recruiters/jobs" className="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-500 hover:text-black dark:hover:text-white mb-4 no-underline">
<ArrowLeft className="w-4 h-4" /> Back to Jobs
</Link>
Expand Down Expand Up @@ -191,7 +219,7 @@ export default function ApplicationsList() {
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2">
<button onClick={() => handleAdvance(app.id)}
<button onClick={() => setPendingAdvanceApp(app)}
disabled={isAdvancing}
className={`inline-flex min-w-21.5 items-center justify-center gap-1.5 text-xs px-3 py-1.5 bg-black dark:bg-white text-white dark:text-gray-950 rounded-lg hover:bg-gray-800 dark:hover:bg-gray-200 transition-colors ${isAdvancing ? "cursor-not-allowed opacity-70" : ""}`}>
{isAdvancing && <Loader2 className="h-3 w-3 animate-spin" />}
Expand Down
Loading