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>
);
}

42 changes: 36 additions & 6 deletions client/src/module/recruiter/applications/ApplicationsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Pagination | null>(null);
const [search, setSearch] = useState("");
const debouncedSearch = useDebounce(search, 300);
const [statusFilter, setStatusFilter] = useState("");
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 All @@ -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;
Expand All @@ -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 {
Expand All @@ -75,6 +80,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>
)}
Comment on lines +97 to +106
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Provide a fallback for undefined candidate name.

If pendingAdvanceApp.student.name is undefined or empty, the confirmation message will have an empty <strong> tag, degrading readability.

🛡️ Proposed fix to add a name fallback
           <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?
+            Are you sure you want to advance <strong className="font-semibold text-stone-900 dark:text-white">{pendingAdvanceApp.student?.name || "this candidate"}</strong> to the next hiring stage?
           </p>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{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>
)}
{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 || "this candidate"}</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>
)}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/module/recruiter/applications/ApplicationsList.tsx` around lines
97 - 106, The confirmation message can render an empty <strong> when
pendingAdvanceApp.student?.name is missing; update the JSX in ApplicationsList
(the interpolation inside the <strong> in the pendingAdvanceApp block) to use a
safe fallback (e.g., pendingAdvanceApp.student?.name ?? "this candidate" or
"Unnamed candidate") so the UI always shows a readable name, keeping the rest of
the message unchanged.

</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 +221,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