diff --git a/frontend/src/components/InterviewOfferStaging/ApplicantRow/index.tsx b/frontend/src/components/InterviewOfferStaging/ApplicantRow/index.tsx new file mode 100644 index 000000000..c88fb836e --- /dev/null +++ b/frontend/src/components/InterviewOfferStaging/ApplicantRow/index.tsx @@ -0,0 +1,65 @@ +import { EmailOutlined } from "@mui/icons-material"; +import tw from "twin.macro"; + +import RoleSelector from "../RoleSelector"; +import StatusBadge from "../StatusBadge"; + +import type { Applicant } from "pages/interview_offer_staging_test/sampleData"; +import type React from "react"; + +interface ApplicantRowProps { + applicant: Applicant; + isSelected: boolean; + onSelect: (id: number) => void; + onRoleChange: (id: number, role: string) => void; + onSendEmail: (applicant: Applicant) => void; + roleOptions: string[]; +} + +const ApplicantRow: React.FC = ({ + applicant, + isSelected, + onSelect, + onRoleChange, + onSendEmail, + roleOptions, +}) => ( + + + onSelect(applicant.id)} + css={tw`h-4 w-4 shrink-0 rounded-sm border border-gray-300 focus-visible:ring-2 focus-visible:ring-blue-500`} + /> + + +
{applicant.name}
+ + +
{applicant.zid}
+ + + + + + onRoleChange(applicant.id, role)} + roleOptions={roleOptions} + /> + + + + + +); + +export default ApplicantRow; diff --git a/frontend/src/components/InterviewOfferStaging/ApplicantTable/index.tsx b/frontend/src/components/InterviewOfferStaging/ApplicantTable/index.tsx new file mode 100644 index 000000000..6f2417066 --- /dev/null +++ b/frontend/src/components/InterviewOfferStaging/ApplicantTable/index.tsx @@ -0,0 +1,125 @@ +import React, { useMemo, useState } from "react"; +import tw from "twin.macro"; + +import { + ROLE_OPTIONS, + SAMPLE_APPLICANTS, +} from "pages/interview_offer_staging_test/sampleData"; + +import ApplicantRow from "../ApplicantRow"; +import TableControls from "../TableControls"; + +import type { Applicant } from "pages/interview_offer_staging_test/sampleData"; + +const ApplicantTable: React.FC = () => { + const [applicants, setApplicants] = useState(SAMPLE_APPLICANTS); + const [selectedApplicants, setSelectedApplicants] = useState>( + new Set() + ); + const [statusFilter, setStatusFilter] = useState("all"); + + const filteredApplicants = useMemo(() => { + if (statusFilter === "all") return applicants; + return applicants.filter((a) => a.status === statusFilter); + }, [applicants, statusFilter]); + + const handleRoleChange = (id: number, role: string) => + setApplicants((prev) => + prev.map((a) => (a.id === id ? { ...a, role } : a)) + ); + + const handleSelectApplicant = (id: number) => { + const next = new Set(selectedApplicants); + next.has(id) ? next.delete(id) : next.add(id); + setSelectedApplicants(next); + }; + + const handleSelectAll = () => { + if (selectedApplicants.size === filteredApplicants.length) { + setSelectedApplicants(new Set()); + } else { + setSelectedApplicants(new Set(filteredApplicants.map((a) => a.id))); + } + }; + + // Stubs + const handleSendEmail = (_: Applicant) => {}; + const handleSendSelected = () => {}; + + return ( +
+ {/* Header */} +
+

+ Interview Offer Staging +

+ +
+ + {/* Table wrapper now handles scrolling */} +
+ + + + + + + + + + + + + {["Name", "zID", "Status", "Role", "Actions"].map((h) => ( + + ))} + + + + {filteredApplicants.map((app) => ( + + ))} + +
+ 0 && + selectedApplicants.size === filteredApplicants.length + } + onChange={handleSelectAll} + css={tw`h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500`} + /> + + {h} +
+ + {filteredApplicants.length === 0 && ( +
+ No applicants found for the selected status. +
+ )} +
+
+ ); +}; + +export default ApplicantTable; diff --git a/frontend/src/components/InterviewOfferStaging/RoleSelector/index.tsx b/frontend/src/components/InterviewOfferStaging/RoleSelector/index.tsx new file mode 100644 index 000000000..a378f4a61 --- /dev/null +++ b/frontend/src/components/InterviewOfferStaging/RoleSelector/index.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import tw from "twin.macro"; + +interface RoleSelectorProps { + value: string; + onChange: (value: string) => void; + roleOptions: string[]; +} + +const RoleSelector: React.FC = ({ + value, + onChange, + roleOptions, +}) => ( +
+ + + + +
+); + +export default RoleSelector; diff --git a/frontend/src/components/InterviewOfferStaging/StatusBadge/index.tsx b/frontend/src/components/InterviewOfferStaging/StatusBadge/index.tsx new file mode 100644 index 000000000..26b349e3c --- /dev/null +++ b/frontend/src/components/InterviewOfferStaging/StatusBadge/index.tsx @@ -0,0 +1,13 @@ +import type { Status } from "pages/interview_offer_staging_test/sampleData"; +import type React from "react"; + +interface StatusBadgeProps { + status: Status; +} + +const StatusBadge: React.FC = ({ status }) => { + const label = status.charAt(0).toUpperCase() + status.slice(1); + return {label}; +}; + +export default StatusBadge; diff --git a/frontend/src/components/InterviewOfferStaging/TableControls/index.tsx b/frontend/src/components/InterviewOfferStaging/TableControls/index.tsx new file mode 100644 index 000000000..8d8a86604 --- /dev/null +++ b/frontend/src/components/InterviewOfferStaging/TableControls/index.tsx @@ -0,0 +1,71 @@ +import { EmailOutlined, FilterAlt } from "@mui/icons-material"; +import tw from "twin.macro"; + +import { STATUS_OPTIONS } from "pages/interview_offer_staging_test/sampleData"; + +import type React from "react"; + +interface TableControlsProps { + statusFilter: string; + onStatusFilterChange: (status: string) => void; + selectedCount: number; + totalCount: number; + onSelectAll: () => void; + onSendSelected: () => void; +} + +const TableControls: React.FC = ({ + statusFilter, + onStatusFilterChange, + selectedCount, + totalCount, + onSelectAll, + onSendSelected, +}) => ( +
+ {/* Status Filter */} +
+ + + + +
+ +
+ + + +
+
+); + +export default TableControls; diff --git a/frontend/src/pages/interview_offer_staging_test/index.tsx b/frontend/src/pages/interview_offer_staging_test/index.tsx new file mode 100644 index 000000000..919d00124 --- /dev/null +++ b/frontend/src/pages/interview_offer_staging_test/index.tsx @@ -0,0 +1,11 @@ +import ApplicantTable from "components/InterviewOfferStaging/ApplicantTable"; + +const InterviewOfferStagingTest = () => { + return ( +
+ +
+ ); +}; + +export default InterviewOfferStagingTest; diff --git a/frontend/src/pages/interview_offer_staging_test/sampleData.ts b/frontend/src/pages/interview_offer_staging_test/sampleData.ts new file mode 100644 index 000000000..f317cff7a --- /dev/null +++ b/frontend/src/pages/interview_offer_staging_test/sampleData.ts @@ -0,0 +1,41 @@ +export type Status = "pending" | "successful" | "rejected"; + +export interface Applicant { + id: number; + name: string; + zid: string; + status: Status; + role: string; +} + +export interface StatusOption { + value: string; + label: string; +} + +export const ROLE_OPTIONS: string[] = [ + "Software Engineer", + "Product Manager", + "Designer", + "Data Scientist", + "Marketing Specialist", + "Sales Representative", +]; + +export const SAMPLE_APPLICANTS: Applicant[] = [ + { id: 1, name: "Alice Johnson", zid: "z1234567", status: "pending", role: "" }, + { id: 2, name: "Bob Smith", zid: "z2345678", status: "successful", role: "Software Engineer" }, + { id: 3, name: "Carol Brown", zid: "z3456789", status: "rejected", role: "" }, + { id: 4, name: "David Wilson", zid: "z4567890", status: "pending", role: "" }, + { id: 5, name: "Emma Davis", zid: "z5678901", status: "successful", role: "Product Manager" }, + { id: 6, name: "Frank Miller", zid: "z6789012", status: "pending", role: "" }, + { id: 7, name: "Grace Lee", zid: "z7890123", status: "rejected", role: "" }, + { id: 8, name: "Henry Taylor", zid: "z8901234", status: "successful", role: "Designer" }, +]; + +export const STATUS_OPTIONS: StatusOption[] = [ + { value: "all", label: "All Statuses" }, + { value: "pending", label: "Pending" }, + { value: "successful", label: "Successful" }, + { value: "rejected", label: "Rejected" } +]; diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index ecf361176..19f145553 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -14,6 +14,9 @@ const Marking = lazy(() => import("./pages/admin/review/marking")); const Rankings = lazy(() => import("./pages/admin/review/rankings")); const Review = lazy(() => import("./pages/admin/review")); const SignupPage = lazy(() => import("./pages/signup")); +const InterviewOfferStagingTest = lazy( + () => import("./pages/interview_offer_staging_test") +); const routes = [ } />, @@ -41,6 +44,11 @@ const routes = [ path="/application/:campaignId" element={} />, + } + />, ]; export default routes;