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
31 changes: 19 additions & 12 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,32 @@ import { useState } from "react";
import "./App.css";
import PeopleFlow from "./components/PeopleFlow.tsx";
import CampusFilterButtons from "./components/CampusFilterButtons.tsx";
import TeamNodeDrawer from "./components/TeamNodeDrawer.tsx";
import { PathwayProvider } from "./contexts/PathwayProvider.tsx";

function App() {
const [selectedCampusId, setSelectedCampusId] = useState<string | null>("3");

return (
<ReactFlowProvider>
<div className="h-screen w-screen flex flex-col overflow-hidden">
{/* Campus Filter Buttons */}
<CampusFilterButtons
onCampusFilter={setSelectedCampusId}
selectedCampusId={selectedCampusId}
/>
<PathwayProvider>
<ReactFlowProvider>
<div className="h-screen w-screen flex flex-col overflow-hidden">
{/* Campus Filter Buttons */}
<CampusFilterButtons
onCampusFilter={setSelectedCampusId}
selectedCampusId={selectedCampusId}
/>

{/* React Flow Container */}
<div className="flex-1 w-full overflow-hidden">
<PeopleFlow campusFilter={selectedCampusId} />
{/* React Flow Container */}
<div className="flex-1 w-full overflow-hidden">
<PeopleFlow campusFilter={selectedCampusId} />
</div>

{/* Team Node Drawer */}
<TeamNodeDrawer />
</div>
</div>
</ReactFlowProvider>
</ReactFlowProvider>
</PathwayProvider>
);
}

Expand Down
22 changes: 21 additions & 1 deletion src/components/PeopleFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import StaticNode from "./StaticNode";
import { usePeopleFlowData } from "../hooks/usePeopleFlowData";
import { getInitialLayout } from "../utils/layoutUtils";
import { createNodesFromStatuses } from "../utils/nodeUtils";
import { usePathway } from "../hooks/usePathway";
import { selectTeamNode } from "../contexts/pathwayActions";
import type { SelectedTeamNode } from "../contexts/PathwayContext";

// Define custom node types outside component to prevent re-renders
const nodeTypes = {
Expand All @@ -27,6 +30,10 @@ const PeopleFlow = ({ campusFilter }: PeopleFlowProps) => {
const { data, isLoading, error } = usePeopleFlowData(campusFilter);
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const {
state: { selectedTeamNode },
dispatch,
} = usePathway();

// Transform data into React Flow nodes and edges
const flowData = useMemo(() => {
Expand All @@ -43,8 +50,17 @@ const PeopleFlow = ({ campusFilter }: PeopleFlowProps) => {

// Set nodes and edges when data changes
useEffect(() => {
if (selectedTeamNode) {
const teamNode = flowData.nodes.find(
(node) => node.id == selectedTeamNode.id
);
if (teamNode) {
dispatch(selectTeamNode(teamNode as unknown as SelectedTeamNode));
}
}
setNodes(flowData.nodes);
setEdges(flowData.edges);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [flowData.nodes, flowData.edges, setNodes, setEdges]);

if (isLoading) {
Expand All @@ -62,7 +78,11 @@ const PeopleFlow = ({ campusFilter }: PeopleFlowProps) => {
}

return (
<div className="w-full h-full relative">
<div
className={`h-full relative transition-all duration-300 ease-in-out ${
selectedTeamNode ? "w-[calc(100%-320px)]" : "w-full"
}`}
>
<ReactFlow
nodes={nodes}
onNodesChange={onNodesChange}
Expand Down
184 changes: 184 additions & 0 deletions src/components/PeopleList/PeopleFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { useState, useMemo } from "react";
import type { Person } from "../../hooks/usePeopleFlowData";

interface PeopleFilterProps {
people: Person[];
onFilterChange: (filteredPeople: Person[]) => void;
}

function PeopleFilter({ people, onFilterChange }: PeopleFilterProps) {
const [searchTerm, setSearchTerm] = useState("");
const [showOnlyCG, setShowOnlyCG] = useState(false);
const [showOnlyServing, setShowOnlyServing] = useState(false);
const [excludeCG, setExcludeCG] = useState(false);
const [excludeServing, setExcludeServing] = useState(false);

const filteredPeople = useMemo(() => {
return people
.filter((person) => {
// Text search filter
const matchesSearch = person.fullName
.toLowerCase()
.includes(searchTerm.toLowerCase());

// CG group filter
let matchesCG = true;
if (showOnlyCG) {
matchesCG = !!person.cgGroup;
} else if (excludeCG) {
matchesCG = !person.cgGroup;
}

// Serving filter
let matchesServing = true;
if (showOnlyServing) {
matchesServing = !!person.isServing;
} else if (excludeServing) {
matchesServing = !person.isServing;
}

return matchesSearch && matchesCG && matchesServing;
})
.sort((a, b) => a.fullName.localeCompare(b.fullName));
}, [
people,
searchTerm,
showOnlyCG,
showOnlyServing,
excludeCG,
excludeServing,
]);

// Update parent component when filters change
useMemo(() => {
onFilterChange(filteredPeople);
}, [filteredPeople, onFilterChange]);

const cgCount = people.filter((p) => p.cgGroup).length;
const servingCount = people.filter((p) => p.isServing).length;

return (
<div className="mb-4 space-y-3">
{/* Search Box */}
<div className="relative">
<input
type="text"
placeholder="Search people by name..."
value={searchTerm}
onChange={(e) => {
setSearchTerm(e.target.value);
}}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
{searchTerm && (
<button
type="button"
onClick={() => {
setSearchTerm("");
}}
className="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
)}
</div>

{/* Filter Chips */}
<div className="flex gap-2">
{/* CG Filter Chip - only show if there are people with CG groups */}
{cgCount > 0 && (
<button
type="button"
onClick={() => {
if (showOnlyCG) {
setShowOnlyCG(false);
setExcludeCG(true);
} else if (excludeCG) {
setExcludeCG(false);
} else {
setShowOnlyCG(true);
setExcludeCG(false);
}
}}
className={`text-xs px-3 py-1.5 font-semibold rounded-full transition-colors ${
showOnlyCG
? "bg-blue-500 text-white"
: excludeCG
? "bg-red-500 text-white"
: "bg-gray-200 text-gray-700 hover:bg-gray-300"
}`}
>
CG ({cgCount})
</button>
)}

{/* Serving Filter Chip - only show if there are people serving */}
{servingCount > 0 && (
<button
type="button"
onClick={() => {
if (showOnlyServing) {
setShowOnlyServing(false);
setExcludeServing(true);
} else if (excludeServing) {
setExcludeServing(false);
} else {
setShowOnlyServing(true);
setExcludeServing(false);
}
}}
className={`text-xs px-3 py-1.5 font-semibold rounded-full transition-colors ${
showOnlyServing
? "bg-green-500 text-white"
: excludeServing
? "bg-red-500 text-white"
: "bg-gray-200 text-gray-700 hover:bg-gray-400"
}`}
>
S ({servingCount})
</button>
)}

{/* Clear Filters Button */}
{(searchTerm ||
showOnlyCG ||
showOnlyServing ||
excludeCG ||
excludeServing) && (
<button
type="button"
onClick={() => {
setSearchTerm("");
setShowOnlyCG(false);
setShowOnlyServing(false);
setExcludeCG(false);
setExcludeServing(false);
}}
className="text-xs px-3 py-1.5 font-semibold rounded-full bg-gray-300 text-gray-700 hover:bg-gray-400 transition-colors"
>
Clear
</button>
)}
</div>

{/* Results Count */}
<div className="text-xs text-gray-500">
Showing {filteredPeople.length} of {people.length} people
</div>
</div>
);
}

export default PeopleFilter;
21 changes: 15 additions & 6 deletions src/components/PeopleList/PeopleList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,32 @@ import PeopleListItem from "./PeopleListItem";
interface PeopleListProps {
people: Person[];
surveys: Survey[];
label: string;
label?: string;
hasActiveFilters?: boolean;
}

function PeopleList({ people, surveys, label }: PeopleListProps) {
function PeopleList({
people,
surveys,
label,
hasActiveFilters,
}: PeopleListProps) {
if (people.length === 0) {
return (
<div className="text-center">
<div className="text-xs text-gray-500">No people</div>
<div className="text-center mt-4">
<div className="text-xs text-gray-500 bg-gray-50 rounded-lg py-3 px-4 border border-gray-200">
{hasActiveFilters
? "No people match your filters."
: `No people in this ${label ?? "category"}`}
</div>
</div>
);
}

// Create a map of person ID to survey for quick lookup
const surveyMap = new Map(surveys.map((survey) => [survey.personId, survey]));

return (
<div className="grid grid-cols-2 gap-2 w-full">
<div className="flex flex-col divide-y-2 divide-gray-100 mx-[-16px]">
{people.map((person) => (
<PeopleListItem
key={person.id}
Expand Down
Loading