Dashboard: search and filter studies by tag
Build and Push Docker Image / build-and-push (push) Failing after 20s
Build and Push Docker Image / build-and-push (push) Failing after 20s
- The search box matches tags as well as title and passage; "#tag" searches tags only - Tag chips (with counts) replace the Tag dropdown; several can be on at once and a study must have all of them - Tags on each study card are clickable filters - "No studies match" message with a Clear filters link Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
dc825f0112
commit
88bc59bac0
4 files changed
+138
-21
No files matched your search
@@ -0,0 +1,25 @@
|
||||
// Searching and filtering the studies on the dashboard. The search box matches a study's title, its
|
||||
// passages and its tags; starting it with "#" searches tags only ("#faith"). Chosen tag chips narrow
|
||||
// the list to studies that have every one of them.
|
||||
|
||||
export const isTagQuery = (query) => String(query ?? '').trim().startsWith('#');
|
||||
|
||||
// -> [{ tag, count }], most-used first, then alphabetical
|
||||
export function tagCounts(index) {
|
||||
const counts = new Map();
|
||||
for (const entry of index) for (const tag of new Set(entry.tags ?? [])) counts.set(tag, (counts.get(tag) ?? 0) + 1);
|
||||
return [...counts].map(([tag, count]) => ({ tag, count })).sort((a, b) => b.count - a.count || a.tag.localeCompare(b.tag));
|
||||
}
|
||||
|
||||
export function filterStudies(index, { query = '', tags = [] } = {}) {
|
||||
const raw = String(query).trim().toLowerCase();
|
||||
const tagOnly = raw.startsWith('#');
|
||||
const q = tagOnly ? raw.slice(1).trim() : raw;
|
||||
return index.filter((entry) => {
|
||||
const own = (entry.tags ?? []).map((t) => t.toLowerCase());
|
||||
if (tags.length && !tags.every((t) => own.includes(t.toLowerCase()))) return false;
|
||||
if (!q) return true;
|
||||
if (own.some((t) => t.includes(q))) return true;
|
||||
return !tagOnly && ((entry.title ?? '').toLowerCase().includes(q) || (entry.chapterSummary ?? '').toLowerCase().includes(q));
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { test, expect } from 'vitest';
|
||||
import { filterStudies, tagCounts, isTagQuery } from './studyFilter.js';
|
||||
|
||||
const index = [
|
||||
{ id: 'a', title: 'Titus Notes', chapterSummary: 'Titus 1', tags: ['hope', 'leadership'] },
|
||||
{ id: 'b', title: 'New Birth', chapterSummary: 'John 3', tags: ['grace', 'hope'] },
|
||||
{ id: 'c', title: 'Grace Alone', chapterSummary: 'Romans 3', tags: [] },
|
||||
];
|
||||
const ids = (list) => list.map((e) => e.id);
|
||||
|
||||
test('the search box finds a study by title, passage or tag', () => {
|
||||
expect(ids(filterStudies(index, { query: 'titus' }))).toEqual(['a']);
|
||||
expect(ids(filterStudies(index, { query: 'romans' }))).toEqual(['c']);
|
||||
expect(ids(filterStudies(index, { query: 'lead' }))).toEqual(['a']); // a tag
|
||||
expect(ids(filterStudies(index, { query: 'grace' }))).toEqual(['b', 'c']); // a tag on one, a title on the other
|
||||
});
|
||||
|
||||
test('starting with # searches tags only', () => {
|
||||
expect(isTagQuery(' #grace')).toBe(true);
|
||||
expect(ids(filterStudies(index, { query: '#grace' }))).toEqual(['b']); // not the study named "Grace Alone"
|
||||
expect(ids(filterStudies(index, { query: '#HOP' }))).toEqual(['a', 'b']);
|
||||
expect(ids(filterStudies(index, { query: '#' }))).toEqual(['a', 'b', 'c']);
|
||||
expect(ids(filterStudies(index, { query: '#nothing' }))).toEqual([]);
|
||||
});
|
||||
|
||||
test('chosen tags must all be present, and combine with the search', () => {
|
||||
expect(ids(filterStudies(index, { tags: ['hope'] }))).toEqual(['a', 'b']);
|
||||
expect(ids(filterStudies(index, { tags: ['hope', 'grace'] }))).toEqual(['b']);
|
||||
expect(ids(filterStudies(index, { tags: ['hope'], query: 'john' }))).toEqual(['b']);
|
||||
expect(ids(filterStudies(index, { tags: ['Hope'] }))).toEqual(['a', 'b']); // case does not matter
|
||||
});
|
||||
|
||||
test('tags are counted, most used first', () => {
|
||||
expect(tagCounts(index)).toEqual([{ tag: 'hope', count: 2 }, { tag: 'grace', count: 1 }, { tag: 'leadership', count: 1 }]);
|
||||
});
|
||||
Reference in new issue
Block a user