From 8bda0b814cd0c160ea46a39258918f0045215c68 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 23:28:17 +0000 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01DReRqHxSYuVfCzTzQGD9hZ --- public/index.html | 2 +- src/App.css | 67 +++++++++++++++++++++++++++++++++++++++++++++++ src/App.tsx | 63 +++++++++++++++++++++++++++++++++++++++----- 3 files changed, 124 insertions(+), 8 deletions(-) diff --git a/public/index.html b/public/index.html index aa069f2..0be728f 100644 --- a/public/index.html +++ b/public/index.html @@ -24,7 +24,7 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> - React App + Prayer Journal diff --git a/src/App.css b/src/App.css index d23d724..f04f95d 100644 --- a/src/App.css +++ b/src/App.css @@ -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; diff --git a/src/App.tsx b/src/App.tsx index 1373022..29cec39 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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('active'); + const [activeTag, setActiveTag] = useState(null); const [newRequest, setNewRequest] = useState(''); const [newTags, setNewTags] = useState(''); const [expandedId, setExpandedId] = useState(null); const [editingAnswer, setEditingAnswer] = useState<{ id: string; text: string } | null>(null); const [writeError, setWriteError] = useState(null); + // All unique tags across all non-archived prayers + const allTags = useMemo(() => { + const set = new Set(); + 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 (
@@ -121,6 +136,26 @@ export default function App() {
+ {allTags.length > 0 && ( +
+ Filter by tag: + {allTags.map(tag => ( + + ))} + {activeTag && ( + + )} +
+ )} + {(writeError || apiError) && (
⚠ {writeError || apiError} @@ -130,9 +165,11 @@ export default function App() { {loading &&

Loading prayers…

}
    - {filtered.length === 0 && ( + {filtered.length === 0 && !loading && (
  • - {filter === 'active' + {activeTag + ? `No ${filter === 'all' ? '' : filter + ' '}prayers tagged "${activeTag}".` + : filter === 'active' ? 'No active prayers. Add one above.' : 'Nothing here yet.'}
  • @@ -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({
    {formatDate(prayer.createdAt)} {prayer.tags.map(tag => ( - {tag} + { e.stopPropagation(); onTagClick(tag); }} + > + {tag} + ))} {expanded ? '▲' : '▼'}