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
You need to enable JavaScript to run this app.
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 => (
+ handleTagClick(tag)}
+ >
+ {tag}
+
+ ))}
+ {activeTag && (
+ setActiveTag(null)}>
+ ✕ Clear
+
+ )}
+
+ )}
+
{(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 ? '▲' : '▼'}