From 96c4b541f993158f5f4628d19ed7943a20d8e441 Mon Sep 17 00:00:00 2001 From: nmemmert <67961292+nmemmert@users.noreply.github.com> Date: Tue, 21 Jul 2026 08:39:16 -0400 Subject: [PATCH] Add color-coded calendar dots, prayer notes, and export prayer log MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Each prayer gets a consistent color derived from its ID; calendar dots now use those colors so you can see which prayers were covered at a glance, and the day-detail panel color-codes each entry too - New Notes field on every prayer card for scripture, updates, and mid-prayer thoughts — separate from the answer field, freely editable - Word export now includes Notes and Prayer Log sections for each prayer Co-Authored-By: Claude Sonnet 4.6 --- server.js | 27 +++++++++++++++ src/App.css | 16 +++++++++ src/App.tsx | 84 +++++++++++++++++++++++++++++++++++++++++------ src/types.ts | 1 + src/usePrayers.ts | 4 +++ 5 files changed, 122 insertions(+), 10 deletions(-) diff --git a/server.js b/server.js index b4da116..7a43fb2 100644 --- a/server.js +++ b/server.js @@ -94,6 +94,33 @@ function prayerToSection(prayer, index) { } } + // Notes (if present) + if (prayer.notes && prayer.notes.trim()) { + paras.push(new Paragraph({ + children: [new TextRun({ text: 'Notes', bold: true, size: 20, color: '5A5A2A' })], + spacing: { after: 60 }, + })); + paras.push(new Paragraph({ + children: [new TextRun({ text: prayer.notes, size: 22 })], + spacing: { after: 160 }, + })); + } + + // Prayer log (if present) + if (prayer.prayedLog && prayer.prayedLog.length > 0) { + paras.push(new Paragraph({ + children: [new TextRun({ text: `Prayer Log (${prayer.prayedLog.length} session${prayer.prayedLog.length !== 1 ? 's' : ''})`, bold: true, size: 20, color: '4B6FA8' })], + spacing: { after: 60 }, + })); + const sorted = [...prayer.prayedLog].sort((a, b) => new Date(b.date) - new Date(a.date)); + sorted.forEach(entry => { + const runs = [new TextRun({ text: fmtDate(entry.date), bold: true, size: 19, color: '555555' })]; + if (entry.note) runs.push(new TextRun({ text: ` — ${entry.note}`, size: 19, color: '666666' })); + paras.push(new Paragraph({ children: runs, spacing: { after: 60 } })); + }); + paras.push(new Paragraph({ children: [], spacing: { after: 80 } })); + } + // Divider paragraph with bottom border paras.push(new Paragraph({ children: [], diff --git a/src/App.css b/src/App.css index d4f87e5..a56901b 100644 --- a/src/App.css +++ b/src/App.css @@ -630,6 +630,22 @@ mark.highlight { padding: 0.1rem 0.4rem; } +/* Notes section */ +.notes-section { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding-top: 0.75rem; + border-top: 1px solid var(--border); +} + +.notes-text { + font-size: 0.93rem; + line-height: 1.6; + white-space: pre-wrap; + color: var(--text); +} + /* Prayer log section inside card */ .prayed-log-section { display: flex; diff --git a/src/App.tsx b/src/App.tsx index 9361b28..f03e5b5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,6 +6,17 @@ import './App.css'; type Filter = 'active' | 'answered' | 'all'; type Tab = 'prayers' | 'calendar'; +const DOT_PALETTE = [ + '#c07a3a', '#3d6b52', '#6b4545', '#456b8a', + '#7a5c8a', '#8a7a3a', '#3a6b6b', '#8a4a6b', +]; + +function prayerColor(id: string): string { + let hash = 0; + for (let i = 0; i < id.length; i++) hash = (hash * 31 + id.charCodeAt(i)) & 0xffffffff; + return DOT_PALETTE[Math.abs(hash) % DOT_PALETTE.length]; +} + function normalize(s: string) { return s.toLowerCase().trim(); } @@ -33,6 +44,7 @@ export default function App() { archivePrayer, deletePrayer, updateRequest, + updateNotes, logPrayed, } = usePrayers(); @@ -262,7 +274,8 @@ export default function App() { onArchive={() => archivePrayer(prayer.id)} onDelete={() => deletePrayer(prayer.id)} onEditRequest={text => updateRequest(prayer.id, text)} - onLogPrayed={(entry) => logPrayed(prayer.id, entry)} + onLogPrayed={entry => logPrayed(prayer.id, entry)} + onSaveNotes={notes => updateNotes(prayer.id, notes)} formatDate={formatDate} /> ))} @@ -307,6 +320,7 @@ interface CardProps { onDelete: () => void; onEditRequest: (text: string) => void; onLogPrayed: (entry: PrayedEntry) => void; + onSaveNotes: (notes: string) => void; formatDate: (iso: string) => string; } @@ -326,11 +340,13 @@ function PrayerCard({ onArchive, onDelete, onLogPrayed, + onSaveNotes, formatDate, }: CardProps) { const [logDate, setLogDate] = useState(new Date().toISOString().slice(0, 10)); const [logNote, setLogNote] = useState(''); const [showLogForm, setShowLogForm] = useState(false); + const [editingNotes, setEditingNotes] = useState(null); function handleLogSubmit(e: React.FormEvent) { e.preventDefault(); @@ -339,15 +355,23 @@ function PrayerCard({ setShowLogForm(false); } + function handleNotesSave() { + if (editingNotes === null) return; + onSaveNotes(editingNotes); + setEditingNotes(null); + } + const sortedLog = [...(prayer.prayedLog ?? [])].sort( (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime() ); + const color = prayerColor(prayer.id); + return ( -
  • +
  • - +
    {prayer.person && (

    {highlight(prayer.person, searchQuery)}

    @@ -411,6 +435,39 @@ function PrayerCard({
    )} + {/* Notes section */} +
    +
    + + {editingNotes === null && ( + + )} +
    + {editingNotes !== null ? ( + <> +