From 356e467e633fa34d7b1741deee4c261c74e26b53 Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Sat, 15 Nov 2025 09:26:05 +0000 Subject: [PATCH] Create ApplicantTrackerWidget for NEXUS Client Kanban cgen-2da5148275134a0ba8fdba00651c7b58 --- client/components/ApplicantTrackerWidget.tsx | 242 +++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 client/components/ApplicantTrackerWidget.tsx diff --git a/client/components/ApplicantTrackerWidget.tsx b/client/components/ApplicantTrackerWidget.tsx new file mode 100644 index 00000000..07afb8c7 --- /dev/null +++ b/client/components/ApplicantTrackerWidget.tsx @@ -0,0 +1,242 @@ +import { useState } from "react"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Users, MessageCircle, CheckCircle, Clock, ArrowRight } from "lucide-react"; + +export interface Applicant { + id: string; + user?: { + id: string; + name: string; + avatar_url?: string; + }; + opportunity?: { + id: string; + title: string; + }; + status: "applied" | "interviewing" | "hired"; + rating?: number; + notes?: string; + applied_at?: string; +} + +interface ApplicantTrackerWidgetProps { + applicants: Applicant[]; + title?: string; + description?: string; + onViewProfile?: (applicantId: string) => void; + onMessage?: (applicantId: string) => void; + onUpdateStatus?: (applicantId: string, newStatus: "applied" | "interviewing" | "hired") => void; + accentColor?: "blue" | "purple" | "cyan" | "green"; +} + +const statusColors = { + applied: { + bg: "bg-blue-950/40", + border: "border-blue-500/20", + badge: "bg-blue-600/50 text-blue-100", + label: "Applied", + icon: Clock, + }, + interviewing: { + bg: "bg-purple-950/40", + border: "border-purple-500/20", + badge: "bg-purple-600/50 text-purple-100", + label: "Interviewing", + icon: MessageCircle, + }, + hired: { + bg: "bg-green-950/40", + border: "border-green-500/20", + badge: "bg-green-600/50 text-green-100", + label: "Hired", + icon: CheckCircle, + }, +}; + +const nextStatusMap: Record = { + applied: "interviewing", + interviewing: "hired", + hired: "applied", +}; + +export function ApplicantTrackerWidget({ + applicants, + title = "Applicant Tracker", + description = "Track applicants through your hiring pipeline", + onViewProfile, + onMessage, + onUpdateStatus, + accentColor = "blue", +}: ApplicantTrackerWidgetProps) { + const [draggedApplicant, setDraggedApplicant] = useState(null); + + const statusCounts = { + applied: applicants.filter(a => a.status === "applied").length, + interviewing: applicants.filter(a => a.status === "interviewing").length, + hired: applicants.filter(a => a.status === "hired").length, + }; + + const allStatuses: Array<"applied" | "interviewing" | "hired"> = ["applied", "interviewing", "hired"]; + + const handleDragStart = (applicantId: string) => { + setDraggedApplicant(applicantId); + }; + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + }; + + const handleDrop = (status: "applied" | "interviewing" | "hired") => { + if (draggedApplicant) { + onUpdateStatus?.(draggedApplicant, status); + setDraggedApplicant(null); + } + }; + + return ( + + + + + {title} + + {description} + + + {applicants.length === 0 ? ( +
+ +

No applicants yet

+
+ ) : ( +
+ {allStatuses.map((status) => { + const statusInfo = statusColors[status]; + const StatusIcon = statusInfo.icon; + const statusApplicants = applicants.filter(a => a.status === status); + + return ( +
handleDrop(status)} + className={`p-4 rounded-lg border-2 border-dashed transition ${statusInfo.border} ${statusInfo.bg}`} + > + {/* Column Header */} +
+
+ + {statusInfo.label} +
+

{statusApplicants.length}

+
+ + {/* Applicants List */} +
+ {statusApplicants.length === 0 ? ( +
+ Drag applicants here +
+ ) : ( + statusApplicants.map((app) => ( +
handleDragStart(app.id)} + className="p-3 bg-black/40 rounded-lg border border-gray-500/10 cursor-move hover:border-gray-500/30 hover:bg-black/50 transition space-y-2" + > + {/* Applicant Info */} +
+ {app.user?.avatar_url && ( + {app.user.name} + )} +
+

+ {app.user?.name || "Unknown"} +

+ {app.opportunity?.title && ( +

+ {app.opportunity.title} +

+ )} +
+
+ + {/* Rating */} + {app.rating && ( +
+
+ {Array.from({ length: 5 }).map((_, i) => ( + + ★ + + ))} +
+
+ )} + + {/* Notes */} + {app.notes && ( +

+ "{app.notes}" +

+ )} + + {/* Actions */} +
+ {onViewProfile && ( + + )} + {onMessage && ( + + )} +
+ + {/* Applied Date */} + {app.applied_at && ( +

+ {new Date(app.applied_at).toLocaleDateString()} +

+ )} +
+ )) + )} +
+
+ ); + })} +
+ )} +
+
+ ); +} + +export default ApplicantTrackerWidget;