From af836d4ec996fe3404db6d059b20dbcc5186b1e8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 23:32:37 +0000 Subject: [PATCH] Add person field, full-text search, and search highlighting - New 'Who are you praying for?' field on prayer cards (optional) - Person name shown in primary color above the request text - Search bar filters across person, request, answer, and tags simultaneously - Search terms highlighted in yellow/amber in results - Tag filter and search combine with status filter Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01DReRqHxSYuVfCzTzQGD9hZ --- src/App.css | 110 +++++++++++++++++++++++++++++++++- src/App.tsx | 150 +++++++++++++++++++++++++++++++--------------- src/types.ts | 1 + src/usePrayers.ts | 3 +- 4 files changed, 211 insertions(+), 53 deletions(-) diff --git a/src/App.css b/src/App.css index f04f95d..0094714 100644 --- a/src/App.css +++ b/src/App.css @@ -181,13 +181,89 @@ body { background: var(--surface-hover); } +/* Person field */ +.person-input { + width: 100%; + border: 1px solid var(--border); + border-radius: 6px; + padding: .5rem .75rem; + background: var(--bg); + color: var(--text); + font-family: inherit; + font-size: .95rem; + outline: none; + margin-bottom: .6rem; + transition: border-color .15s; +} + +.person-input:focus { border-color: var(--primary); } +.person-input::placeholder { color: var(--text-muted); } + +/* Toolbar: filter + search on one row */ +.toolbar { + display: flex; + gap: .75rem; + align-items: center; + margin-bottom: 1rem; + flex-wrap: wrap; +} + /* Filter bar */ .filter-bar { display: flex; - gap: 0.5rem; - margin-bottom: 1rem; + gap: .5rem; } +/* Search */ +.search-wrap { + position: relative; + flex: 1; + min-width: 160px; +} + +.search-icon { + position: absolute; + left: .6rem; + top: 50%; + transform: translateY(-50%); + color: var(--text-muted); + font-size: 1.1rem; + pointer-events: none; + line-height: 1; +} + +.search-input { + width: 100%; + border: 1px solid var(--border); + border-radius: 20px; + padding: .38rem .75rem .38rem 1.9rem; + background: var(--surface); + color: var(--text); + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + font-size: .88rem; + outline: none; + transition: border-color .15s; +} + +.search-input:focus { border-color: var(--primary); } +.search-input::placeholder { color: var(--text-muted); } + +.search-clear { + position: absolute; + right: .5rem; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + font-size: .75rem; + padding: .2rem; + line-height: 1; +} + +.search-clear:hover { color: var(--text); } + .filter-btn { padding: 0.4rem 0.9rem; border-radius: 20px; @@ -388,10 +464,38 @@ body { .status-dot.answered { background: var(--answered-dot); } .status-dot.archived { background: var(--text-muted); } +.card-text { + flex: 1; + display: flex; + flex-direction: column; + gap: .15rem; +} + +.person-name { + font-size: .82rem; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + font-weight: 600; + color: var(--primary); + letter-spacing: .01em; +} + +mark.highlight { + background: #fde68a; + color: inherit; + border-radius: 2px; + padding: 0 1px; +} + +@media (prefers-color-scheme: dark) { + mark.highlight { background: #78540a; } +} + +:root[data-theme="dark"] mark.highlight { background: #78540a; } +:root[data-theme="light"] mark.highlight { background: #fde68a; } + .request-text { font-size: 0.97rem; line-height: 1.5; - flex: 1; } .card-meta { diff --git a/src/App.tsx b/src/App.tsx index 29cec39..b52f25c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,6 +5,21 @@ import './App.css'; type Filter = 'active' | 'answered' | 'all'; +function normalize(s: string) { + return s.toLowerCase().trim(); +} + +function matches(prayer: Prayer, query: string) { + const q = normalize(query); + if (!q) return true; + return ( + normalize(prayer.person).includes(q) || + normalize(prayer.request).includes(q) || + normalize(prayer.answer).includes(q) || + prayer.tags.some(t => normalize(t).includes(q)) + ); +} + export default function App() { const { prayers, @@ -21,27 +36,29 @@ export default function App() { const [filter, setFilter] = useState('active'); const [activeTag, setActiveTag] = useState(null); + const [search, setSearch] = useState(''); + const [newPerson, setNewPerson] = useState(''); 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 => { + const filtered = useMemo(() => prayers.filter(p => { 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 searchMatch = matches(p, search); + return statusMatch && tagMatch && searchMatch; + }), [prayers, filter, activeTag, search]); const counts = { active: prayers.filter(p => p.status === 'active').length, @@ -53,7 +70,8 @@ export default function App() { if (!newRequest.trim()) return; const tags = newTags.split(',').map(t => t.trim()).filter(Boolean); try { - await addPrayer(newRequest.trim(), tags); + await addPrayer(newPerson.trim(), newRequest.trim(), tags); + setNewPerson(''); setNewRequest(''); setNewTags(''); setWriteError(null); @@ -77,9 +95,7 @@ export default function App() { function formatDate(iso: string) { return new Date(iso).toLocaleDateString(undefined, { - month: 'short', - day: 'numeric', - year: 'numeric', + month: 'short', day: 'numeric', year: 'numeric', }); } @@ -87,6 +103,14 @@ export default function App() { setActiveTag(prev => prev === tag ? null : tag); } + const emptyMessage = search + ? `No results for "${search}".` + : activeTag + ? `No ${filter === 'all' ? '' : filter + ' '}prayers tagged "${activeTag}".` + : filter === 'active' + ? 'No active prayers. Add one above.' + : 'Nothing here yet.'; + return (
@@ -95,6 +119,12 @@ export default function App() {
+ setNewPerson(e.target.value)} + />