Add tag filtering and fix browser title

- Tag filter bar appears when any prayers have tags; click to filter,
  click again or hit Clear to reset. Tags on cards are also clickable.
- Status filter and tag filter combine (e.g. active + family tag).
- Browser tab title changed from 'React App' to 'Prayer Journal'.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DReRqHxSYuVfCzTzQGD9hZ
This commit is contained in:
Claude committed 2026-07-18 23:28:17 +00:00
1 parent bdf27b8b99
commit 8bda0b814c
3 files changed
+124 -8

No files matched your search

+67
View File
@@ -227,6 +227,73 @@ body {
}
/* Prayer list */
/* Tag filter bar */
.tag-filter-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: .4rem;
margin-bottom: 1rem;
padding: .6rem .75rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
}
.tag-filter-label {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: .72rem;
letter-spacing: .06em;
text-transform: uppercase;
color: var(--text-muted);
margin-right: .2rem;
}
.tag-filter-btn {
background: var(--tag-bg);
color: var(--tag-text);
border: 1px solid transparent;
border-radius: 4px;
padding: .2rem .55rem;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: .78rem;
cursor: pointer;
transition: background .12s, border-color .12s, color .12s;
}
.tag-filter-btn:hover {
border-color: var(--primary);
color: var(--primary);
}
.tag-filter-btn.active {
background: var(--primary);
color: var(--primary-fg);
border-color: var(--primary);
}
.tag-clear-btn {
background: none;
border: none;
color: var(--text-muted);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: .78rem;
cursor: pointer;
padding: .2rem .4rem;
margin-left: .2rem;
}
.tag-clear-btn:hover { color: var(--danger); }
/* Clickable tags on cards */
.tag {
cursor: pointer;
transition: background .12s, color .12s;
}
.tag:hover { background: var(--primary); color: var(--primary-fg); }
.tag.tag-active { background: var(--primary); color: var(--primary-fg); }
.prayer-list {
list-style: none;
display: flex;
+56 -7
View File
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useMemo } from 'react';
import { usePrayers } from './usePrayers';
import { Prayer } from './types';
import './App.css';
@@ -20,16 +20,27 @@ export default function App() {
} = usePrayers();
const [filter, setFilter] = useState<Filter>('active');
const [activeTag, setActiveTag] = useState<string | null>(null);
const [newRequest, setNewRequest] = useState('');
const [newTags, setNewTags] = useState('');
const [expandedId, setExpandedId] = useState<string | null>(null);
const [editingAnswer, setEditingAnswer] = useState<{ id: string; text: string } | null>(null);
const [writeError, setWriteError] = useState<string | null>(null);
// All unique tags across all non-archived prayers
const allTags = useMemo(() => {
const set = new Set<string>();
prayers.filter(p => p.status !== 'archived').forEach(p => p.tags.forEach(t => set.add(t)));
return Array.from(set).sort();
}, [prayers]);
const filtered = prayers.filter(p => {
if (filter === 'active') return p.status === 'active';
if (filter === 'answered') return p.status === 'answered';
return p.status !== 'archived';
const statusMatch =
filter === 'active' ? p.status === 'active' :
filter === 'answered' ? p.status === 'answered' :
p.status !== 'archived';
const tagMatch = activeTag ? p.tags.includes(activeTag) : true;
return statusMatch && tagMatch;
});
const counts = {
@@ -72,6 +83,10 @@ export default function App() {
});
}
function handleTagClick(tag: string) {
setActiveTag(prev => prev === tag ? null : tag);
}
return (
<div className="app">
<header className="header">
@@ -121,6 +136,26 @@ export default function App() {
</button>
</div>
{allTags.length > 0 && (
<div className="tag-filter-bar">
<span className="tag-filter-label">Filter by tag:</span>
{allTags.map(tag => (
<button
key={tag}
className={`tag-filter-btn ${activeTag === tag ? 'active' : ''}`}
onClick={() => handleTagClick(tag)}
>
{tag}
</button>
))}
{activeTag && (
<button className="tag-clear-btn" onClick={() => setActiveTag(null)}>
✕ Clear
</button>
)}
</div>
)}
{(writeError || apiError) && (
<div className="error-banner">
⚠ {writeError || apiError}
@@ -130,9 +165,11 @@ export default function App() {
{loading && <p className="loading">Loading prayers…</p>}
<ul className="prayer-list">
{filtered.length === 0 && (
{filtered.length === 0 && !loading && (
<li className="empty">
{filter === 'active'
{activeTag
? `No ${filter === 'all' ? '' : filter + ' '}prayers tagged "${activeTag}".`
: filter === 'active'
? 'No active prayers. Add one above.'
: 'Nothing here yet.'}
</li>
@@ -144,6 +181,8 @@ export default function App() {
prayer={prayer}
expanded={expandedId === prayer.id}
editingAnswer={editingAnswer?.id === prayer.id ? editingAnswer.text : null}
activeTag={activeTag}
onTagClick={handleTagClick}
onToggle={() =>
setExpandedId(expandedId === prayer.id ? null : prayer.id)
}
@@ -170,6 +209,8 @@ interface CardProps {
prayer: Prayer;
expanded: boolean;
editingAnswer: string | null;
activeTag: string | null;
onTagClick: (tag: string) => void;
onToggle: () => void;
onStartAnswer: () => void;
onEditAnswer: (text: string) => void;
@@ -186,6 +227,8 @@ function PrayerCard({
prayer,
expanded,
editingAnswer,
activeTag,
onTagClick,
onToggle,
onStartAnswer,
onEditAnswer,
@@ -206,7 +249,13 @@ function PrayerCard({
<div className="card-meta">
<span className="date">{formatDate(prayer.createdAt)}</span>
{prayer.tags.map(tag => (
<span key={tag} className="tag">{tag}</span>
<span
key={tag}
className={`tag ${activeTag === tag ? 'tag-active' : ''}`}
onClick={e => { e.stopPropagation(); onTagClick(tag); }}
>
{tag}
</span>
))}
<span className="chevron">{expanded ? '▲' : '▼'}</span>
</div>