From 73448a5a68f14a6a0c28e1a7fb070a2357bf9ffa Mon Sep 17 00:00:00 2001 From: Hunter Shierman Date: Sat, 7 Mar 2026 11:57:30 -0500 Subject: [PATCH 1/2] feat: add add interviewer page - allow admin to add other interviewers by sending them an email - basic form page to be used by the backend --- .../admin/components/AddInterviewers.tsx | 240 ++++++++++++++++++ frontend/src/features/dashboard/Dashboard.tsx | 10 +- .../features/dashboard/components/Sidebar.tsx | 16 +- 3 files changed, 264 insertions(+), 2 deletions(-) create mode 100644 frontend/src/features/admin/components/AddInterviewers.tsx diff --git a/frontend/src/features/admin/components/AddInterviewers.tsx b/frontend/src/features/admin/components/AddInterviewers.tsx new file mode 100644 index 0000000..0d17ae7 --- /dev/null +++ b/frontend/src/features/admin/components/AddInterviewers.tsx @@ -0,0 +1,240 @@ +import { useState } from "react"; + +interface AddInterviewersProps { + departments?: string[]; +} + +export default function AddInterviewers({ departments = ["Engineering", "Design", "Marketing", "Product", "Operations"] }: AddInterviewersProps) { + const [activeTab, setActiveTab] = useState<"interviewers" | "candidates">("interviewers"); + + // Interviewer form state + const [firstName, setFirstName] = useState(""); + const [lastName, setLastName] = useState(""); + const [email, setEmail] = useState(""); + const [department, setDepartment] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + const [successMessage, setSuccessMessage] = useState(""); + const [errorMessage, setErrorMessage] = useState(""); + + // Candidate form state + const [candidateFirstName, setCandidateFirstName] = useState(""); + const [candidateLastName, setCandidateLastName] = useState(""); + const [candidateEmail, setCandidateEmail] = useState(""); + const [candidateDepartment, setCandidateDepartment] = useState(""); + const [isCandidateSubmitting, setIsCandidateSubmitting] = useState(false); + const [candidateSuccessMessage, setCandidateSuccessMessage] = useState(""); + const [candidateErrorMessage, setCandidateErrorMessage] = useState(""); + + const handleSendInvite = async () => { + setSuccessMessage(""); + setErrorMessage(""); + + if (!firstName || !lastName || !email || !department) { + setErrorMessage("Please fill in all fields."); + return; + } + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + setErrorMessage("Please enter a valid email address."); + return; + } + + try { + setIsSubmitting(true); + // TODO: Replace with actual API call + await new Promise((resolve) => setTimeout(resolve, 800)); + + setSuccessMessage(`Invite sent to ${email} successfully.`); + setFirstName(""); + setLastName(""); + setEmail(""); + setDepartment(""); + } catch (err) { + setErrorMessage("Failed to send invite. Please try again."); + console.log(err); + } finally { + setIsSubmitting(false); + } + }; + + const handleSendCandidateInvite = async () => { + setCandidateSuccessMessage(""); + setCandidateErrorMessage(""); + + if (!candidateFirstName || !candidateLastName || !candidateEmail || !candidateDepartment) { + setCandidateErrorMessage("Please fill in all fields."); + return; + } + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(candidateEmail)) { + setCandidateErrorMessage("Please enter a valid email address."); + return; + } + + try { + setIsCandidateSubmitting(true); + // TODO: Replace with actual API call + await new Promise((resolve) => setTimeout(resolve, 800)); + + setCandidateSuccessMessage(`Invite sent to ${candidateEmail} successfully.`); + setCandidateFirstName(""); + setCandidateLastName(""); + setCandidateEmail(""); + setCandidateDepartment(""); + } catch (err) { + setCandidateErrorMessage("Failed to send invite. Please try again."); + console.log(err); + } finally { + setIsCandidateSubmitting(false); + } + }; + + const formUI = ( + values: { firstName: string; lastName: string; email: string; department: string }, + handlers: { + setFirstName: (v: string) => void; + setLastName: (v: string) => void; + setEmail: (v: string) => void; + setDepartment: (v: string) => void; + }, + isSubmitting: boolean, + successMessage: string, + errorMessage: string, + handleSubmit: () => void + ) => ( +
+

Add Information

+ + {successMessage && ( +
+ {successMessage} +
+ )} + {errorMessage && ( +
+ {errorMessage} +
+ )} + +
+ + handlers.setFirstName(e.target.value)} + className="w-full px-4 py-3 bg-gray-100 rounded-md text-base text-gray-800 focus:outline-none focus:ring-2 focus:ring-blue-300" + /> +
+ +
+ + handlers.setLastName(e.target.value)} + className="w-full px-4 py-3 bg-gray-100 rounded-md text-base text-gray-800 focus:outline-none focus:ring-2 focus:ring-blue-300" + /> +
+ +
+ + handlers.setEmail(e.target.value)} + className="w-full px-4 py-3 bg-gray-100 rounded-md text-base text-gray-800 focus:outline-none focus:ring-2 focus:ring-blue-300" + /> +
+ +
+ + +
+ +
+ +
+
+ ); + return ( + <> + +
+

Add Team Members

+

+ Send invitations for interviewers and candidates to join your team! +

+ + {/* Tabs */} +
+
+ + +
+
+ +
+ {activeTab === "interviewers" + ? formUI( + { firstName, lastName, email, department }, + { setFirstName, setLastName, setEmail, setDepartment }, + isSubmitting, + successMessage, + errorMessage, + handleSendInvite + ) + : formUI( + { firstName: candidateFirstName, lastName: candidateLastName, email: candidateEmail, department: candidateDepartment }, + { setFirstName: setCandidateFirstName, setLastName: setCandidateLastName, setEmail: setCandidateEmail, setDepartment: setCandidateDepartment }, + isCandidateSubmitting, + candidateSuccessMessage, + candidateErrorMessage, + handleSendCandidateInvite + )} +
+
+ + ); +} \ No newline at end of file diff --git a/frontend/src/features/dashboard/Dashboard.tsx b/frontend/src/features/dashboard/Dashboard.tsx index b8dffb8..5cf760d 100644 --- a/frontend/src/features/dashboard/Dashboard.tsx +++ b/frontend/src/features/dashboard/Dashboard.tsx @@ -7,9 +7,15 @@ import { Availability, } from "./components"; import { AdminSettings } from "../admin/components"; +import AddInterviewers from "../admin/components/AddInterviewers"; +import { useContext } from "react"; +import { AuthContext } from "@/features/auth/services/AuthContext"; +import { UserRole } from "@/features/auth/types/authTypes"; export default function Dashboard() { const [activePage, setActivePage] = useState("dashboard"); + const auth = useContext(AuthContext); + const isCandidate = auth?.user?.role === UserRole.CANDIDATE; return (
@@ -30,9 +36,11 @@ export default function Dashboard() { ) : activePage === "availability" ? ( + ) : activePage === "add-interviewers" && !isCandidate ? ( + ) : null}
); -} +} \ No newline at end of file diff --git a/frontend/src/features/dashboard/components/Sidebar.tsx b/frontend/src/features/dashboard/components/Sidebar.tsx index 6dffbdd..2892dbb 100644 --- a/frontend/src/features/dashboard/components/Sidebar.tsx +++ b/frontend/src/features/dashboard/components/Sidebar.tsx @@ -1,4 +1,4 @@ -import { CalendarDays, LayoutDashboard, Users, Settings, Plus } from "lucide-react"; +import { CalendarDays, LayoutDashboard, Users, Settings, Plus, UserPlus } from "lucide-react"; import { useContext } from "react"; import { AuthContext } from "@/features/auth/services/AuthContext"; import { UserRole } from "@/features/auth/types/authTypes"; @@ -80,11 +80,25 @@ export default function DashboardSidebar({ activePage = "dashboard", onPageChang Admin Settings + + {/* Schedule Interviews Button */} + + )} From ec6aa6869f1222db041b536e33bc2552ad065e6a Mon Sep 17 00:00:00 2001 From: Hunter Shierman Date: Sat, 7 Mar 2026 13:07:18 -0500 Subject: [PATCH 2/2] feat: add candidate availability page with detail view - Add CandidateAvailability component with search and status filter tabs - Add CandidateCalendar detail view accessible by clicking a candidate card - Add candidateTypes.ts to share Candidate interface and statusConfig - Add AddInterviewers component with tabbed form for interviewers and candidates - Update AdminSettings with larger fonts and spacing to match design system - Update DashboardSidebar with new nav items for candidate availability and add interviewers - Update Dashboard to conditionally render new pages based on activePage and role --- .../admin/components/AddInterviewers.tsx | 2 +- .../admin/components/AdminSettings.tsx | 96 +++-- .../components/CandidateAvailability.tsx | 388 ++++++++++++++++++ .../admin/components/CandidateCalendar.tsx | 122 ++++++ frontend/src/features/admin/types/index.ts | 27 +- frontend/src/features/dashboard/Dashboard.tsx | 3 + .../features/dashboard/components/Sidebar.tsx | 14 +- 7 files changed, 596 insertions(+), 56 deletions(-) create mode 100644 frontend/src/features/admin/components/CandidateAvailability.tsx create mode 100644 frontend/src/features/admin/components/CandidateCalendar.tsx diff --git a/frontend/src/features/admin/components/AddInterviewers.tsx b/frontend/src/features/admin/components/AddInterviewers.tsx index 0d17ae7..41aa189 100644 --- a/frontend/src/features/admin/components/AddInterviewers.tsx +++ b/frontend/src/features/admin/components/AddInterviewers.tsx @@ -183,7 +183,7 @@ export default function AddInterviewers({ departments = ["Engineering", "Design" to { opacity: 1; transform: translateY(0); } } `} -
+

Add Team Members

Send invitations for interviewers and candidates to join your team! diff --git a/frontend/src/features/admin/components/AdminSettings.tsx b/frontend/src/features/admin/components/AdminSettings.tsx index 2a357a0..5212c2d 100644 --- a/frontend/src/features/admin/components/AdminSettings.tsx +++ b/frontend/src/features/admin/components/AdminSettings.tsx @@ -37,7 +37,6 @@ export default function AdminSettings() { const [interviewersPerInterviewee, setInterviewersPerInterviewee] = useState(2); const [maxInterviewsPerDay, setMaxInterviewsPerDay] = useState(5); - // Input visibility states const [showRoleInput, setShowRoleInput] = useState(false); const [newRoleName, setNewRoleName] = useState(""); const [showDeptInput, setShowDeptInput] = useState(false); @@ -75,9 +74,7 @@ export default function AdminSettings() { const inviteModerator = () => { if (newModeratorEmail.trim()) { - // This would typically make an API call to send an invite email console.log("Inviting moderator:", newModeratorEmail); - // For demo purposes, add them to the list setModerators([ ...moderators, { @@ -93,7 +90,6 @@ export default function AdminSettings() { }; const handleSave = () => { - // This would typically make an API call to save all settings console.log("Saving settings:", { moderators, roles, @@ -105,35 +101,35 @@ export default function AdminSettings() { }; return ( -

+
{/* Header */}

Admin Settings

{/* Moderators Section */} -
-
-

Team Moderators

+
+
+

Team Moderators

{showModeratorInput && ( -
+
setNewModeratorEmail(e.target.value)} placeholder="Enter moderator email" - className="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" + className="flex-1 px-4 py-3 text-base border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" onKeyPress={(e) => e.key === "Enter" && inviteModerator()} /> @@ -142,7 +138,7 @@ export default function AdminSettings() { setShowModeratorInput(false); setNewModeratorEmail(""); }} - className="px-4 py-2 bg-gray-300 text-gray-700 text-sm rounded-md hover:bg-gray-400 transition-colors" + className="px-5 py-3 bg-gray-300 text-gray-700 text-base rounded-md hover:bg-gray-400 transition-colors" > Cancel @@ -153,12 +149,12 @@ export default function AdminSettings() { {moderators.map((moderator) => (
{moderator.name} {moderator.isMain && ( - (main) + (main) )} {!moderator.isMain && ( @@ -166,7 +162,7 @@ export default function AdminSettings() { onClick={() => removeModerator(moderator.id)} className="text-gray-400 hover:text-red-500 transition-colors ml-1" > - + )}
@@ -175,13 +171,13 @@ export default function AdminSettings() {
{/* Auto Scheduling Preferences Section */} -
-

+
+

Auto Scheduling Preferences

-
+
-
-
@@ -218,31 +214,31 @@ export default function AdminSettings() { {/* Two Column Layout for Roles and Departments */}
{/* Manage Roles Section */} -
-
-

Interview Roles

+
+
+

Interview Roles

-

Roles available for interviewees

+

Roles available for interviewees

{showRoleInput && ( -
+
setNewRoleName(e.target.value)} placeholder="Enter role name" - className="flex-1 px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" + className="flex-1 px-4 py-3 text-base border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" onKeyPress={(e) => e.key === "Enter" && addRole()} /> @@ -251,7 +247,7 @@ export default function AdminSettings() { setShowRoleInput(false); setNewRoleName(""); }} - className="px-3 py-1.5 bg-gray-300 text-gray-700 text-sm rounded-md hover:bg-gray-400" + className="px-4 py-3 bg-gray-300 text-gray-700 text-base rounded-md hover:bg-gray-400" > Cancel @@ -262,14 +258,14 @@ export default function AdminSettings() { {roles.map((role) => (
{role.name}
))} @@ -277,31 +273,31 @@ export default function AdminSettings() {
{/* Manage Departments Section */} -
-
-

Departments

+
+
+

Departments

-

Departments for interviewers

+

Departments for interviewers

{showDeptInput && ( -
+
setNewDeptName(e.target.value)} placeholder="Enter department name" - className="flex-1 px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" + className="flex-1 px-4 py-3 text-base border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" onKeyPress={(e) => e.key === "Enter" && addDepartment()} /> @@ -310,7 +306,7 @@ export default function AdminSettings() { setShowDeptInput(false); setNewDeptName(""); }} - className="px-3 py-1.5 bg-gray-300 text-gray-700 text-sm rounded-md hover:bg-gray-400" + className="px-4 py-3 bg-gray-300 text-gray-700 text-base rounded-md hover:bg-gray-400" > Cancel @@ -321,14 +317,14 @@ export default function AdminSettings() { {departments.map((dept) => (
{dept.name}
))} @@ -337,14 +333,14 @@ export default function AdminSettings() {
{/* Save Button */} -
+
); -} +} \ No newline at end of file diff --git a/frontend/src/features/admin/components/CandidateAvailability.tsx b/frontend/src/features/admin/components/CandidateAvailability.tsx new file mode 100644 index 0000000..9bc47c0 --- /dev/null +++ b/frontend/src/features/admin/components/CandidateAvailability.tsx @@ -0,0 +1,388 @@ +import { useState } from "react"; +import { Search, User, ChevronRight } from "lucide-react"; +import CandidateCalendar from "./CandidateCalendar"; +import { Candidate, statusConfig } from "../types/index"; + +const mockCandidates: Candidate[] = [ + { + id: "1", + name: "Aisha Patel", + email: "aisha.patel@email.com", + role: "Software Engineer", + department: "Engineering", + status: "submitted", + availability: [ + { + day: "Mon", date: "Jan 13", + slots: [ + { time: "9:00 AM", available: true }, + { time: "10:00 AM", available: true }, + { time: "11:00 AM", available: false }, + { time: "1:00 PM", available: true }, + { time: "2:00 PM", available: false }, + { time: "3:00 PM", available: true }, + ], + }, + { + day: "Tue", date: "Jan 14", + slots: [ + { time: "9:00 AM", available: false }, + { time: "10:00 AM", available: true }, + { time: "11:00 AM", available: true }, + { time: "1:00 PM", available: false }, + { time: "2:00 PM", available: true }, + { time: "3:00 PM", available: true }, + ], + }, + { + day: "Wed", date: "Jan 15", + slots: [ + { time: "9:00 AM", available: true }, + { time: "10:00 AM", available: false }, + { time: "11:00 AM", available: false }, + { time: "1:00 PM", available: true }, + { time: "2:00 PM", available: true }, + { time: "3:00 PM", available: false }, + ], + }, + { + day: "Thu", date: "Jan 16", + slots: [ + { time: "9:00 AM", available: true }, + { time: "10:00 AM", available: true }, + { time: "11:00 AM", available: true }, + { time: "1:00 PM", available: false }, + { time: "2:00 PM", available: false }, + { time: "3:00 PM", available: true }, + ], + }, + { + day: "Fri", date: "Jan 17", + slots: [ + { time: "9:00 AM", available: false }, + { time: "10:00 AM", available: false }, + { time: "11:00 AM", available: true }, + { time: "1:00 PM", available: true }, + { time: "2:00 PM", available: true }, + { time: "3:00 PM", available: false }, + ], + }, + ], + }, + { + id: "2", + name: "Marcus Chen", + email: "marcus.chen@email.com", + role: "Academic Coordinator", + department: "Academics", + status: "submitted", + availability: [ + { + day: "Mon", date: "Jan 13", + slots: [ + { time: "9:00 AM", available: false }, + { time: "10:00 AM", available: false }, + { time: "11:00 AM", available: true }, + { time: "1:00 PM", available: true }, + { time: "2:00 PM", available: true }, + { time: "3:00 PM", available: false }, + ], + }, + { + day: "Tue", date: "Jan 14", + slots: [ + { time: "9:00 AM", available: true }, + { time: "10:00 AM", available: true }, + { time: "11:00 AM", available: false }, + { time: "1:00 PM", available: true }, + { time: "2:00 PM", available: false }, + { time: "3:00 PM", available: true }, + ], + }, + { + day: "Wed", date: "Jan 15", + slots: [ + { time: "9:00 AM", available: true }, + { time: "10:00 AM", available: true }, + { time: "11:00 AM", available: true }, + { time: "1:00 PM", available: false }, + { time: "2:00 PM", available: false }, + { time: "3:00 PM", available: false }, + ], + }, + { + day: "Thu", date: "Jan 16", + slots: [ + { time: "9:00 AM", available: false }, + { time: "10:00 AM", available: true }, + { time: "11:00 AM", available: false }, + { time: "1:00 PM", available: true }, + { time: "2:00 PM", available: true }, + { time: "3:00 PM", available: true }, + ], + }, + { + day: "Fri", date: "Jan 17", + slots: [ + { time: "9:00 AM", available: true }, + { time: "10:00 AM", available: false }, + { time: "11:00 AM", available: true }, + { time: "1:00 PM", available: false }, + { time: "2:00 PM", available: true }, + { time: "3:00 PM", available: true }, + ], + }, + ], + }, + { + id: "3", + name: "Sofia Reyes", + email: "sofia.reyes@email.com", + role: "Software Engineer", + department: "Engineering", + status: "pending", + availability: [], + }, + { + id: "4", + name: "James Okafor", + email: "james.okafor@email.com", + role: "Software Engineer", + department: "Engineering", + status: "interviewed", + availability: [ + { + day: "Mon", date: "Jan 13", + slots: [ + { time: "9:00 AM", available: true }, + { time: "10:00 AM", available: true }, + { time: "11:00 AM", available: true }, + { time: "1:00 PM", available: true }, + { time: "2:00 PM", available: false }, + { time: "3:00 PM", available: false }, + ], + }, + { + day: "Tue", date: "Jan 14", + slots: [ + { time: "9:00 AM", available: false }, + { time: "10:00 AM", available: false }, + { time: "11:00 AM", available: true }, + { time: "1:00 PM", available: true }, + { time: "2:00 PM", available: true }, + { time: "3:00 PM", available: false }, + ], + }, + { + day: "Wed", date: "Jan 15", + slots: [ + { time: "9:00 AM", available: false }, + { time: "10:00 AM", available: true }, + { time: "11:00 AM", available: false }, + { time: "1:00 PM", available: false }, + { time: "2:00 PM", available: true }, + { time: "3:00 PM", available: true }, + ], + }, + { + day: "Thu", date: "Jan 16", + slots: [ + { time: "9:00 AM", available: true }, + { time: "10:00 AM", available: false }, + { time: "11:00 AM", available: true }, + { time: "1:00 PM", available: true }, + { time: "2:00 PM", available: false }, + { time: "3:00 PM", available: true }, + ], + }, + { + day: "Fri", date: "Jan 17", + slots: [ + { time: "9:00 AM", available: true }, + { time: "10:00 AM", available: true }, + { time: "11:00 AM", available: false }, + { time: "1:00 PM", available: false }, + { time: "2:00 PM", available: false }, + { time: "3:00 PM", available: true }, + ], + }, + ], + }, + { + id: "5", + name: "Priya Nair", + email: "priya.nair@email.com", + role: "Academic Coordinator", + department: "Academics", + status: "submitted", + availability: [ + { + day: "Mon", date: "Jan 13", + slots: [ + { time: "9:00 AM", available: true }, + { time: "10:00 AM", available: false }, + { time: "11:00 AM", available: true }, + { time: "1:00 PM", available: false }, + { time: "2:00 PM", available: true }, + { time: "3:00 PM", available: true }, + ], + }, + { + day: "Tue", date: "Jan 14", + slots: [ + { time: "9:00 AM", available: true }, + { time: "10:00 AM", available: true }, + { time: "11:00 AM", available: true }, + { time: "1:00 PM", available: true }, + { time: "2:00 PM", available: false }, + { time: "3:00 PM", available: false }, + ], + }, + { + day: "Wed", date: "Jan 15", + slots: [ + { time: "9:00 AM", available: false }, + { time: "10:00 AM", available: false }, + { time: "11:00 AM", available: false }, + { time: "1:00 PM", available: true }, + { time: "2:00 PM", available: true }, + { time: "3:00 PM", available: true }, + ], + }, + { + day: "Thu", date: "Jan 16", + slots: [ + { time: "9:00 AM", available: true }, + { time: "10:00 AM", available: true }, + { time: "11:00 AM", available: false }, + { time: "1:00 PM", available: false }, + { time: "2:00 PM", available: true }, + { time: "3:00 PM", available: false }, + ], + }, + { + day: "Fri", date: "Jan 17", + slots: [ + { time: "9:00 AM", available: false }, + { time: "10:00 AM", available: true }, + { time: "11:00 AM", available: true }, + { time: "1:00 PM", available: true }, + { time: "2:00 PM", available: false }, + { time: "3:00 PM", available: true }, + ], + }, + ], + }, +]; + +export default function CandidateAvailability() { + const [selectedCandidate, setSelectedCandidate] = useState(null); + const [search, setSearch] = useState(""); + const [filterStatus, setFilterStatus] = useState("all"); + const [viewingAvailability, setViewingAvailability] = useState(false); + + const filtered = mockCandidates.filter((c) => { + const matchesSearch = + c.name.toLowerCase().includes(search.toLowerCase()) || + c.email.toLowerCase().includes(search.toLowerCase()) || + c.role.toLowerCase().includes(search.toLowerCase()); + const matchesStatus = filterStatus === "all" || c.status === filterStatus; + return matchesSearch && matchesStatus; + }); + + if (viewingAvailability && selectedCandidate) { + return ( + setViewingAvailability(false)} + /> + ); + } + + return ( +
+ {/* Header */} +
+

Candidate Availability

+

+ {mockCandidates.length} candidates total +

+
+ + {/* Search */} +
+ + setSearch(e.target.value)} + className="w-full pl-9 pr-4 py-3 bg-white border border-gray-200 rounded-lg text-base text-gray-800 focus:outline-none focus:ring-2 focus:ring-blue-300" + /> +
+ + {/* Filter Tabs */} +
+ {["all", "submitted", "pending", "interviewed"].map((status) => ( + + ))} +
+ + {/* Candidate Cards */} +
+ {filtered.map((candidate) => ( + + ))} + + {filtered.length === 0 && ( +
+ No candidates found +
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/features/admin/components/CandidateCalendar.tsx b/frontend/src/features/admin/components/CandidateCalendar.tsx new file mode 100644 index 0000000..b932d4b --- /dev/null +++ b/frontend/src/features/admin/components/CandidateCalendar.tsx @@ -0,0 +1,122 @@ +import { ArrowLeft, Clock, CheckCircle, XCircle, User } from "lucide-react"; +import { Candidate, statusConfig } from "../types/index"; + +interface CandidateDetailProps { + candidate: Candidate; + onBack: () => void; +} + +export default function CandidateDetail({ candidate, onBack }: CandidateDetailProps) { + const totalAvailable = candidate.availability.reduce( + (acc, day) => acc + day.slots.filter((s) => s.available).length, + 0 + ); + + return ( +
+ {/* Back Button */} + + + {/* Candidate Header */} +
+
+
+
+ +
+
+

{candidate.name}

+

{candidate.email}

+

{candidate.role} · {candidate.department}

+
+
+
+
+

{totalAvailable}

+

Open slots

+
+
+

{candidate.availability.length}

+

Days submitted

+
+ + {statusConfig[candidate.status].label} + +
+
+
+ + {/* Availability Grid */} + {candidate.availability.length > 0 ? ( +
+
+ +

Weekly Availability

+
+ + Available + + + Unavailable + +
+
+ +
+ + + + + {candidate.availability.map((day) => ( + + ))} + + + + {candidate.availability[0].slots.map((_, slotIndex) => ( + + + {candidate.availability.map((day) => ( + + ))} + + ))} + +
Time + {day.day} + {day.date} +
+ {candidate.availability[0].slots[slotIndex].time} + + {day.slots[slotIndex].available ? ( + + + + ) : ( + + + + )} +
+
+
+ ) : ( +
+
+ +
+

No availability submitted

+

+ {candidate.name} hasn't submitted their availability yet. +

+
+ )} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/features/admin/types/index.ts b/frontend/src/features/admin/types/index.ts index ab0c014..b6b97e9 100644 --- a/frontend/src/features/admin/types/index.ts +++ b/frontend/src/features/admin/types/index.ts @@ -1 +1,26 @@ -// \ No newline at end of file +export interface TimeSlot { + time: string; + available: boolean; +} + +export interface DayAvailability { + day: string; + date: string; + slots: TimeSlot[]; +} + +export interface Candidate { + id: string; + name: string; + email: string; + role: string; + department: string; + status: "pending" | "submitted" | "interviewed"; + availability: DayAvailability[]; +} + +export const statusConfig = { + submitted: { label: "Submitted", color: "bg-green-100 text-green-700" }, + pending: { label: "Pending", color: "bg-yellow-100 text-yellow-700" }, + interviewed: { label: "Interviewed", color: "bg-blue-100 text-blue-700" }, +}; \ No newline at end of file diff --git a/frontend/src/features/dashboard/Dashboard.tsx b/frontend/src/features/dashboard/Dashboard.tsx index 5cf760d..b535369 100644 --- a/frontend/src/features/dashboard/Dashboard.tsx +++ b/frontend/src/features/dashboard/Dashboard.tsx @@ -8,6 +8,7 @@ import { } from "./components"; import { AdminSettings } from "../admin/components"; import AddInterviewers from "../admin/components/AddInterviewers"; +import CandidateAvailability from "../admin/components/CandidateAvailability"; import { useContext } from "react"; import { AuthContext } from "@/features/auth/services/AuthContext"; import { UserRole } from "@/features/auth/types/authTypes"; @@ -38,6 +39,8 @@ export default function Dashboard() { ) : activePage === "add-interviewers" && !isCandidate ? ( + ) : activePage === "candidate-availability" && !isCandidate ? ( + ) : null}
diff --git a/frontend/src/features/dashboard/components/Sidebar.tsx b/frontend/src/features/dashboard/components/Sidebar.tsx index 2892dbb..7bacc64 100644 --- a/frontend/src/features/dashboard/components/Sidebar.tsx +++ b/frontend/src/features/dashboard/components/Sidebar.tsx @@ -61,13 +61,19 @@ export default function DashboardSidebar({ activePage = "dashboard", onPageChang Team Availability - onPageChange?.("candidate-availability")} + className={`w-full flex items-center text-left space-x-3 p-3 rounded-lg transition-colors ${ + activePage === "candidate-availability" + ? "bg-blue-100 text-blue-700 font-semibold" + : "text-gray-600 hover:bg-gray-200" + }`} > Candidate Availability - + +