Skip to content
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f5814c0
Implemented getVolunteersTable API function
Jan 24, 2026
99a5340
Export getVolunteersTable in API index
Jan 24, 2026
b3ef3f6
Implemented testing for getVolunteersTable API function
Jan 24, 2026
3095b60
Merge branch 'backend/get-volunteers-table-api' into frontend/volunte…
a1-su Feb 14, 2026
5494b9d
Implement Volunteer Table main functionality
a1-su Feb 14, 2026
c82ba8e
Merge branch 'main' into frontend/volunteer-table
a1-su Feb 25, 2026
ea67fea
Implement Volunteer Table Modal
a1-su Mar 2, 2026
e30bfd4
Fix table columns and search
a1-su Mar 15, 2026
58558c3
Merge branch 'frontend/volunteer-table' into frontend/volunteer-table…
a1-su Mar 15, 2026
f97ff1d
Fix formatting color error
a1-su Mar 15, 2026
1fea410
Refactor table to include opt in communication
a1-su Mar 15, 2026
45b9516
Modify volunteer table imports
a1-su Mar 15, 2026
39c8f8a
Merge branch 'frontend/volunteer-table' into frontend/volunteer-table…
a1-su Mar 15, 2026
da17054
Fix icon sizing
a1-su Mar 16, 2026
d93bbe4
Fix icon sizing
a1-su Mar 16, 2026
d4bc46e
Table optimizationa nd aria labels
a1-su Mar 16, 2026
6137a60
Merge branch 'frontend/volunteer-table' into frontend/volunteer-table…
a1-su Mar 16, 2026
49aa6cc
Merge branch 'main' into frontend/volunteer-table-filter-modal
a1-su Mar 16, 2026
182a802
Fix row filtering selection bug and imports
a1-su Mar 16, 2026
1e35372
Fix searchbar styling
a1-su Mar 16, 2026
05e7b31
Fix opt_in_communication column type and add proper tests
a1-su Mar 16, 2026
d667298
Implement sort modal
a1-su Mar 17, 2026
352be37
Merge branch 'main' into frontend/volunteer-table-sort-modal
a1-su Mar 26, 2026
aa0dfc7
Fix variable color names
a1-su Mar 26, 2026
901ba3f
Fix admin account info page color names
a1-su Mar 26, 2026
a4ecb3e
something
notjackl3 Mar 27, 2026
46f08cd
Merge branch 'main' into frontend/volunteer-table-sort-modal
notjackl3 Mar 27, 2026
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
72 changes: 72 additions & 0 deletions src/components/volunteers/ColumnSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React, { useState, useRef, useEffect } from "react";

export interface SelectorColumn {
id: string;
label: string;
icon: React.ElementType;
}

interface ColumnSelectorProps {
columns: SelectorColumn[];
onSelect: (colId: string) => void;
placeholder?: string;
}

export const ColumnSelector = ({
columns,
onSelect,
placeholder = "Select column...",
}: ColumnSelectorProps): React.JSX.Element => {
const [search, setSearch] = useState("");
const inputRef = useRef<HTMLInputElement>(null);

useEffect(() => {
setTimeout(() => inputRef.current?.focus(), 0);
}, []);

const visibleColumns = columns.filter((col) =>
col.label.toLowerCase().includes(search.toLowerCase())
);

return (
<>
<input
ref={inputRef}
type="text"
placeholder={placeholder}
value={search}
onChange={(e) => setSearch(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
if (visibleColumns.length > 0 && visibleColumns[0]) {
onSelect(visibleColumns[0].id);
}
}
}}
className="w-full bg-gray-50 border border-gray-200 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-secondary-purple"
/>
<div className="flex flex-col max-h-60 overflow-y-auto mt-2">
{visibleColumns.length > 0 ? (
visibleColumns.map((col) => {
const Icon = col.icon;
return (
<button
key={col.id}
onClick={() => onSelect(col.id)}
className="text-left px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded-md transition-colors flex items-center gap-2"
>
<Icon className="w-4 h-4 text-gray-400 shrink-0" />
<span>{col.label}</span>
</button>
);
})
) : (
<p className="text-xs text-gray-500 italic p-2 text-center">
No available columns
</p>
)}
</div>
</>
);
};
52 changes: 47 additions & 5 deletions src/components/volunteers/FilterBar.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { FilterTuple } from "@/lib/api/getVolunteersByMultipleColumns";
import { ChevronDown, Plus } from "lucide-react";
import { ChevronDown, Plus, ArrowUpDown } from "lucide-react";
import { SortingState } from "@tanstack/react-table";
import clsx from "clsx";
import { FILTERABLE_COLUMNS } from "./volunteerColumns";
import { FilterModal, filterModalAlignRight } from "./FilterModal";
import { SortModal } from "./SortModal";

interface FilterBarProps {
filters: FilterTuple[];
setFilters: React.Dispatch<React.SetStateAction<FilterTuple[]>>;
globalOp: "AND" | "OR";
setGlobalOp: (op: "AND" | "OR") => void;
optionsData: Record<string, string[]>;
sorting: SortingState;
setSorting: React.Dispatch<React.SetStateAction<SortingState>>;
}

export const FilterBar = ({
Expand All @@ -19,13 +23,23 @@ export const FilterBar = ({
globalOp,
setGlobalOp,
optionsData,
sorting,
setSorting,
}: FilterBarProps): React.JSX.Element | null => {
const [editingIndex, setEditingIndex] = useState<number | null>(null);
const [editAlignRight, setEditAlignRight] = useState(false);
const [isAddingNew, setIsAddingNew] = useState(false);
const [newAlignRight, setNewAlignRight] = useState(false);
const [isSortModalOpen, setIsSortModalOpen] = useState(false);
const [sortAlignRight, setSortAlignRight] = useState(false);

if (filters.length === 0) return null;
useEffect(() => {
if (sorting.length === 0) {
setIsSortModalOpen(false);
}
}, [sorting.length]);

if (filters.length === 0 && sorting.length === 0) return null;

const handleEditClick = (e: React.MouseEvent, index: number): void => {
setIsAddingNew(false);
Expand Down Expand Up @@ -68,6 +82,34 @@ export const FilterBar = ({

return (
<div className="flex flex-wrap items-center gap-2 py-2 relative">
{sorting.length > 0 && (
<>
<div className={clsx("relative", isSortModalOpen ? "z-50" : "z-10")}>
<button
onClick={(e) => {
setSortAlignRight(
filterModalAlignRight(e.currentTarget as HTMLElement)
);
setIsSortModalOpen(!isSortModalOpen);
}}
className="bg-purple-200 text-purple-600 hover:bg-purple-300 rounded-lg px-3 py-1.5 text-sm font-medium flex items-center gap-2 transition-colors cursor-pointer"
>
<ArrowUpDown className="w-3.5 h-3.5" />
{sorting.length} Sort{sorting.length !== 1 && "s"}
</button>

<SortModal
isOpen={isSortModalOpen}
onClose={() => setIsSortModalOpen(false)}
sorting={sorting}
setSorting={setSorting}
alignRight={sortAlignRight}
/>
</div>
<div className="w-px h-5 bg-gray-300 mx-1" />
</>
)}

<div className="bg-gray-100 rounded-lg px-3 py-1.5 text-sm font-medium flex items-center gap-2 text-gray-800 relative z-10">
Matches
<select
Expand All @@ -93,7 +135,7 @@ export const FilterBar = ({
>
<button
onClick={(e) => handleEditClick(e, index)}
className="bg-primary-purple text-gray-900 hover:bg-secondary-purple rounded-lg px-3 py-1.5 text-sm font-medium flex items-center gap-2 transition-colors cursor-pointer"
className="bg-purple-200 text-gray-900 hover:bg-purple-300 rounded-lg px-3 py-1.5 text-sm font-medium flex items-center gap-2 transition-colors cursor-pointer"
>
{Icon && <Icon className="w-3.5 h-3.5 opacity-70" />}
{colDef?.label}
Expand All @@ -116,7 +158,7 @@ export const FilterBar = ({
<div className={clsx("relative", isAddingNew ? "z-50" : "z-10")}>
<button
onClick={handleAddNewClick}
className="bg-primary-purple hover:bg-secondary-purple text-gray-900 rounded-lg px-3 py-1.5 text-sm font-medium flex items-center gap-2 transition-colors cursor-pointer"
className="bg-purple-200 hover:bg-purple-300 text-gray-900 rounded-lg px-3 py-1.5 text-sm font-medium flex items-center gap-2 transition-colors cursor-pointer"
>
<Plus className="w-4 h-4" />
New Filter
Expand Down
79 changes: 20 additions & 59 deletions src/components/volunteers/FilterModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useState, useRef, useEffect } from "react";
import { ColumnSelector } from "./ColumnSelector";
import { FilterTuple } from "@/lib/api/getVolunteersByMultipleColumns";
import { Trash2 } from "lucide-react";
import { VolunteerTag } from "./VolunteerTag";
Expand Down Expand Up @@ -34,18 +35,18 @@ export const FilterModal = ({
"SELECT_COLUMN" | "SELECT_VALUES"
>("SELECT_COLUMN");
const [selectedCol, setSelectedCol] = useState<string | null>(null);
const [columnSearch, setColumnSearch] = useState("");
const [inputValue, setInputValue] = useState("");
const [selectedOptions, setSelectedOptions] = useState<string[]>([]);
const [miniOp, setMiniOp] = useState<"AND" | "OR">("OR");

const colDef = FILTERABLE_COLUMNS.find((c) => c.id === selectedCol);
const availableOptions = selectedCol ? optionsData[selectedCol] || [] : [];
const visibleColumns = FILTERABLE_COLUMNS.filter((col) =>
col.label.toLowerCase().includes(columnSearch.toLowerCase())
);
const visibleColumns = FILTERABLE_COLUMNS.map((c) => ({
id: c.id,
label: c.label,
icon: c.icon,
}));

const columnInputRef = useRef<HTMLInputElement>(null);
const valueInputRef = useRef<HTMLInputElement>(null);

useEffect(() => {
Expand All @@ -67,7 +68,6 @@ export const FilterModal = ({
} else {
setActiveStep("SELECT_COLUMN");
setSelectedCol(null);
setColumnSearch("");
setInputValue("");
setSelectedOptions([]);
setMiniOp("OR");
Expand All @@ -77,9 +77,7 @@ export const FilterModal = ({

useEffect(() => {
if (isOpen) {
if (activeStep === "SELECT_COLUMN") {
setTimeout(() => columnInputRef.current?.focus(), 0);
} else if (activeStep === "SELECT_VALUES") {
if (activeStep === "SELECT_VALUES") {
setTimeout(() => valueInputRef.current?.focus(), 0);
}
}
Expand Down Expand Up @@ -154,53 +152,16 @@ export const FilterModal = ({
)}
>
{activeStep === "SELECT_COLUMN" ? (
<>
<input
ref={columnInputRef}
type="text"
placeholder="Filter by..."
value={columnSearch}
onChange={(e) => setColumnSearch(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
if (visibleColumns.length > 0 && visibleColumns[0]) {
setSelectedCol(visibleColumns[0].id);
setActiveStep("SELECT_VALUES");
setInputValue("");
setMiniOp("OR");
}
}
}}
className="w-full bg-gray-50 border border-gray-200 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-secondary-purple"
/>
<div className="flex flex-col max-h-60 overflow-y-auto mt-2">
{visibleColumns.length > 0 ? (
visibleColumns.map((col) => {
const Icon = col.icon;
return (
<button
key={col.id}
onClick={() => {
setSelectedCol(col.id);
setActiveStep("SELECT_VALUES");
setInputValue("");
setMiniOp("OR");
}}
className="text-left px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded-md transition-colors flex items-center gap-2"
>
<Icon className="w-4 h-4 text-gray-400" />
<span>{col.label}</span>
</button>
);
})
) : (
<p className="text-xs text-gray-500 italic p-2 text-center">
No available columns
</p>
)}
</div>
</>
<ColumnSelector
columns={visibleColumns}
placeholder="Filter by..."
onSelect={(colId) => {
setSelectedCol(colId);
setActiveStep("SELECT_VALUES");
setInputValue("");
setMiniOp("OR");
}}
/>
) : (
<>
<div className="flex items-center justify-between">
Expand Down Expand Up @@ -244,7 +205,7 @@ export const FilterModal = ({
handleApplyFilter();
}
}}
className="w-full border border-gray-200 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-secondary-purple"
className="w-full border border-gray-200 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-purple-300"
/>
) : (
<>
Expand All @@ -260,7 +221,7 @@ export const FilterModal = ({
handleApplyFilter();
}
}}
className="w-full border border-gray-200 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-secondary-purple"
className="w-full border border-gray-200 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-purple-300"
/>
<div className="flex flex-col gap-2 max-h-48 overflow-y-auto mt-2">
{availableOptions
Expand All @@ -283,7 +244,7 @@ export const FilterModal = ({
p.filter((o) => o !== opt)
);
}}
className="rounded border-gray-300 text-accent-purple focus:ring-accent-purple cursor-pointer"
className="rounded border-gray-300 text-purple-600 focus:ring-purple-600 cursor-pointer"
/>
<VolunteerTag label={opt} />
</label>
Expand Down
Loading
Loading