Dashboard: search and filter studies by tag
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:
nmemmertandClaude Sonnet 5.5 committed 2026-10-06 10:03:06 -04:00
1 parent dc825f0112
commit 88bc59bac0
4 files changed
+138 -21

No files matched your search

+39
View File
@@ -27,6 +27,45 @@ describe('Home page', () => {
localStorage.setItem('bible-study-index', JSON.stringify(index));
}
test('studies can be found by tag: chips, clicking a tag on a card, and #tag search', async () => {
const add = (id, title, tags) => {
seedProject({ id, title });
const index = JSON.parse(localStorage.getItem('bible-study-index'));
index.find((e) => e.id === id).tags = tags;
localStorage.setItem('bible-study-index', JSON.stringify(index));
};
add('p1', 'Titus Notes', ['hope', 'leadership']);
add('p2', 'New Birth', ['grace', 'hope']);
add('p3', 'Grace Alone', []);
const user = userEvent.setup();
await renderApp();
const titles = () => screen.queryAllByRole('heading', { level: 2 }).map((h) => h.textContent).filter((t) => ['Titus Notes', 'New Birth', 'Grace Alone'].includes(t)).sort();
const bar = screen.getByRole('group', { name: /filter by tag/i });
expect(titles()).toHaveLength(3);
await user.click(within(bar).getByRole('button', { name: /^hope/ }));
expect(titles()).toEqual(['New Birth', 'Titus Notes']);
await user.click(within(bar).getByRole('button', { name: /^grace/ })); // both chips: a study needs both
expect(titles()).toEqual(['New Birth']);
await user.click(within(bar).getByRole('button', { name: 'Clear' }));
expect(titles()).toHaveLength(3);
await user.click(screen.getAllByRole('button', { name: 'leadership' }).find((b) => b.closest('h2, div.rounded-2xl')?.textContent.includes('Titus Notes'))); // a tag on a card
expect(titles()).toEqual(['Titus Notes']);
await user.click(within(bar).getByRole('button', { name: 'Clear' }));
await user.type(screen.getByPlaceholderText(/search projects/i), '#grace'); // tags only: not the study named "Grace Alone"
expect(titles()).toEqual(['New Birth']);
await user.clear(screen.getByPlaceholderText(/search projects/i));
await user.type(screen.getByPlaceholderText(/search projects/i), 'lead'); // plain words match tags too
expect(titles()).toEqual(['Titus Notes']);
await user.clear(screen.getByPlaceholderText(/search projects/i));
await user.type(screen.getByPlaceholderText(/search projects/i), '#nothing');
expect(await screen.findByRole('status')).toHaveTextContent(/No studies match/);
await user.click(screen.getByRole('button', { name: 'Clear filters' }));
expect(titles()).toHaveLength(3);
});
test('studies can be shown as a grid or a list, and the choice is remembered', async () => {
seedProject();
seedProject({ id: 'p2', title: 'Second Study' });