Add filtering and sorting to question bank

- Add search, category, and type filters to questions API
- Add clickable sortable table headers with sort indicators
- Add filter controls UI with clear filters button

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
ryan
2026-01-18 13:01:11 -05:00
parent 758e1a18e4
commit ab962725e6
3 changed files with 206 additions and 21 deletions

View File

@@ -29,14 +29,29 @@ export default function QuestionBankView() {
const [downloadJob, setDownloadJob] = useState(null);
const [downloadProgress, setDownloadProgress] = useState(0);
// Filter and sort state
const [searchTerm, setSearchTerm] = useState("");
const [filterCategory, setFilterCategory] = useState("");
const [filterType, setFilterType] = useState("");
const [sortBy, setSortBy] = useState("created_at");
const [sortOrder, setSortOrder] = useState("desc");
useEffect(() => {
loadQuestions();
loadCategories();
}, []);
}, [searchTerm, filterCategory, filterType, sortBy, sortOrder]);
const loadQuestions = async () => {
try {
const response = await questionsAPI.getAll();
const params = {
sort_by: sortBy,
sort_order: sortOrder,
};
if (searchTerm) params.search = searchTerm;
if (filterCategory) params.category = filterCategory;
if (filterType) params.type = filterType;
const response = await questionsAPI.getAll(params);
setQuestions(response.data);
} catch (error) {
console.error("Error loading questions:", error);
@@ -295,6 +310,28 @@ export default function QuestionBankView() {
}
};
const handleSort = (column) => {
if (sortBy === column) {
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
} else {
setSortBy(column);
setSortOrder("asc");
}
};
const SortIndicator = ({ column }) => {
if (sortBy !== column) return <span style={{ opacity: 0.3, marginLeft: "0.25rem" }}></span>;
return <span style={{ marginLeft: "0.25rem" }}>{sortOrder === "asc" ? "↑" : "↓"}</span>;
};
const sortableHeaderStyle = {
padding: "0.75rem",
textAlign: "left",
borderBottom: "2px solid #ddd",
cursor: "pointer",
userSelect: "none",
};
return (
<>
<AdminNavbar />
@@ -581,6 +618,101 @@ export default function QuestionBankView() {
)}
<div>
{/* Filter and Sort Controls */}
<div
style={{
marginBottom: "1rem",
padding: "1rem",
background: "#f9f9f9",
borderRadius: "8px",
display: "flex",
flexWrap: "wrap",
gap: "1rem",
alignItems: "center",
}}
>
{/* Search */}
<div style={{ flex: "1", minWidth: "200px" }}>
<input
type="text"
placeholder="Search questions or answers..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
style={{
width: "100%",
padding: "0.5rem",
border: "1px solid #ddd",
borderRadius: "4px",
}}
/>
</div>
{/* Category Filter */}
<div>
<select
value={filterCategory}
onChange={(e) => setFilterCategory(e.target.value)}
style={{
padding: "0.5rem",
border: "1px solid #ddd",
borderRadius: "4px",
minWidth: "150px",
}}
>
<option value="">All Categories</option>
<option value="none">Uncategorized</option>
{categories.map((cat) => (
<option key={cat.id} value={cat.name}>
{cat.name}
</option>
))}
</select>
</div>
{/* Type Filter */}
<div>
<select
value={filterType}
onChange={(e) => setFilterType(e.target.value)}
style={{
padding: "0.5rem",
border: "1px solid #ddd",
borderRadius: "4px",
minWidth: "130px",
}}
>
<option value="">All Types</option>
<option value="text">Text</option>
<option value="image">Image</option>
<option value="youtube_audio">YouTube Audio</option>
</select>
</div>
{/* Clear Filters */}
{(searchTerm || filterCategory || filterType || sortBy !== "created_at" || sortOrder !== "desc") && (
<button
onClick={() => {
setSearchTerm("");
setFilterCategory("");
setFilterType("");
setSortBy("created_at");
setSortOrder("desc");
}}
style={{
padding: "0.5rem 1rem",
background: "#f44336",
color: "white",
border: "none",
borderRadius: "4px",
cursor: "pointer",
fontSize: "0.9rem",
}}
>
Clear Filters
</button>
)}
</div>
<div
style={{
display: "flex",
@@ -715,42 +847,38 @@ export default function QuestionBankView() {
/>
</th>
<th
onClick={() => handleSort("type")}
style={{
padding: "0.75rem",
textAlign: "left",
borderBottom: "2px solid #ddd",
...sortableHeaderStyle,
width: "80px",
}}
>
Type
<SortIndicator column="type" />
</th>
<th
onClick={() => handleSort("category")}
style={{
padding: "0.75rem",
textAlign: "left",
borderBottom: "2px solid #ddd",
...sortableHeaderStyle,
width: "120px",
}}
>
Category
<SortIndicator column="category" />
</th>
<th
style={{
padding: "0.75rem",
textAlign: "left",
borderBottom: "2px solid #ddd",
}}
onClick={() => handleSort("question_content")}
style={sortableHeaderStyle}
>
Question
<SortIndicator column="question_content" />
</th>
<th
style={{
padding: "0.75rem",
textAlign: "left",
borderBottom: "2px solid #ddd",
}}
onClick={() => handleSort("answer")}
style={sortableHeaderStyle}
>
Answer
<SortIndicator column="answer" />
</th>
<th
style={{

View File

@@ -48,7 +48,7 @@ api.interceptors.response.use(
// Questions API
export const questionsAPI = {
getAll: () => api.get("/questions"),
getAll: (params = {}) => api.get("/questions", { params }),
getOne: (id) => api.get(`/questions/${id}`),
create: (data) => api.post("/questions", data),
createWithImage: (formData) =>