Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -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<ApplicantRowProps> = ({
applicant,
isSelected,
onSelect,
onRoleChange,
onSendEmail,
roleOptions,
}) => (
<tr css={tw`border-b transition-colors hover:bg-gray-50`}>
<td css={tw`px-4 py-2 align-middle`}>
<input
type="checkbox"
checked={isSelected}
onChange={() => 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`}
/>
</td>
<td css={tw`px-4 py-2 align-middle`}>
<div css={tw`font-medium`}>{applicant.name}</div>
</td>
<td css={tw`px-4 py-2 align-middle`}>
<div css={tw`text-gray-500`}>{applicant.zid}</div>
</td>
<td css={tw`px-4 py-2 align-middle`}>
<StatusBadge status={applicant.status} />
</td>
<td css={tw`px-4 py-2 align-middle`}>
<RoleSelector
value={applicant.role}
onChange={(role) => onRoleChange(applicant.id, role)}
roleOptions={roleOptions}
/>
</td>
<td css={tw`px-4 py-2 align-middle`}>
<button
type="button"
onClick={() => onSendEmail(applicant)}
css={tw`inline-flex items-center gap-2 h-8 px-3 rounded-md bg-blue-600 text-white text-sm font-medium transition-colors hover:bg-blue-700`}
>
<EmailOutlined fontSize="small" />
Send
</button>
</td>
</tr>
);

export default ApplicantRow;
125 changes: 125 additions & 0 deletions frontend/src/components/InterviewOfferStaging/ApplicantTable/index.tsx
Original file line number Diff line number Diff line change
@@ -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<Applicant[]>(SAMPLE_APPLICANTS);
const [selectedApplicants, setSelectedApplicants] = useState<Set<number>>(
new Set()
);
const [statusFilter, setStatusFilter] = useState<string>("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 (
<div css={tw`p-6 w-full h-screen flex flex-col`}>
{/* Header */}
<div css={tw`flex-none mb-6`}>
<h1 css={tw`text-3xl font-bold text-gray-900 mb-6`}>
Interview Offer Staging
</h1>
<TableControls
statusFilter={statusFilter}
onStatusFilterChange={setStatusFilter}
selectedCount={selectedApplicants.size}
totalCount={filteredApplicants.length}
onSelectAll={handleSelectAll}
onSendSelected={handleSendSelected}
/>
</div>

{/* Table wrapper now handles scrolling */}
<div css={tw`flex-1 overflow-auto rounded-lg border border-gray-200 bg-white`}>
<table css={tw`w-full table-fixed`}>
<colgroup>
<col css={tw`w-12`} />
<col css={tw`w-48`} />
<col css={tw`w-32`} />
<col css={tw`w-32`} />
<col css={tw`w-48`} />
<col css={tw`w-24`} />
</colgroup>
<thead css={tw`bg-gray-50`}>
<tr>
<th css={tw`px-4 py-2 text-left`}>
<input
type="checkbox"
checked={
filteredApplicants.length > 0 &&
selectedApplicants.size === filteredApplicants.length
}
onChange={handleSelectAll}
css={tw`h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500`}
/>
</th>
{["Name", "zID", "Status", "Role", "Actions"].map((h) => (
<th
key={h}
css={tw`px-4 py-2 text-left text-sm font-medium text-gray-900`}
>
{h}
</th>
))}
</tr>
</thead>
<tbody css={tw`bg-white divide-y divide-gray-200`}>
{filteredApplicants.map((app) => (
<ApplicantRow
key={app.id}
applicant={app}
isSelected={selectedApplicants.has(app.id)}
onSelect={handleSelectApplicant}
onRoleChange={handleRoleChange}
onSendEmail={handleSendEmail}
roleOptions={ROLE_OPTIONS}
/>
))}
</tbody>
</table>

{filteredApplicants.length === 0 && (
<div css={tw`flex h-full items-center justify-center text-gray-500`}>
No applicants found for the selected status.
</div>
)}
</div>
</div>
);
};

export default ApplicantTable;
Original file line number Diff line number Diff line change
@@ -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<RoleSelectorProps> = ({
value,
onChange,
roleOptions,
}) => (
<div css={tw`relative`}>
<select
value={value}
onChange={(e) => onChange(e.target.value)}
css={tw`flex h-10 w-full items-center justify-between rounded-md border border-gray-300 bg-white px-3 py-2 text-sm placeholder:text-gray-500 focus:ring-2 focus:ring-blue-500`}
>
<option value="">Select role...</option>
{roleOptions.map((role) => (
<option key={role} value={role}>
{role}
</option>
))}
</select>

<svg
css={tw`absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 opacity-50 pointer-events-none`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
</svg>
</div>
);

export default RoleSelector;
Original file line number Diff line number Diff line change
@@ -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<StatusBadgeProps> = ({ status }) => {
const label = status.charAt(0).toUpperCase() + status.slice(1);
return <span>{label}</span>;
};

export default StatusBadge;
Original file line number Diff line number Diff line change
@@ -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<TableControlsProps> = ({
statusFilter,
onStatusFilterChange,
selectedCount,
totalCount,
onSelectAll,
onSendSelected,
}) => (
<div css={tw`flex flex-col sm:flex-row gap-4 mb-6`}>
{/* Status Filter */}
<div css={tw`flex items-center gap-2`}>
<svg
css={tw`w-4 h-4 text-gray-500`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<FilterAlt />
</svg>
<select
value={statusFilter}
onChange={(e) => onStatusFilterChange(e.target.value)}
css={tw`flex h-10 w-full items-center justify-between rounded-md border border-gray-300 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:ring-offset-2`}
>
{STATUS_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>

<div css={tw`flex gap-3`}>
<button
type="button"
onClick={onSelectAll}
css={tw`inline-flex items-center gap-2 h-10 px-4 py-2 rounded-md border border-gray-300 bg-white text-sm font-medium transition-colors hover:bg-gray-50`}
>
{selectedCount === totalCount ? "Deselect All" : "Select All"}
</button>

<button
type="button"
onClick={onSendSelected}
disabled={selectedCount === 0}
css={tw`inline-flex items-center gap-2 h-10 px-4 py-2 rounded-md bg-blue-600 text-white text-sm font-medium transition-colors hover:bg-blue-700`}
>
<EmailOutlined fontSize="small" />
Send Selected ({selectedCount})
</button>
</div>
</div>
);

export default TableControls;
11 changes: 11 additions & 0 deletions frontend/src/pages/interview_offer_staging_test/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import ApplicantTable from "components/InterviewOfferStaging/ApplicantTable";

const InterviewOfferStagingTest = () => {
return (
<div className="min-h-screen bg-gray-50">
<ApplicantTable />
</div>
);
};

export default InterviewOfferStagingTest;
41 changes: 41 additions & 0 deletions frontend/src/pages/interview_offer_staging_test/sampleData.ts
Original file line number Diff line number Diff line change
@@ -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" }
];
8 changes: 8 additions & 0 deletions frontend/src/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
<Route key="dashboard" path="/dashboard" element={<DashboardPage />} />,
Expand Down Expand Up @@ -41,6 +44,11 @@ const routes = [
path="/application/:campaignId"
element={<ApplicationPage />}
/>,
<Route
key="InterviewOfferStagingTest"
path="/interview-offer-staging-test"
element={<InterviewOfferStagingTest />}
/>,
];

export default routes;
Loading