Add color-coded calendar dots, prayer notes, and export prayer log

- 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 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-07-21 08:39:16 -04:00
1 parent 57937d986d
commit 96c4b541f9
5 files changed
+122 -10

No files matched your search

+27
View File
@@ -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 // Divider paragraph with bottom border
paras.push(new Paragraph({ paras.push(new Paragraph({
children: [], children: [],
+16
View File
@@ -630,6 +630,22 @@ mark.highlight {
padding: 0.1rem 0.4rem; 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 */ /* Prayer log section inside card */
.prayed-log-section { .prayed-log-section {
display: flex; display: flex;
+74 -10
View File
@@ -6,6 +6,17 @@ import './App.css';
type Filter = 'active' | 'answered' | 'all'; type Filter = 'active' | 'answered' | 'all';
type Tab = 'prayers' | 'calendar'; 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) { function normalize(s: string) {
return s.toLowerCase().trim(); return s.toLowerCase().trim();
} }
@@ -33,6 +44,7 @@ export default function App() {
archivePrayer, archivePrayer,
deletePrayer, deletePrayer,
updateRequest, updateRequest,
updateNotes,
logPrayed, logPrayed,
} = usePrayers(); } = usePrayers();
@@ -262,7 +274,8 @@ export default function App() {
onArchive={() => archivePrayer(prayer.id)} onArchive={() => archivePrayer(prayer.id)}
onDelete={() => deletePrayer(prayer.id)} onDelete={() => deletePrayer(prayer.id)}
onEditRequest={text => updateRequest(prayer.id, text)} 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} formatDate={formatDate}
/> />
))} ))}
@@ -307,6 +320,7 @@ interface CardProps {
onDelete: () => void; onDelete: () => void;
onEditRequest: (text: string) => void; onEditRequest: (text: string) => void;
onLogPrayed: (entry: PrayedEntry) => void; onLogPrayed: (entry: PrayedEntry) => void;
onSaveNotes: (notes: string) => void;
formatDate: (iso: string) => string; formatDate: (iso: string) => string;
} }
@@ -326,11 +340,13 @@ function PrayerCard({
onArchive, onArchive,
onDelete, onDelete,
onLogPrayed, onLogPrayed,
onSaveNotes,
formatDate, formatDate,
}: CardProps) { }: CardProps) {
const [logDate, setLogDate] = useState(new Date().toISOString().slice(0, 10)); const [logDate, setLogDate] = useState(new Date().toISOString().slice(0, 10));
const [logNote, setLogNote] = useState(''); const [logNote, setLogNote] = useState('');
const [showLogForm, setShowLogForm] = useState(false); const [showLogForm, setShowLogForm] = useState(false);
const [editingNotes, setEditingNotes] = useState<string | null>(null);
function handleLogSubmit(e: React.FormEvent) { function handleLogSubmit(e: React.FormEvent) {
e.preventDefault(); e.preventDefault();
@@ -339,15 +355,23 @@ function PrayerCard({
setShowLogForm(false); setShowLogForm(false);
} }
function handleNotesSave() {
if (editingNotes === null) return;
onSaveNotes(editingNotes);
setEditingNotes(null);
}
const sortedLog = [...(prayer.prayedLog ?? [])].sort( const sortedLog = [...(prayer.prayedLog ?? [])].sort(
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime() (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
); );
const color = prayerColor(prayer.id);
return ( return (
<li className={`prayer-card ${prayer.status}`}> <li className={`prayer-card ${prayer.status}`} style={{ borderLeftColor: color }}>
<div className="card-top" onClick={onToggle}> <div className="card-top" onClick={onToggle}>
<div className="card-main"> <div className="card-main">
<span className={`status-dot ${prayer.status}`} /> <span className="status-dot" style={{ background: color }} />
<div className="card-text"> <div className="card-text">
{prayer.person && ( {prayer.person && (
<p className="person-name">{highlight(prayer.person, searchQuery)}</p> <p className="person-name">{highlight(prayer.person, searchQuery)}</p>
@@ -411,6 +435,39 @@ function PrayerCard({
</div> </div>
)} )}
{/* Notes section */}
<div className="notes-section">
<div className="prayed-log-header">
<label className="answer-label">Notes</label>
{editingNotes === null && (
<button className="btn btn-sm" onClick={() => setEditingNotes(prayer.notes ?? '')}>
{prayer.notes ? 'Edit' : '+ Add notes'}
</button>
)}
</div>
{editingNotes !== null ? (
<>
<textarea
className="log-note-input"
placeholder="Scripture, updates, mid-prayer thoughts…"
value={editingNotes}
onChange={e => setEditingNotes(e.target.value)}
rows={4}
autoFocus
/>
<div className="card-actions">
<button className="btn btn-primary btn-sm" onClick={handleNotesSave}>Save</button>
<button className="btn btn-sm" onClick={() => setEditingNotes(null)}>Cancel</button>
</div>
</>
) : prayer.notes ? (
<p className="notes-text">{prayer.notes}</p>
) : (
<p className="prayed-log-empty">No notes yet.</p>
)}
</div>
{/* Prayer log section */}
<div className="prayed-log-section"> <div className="prayed-log-section">
<div className="prayed-log-header"> <div className="prayed-log-header">
<label className="answer-label">Prayer Log</label> <label className="answer-label">Prayer Log</label>
@@ -482,7 +539,7 @@ function CalendarView({ prayers, formatDate }: { prayers: Prayer[]; formatDate:
const [selectedDate, setSelectedDate] = useState<string | null>(null); const [selectedDate, setSelectedDate] = useState<string | null>(null);
const prayedByDate = useMemo(() => { const prayedByDate = useMemo(() => {
const map = new Map<string, Array<{ prayer: Prayer; entry: PrayedEntry }>>(); const map = new Map<string, Array<{ prayer: Prayer; entry: PrayedEntry }>>() ;
prayers.forEach(prayer => { prayers.forEach(prayer => {
(prayer.prayedLog ?? []).forEach(entry => { (prayer.prayedLog ?? []).forEach(entry => {
const key = entry.date.slice(0, 10); const key = entry.date.slice(0, 10);
@@ -552,19 +609,26 @@ function CalendarView({ prayers, formatDate }: { prayers: Prayer[]; formatDate:
day === today.getDate() && month === today.getMonth() && year === today.getFullYear(); day === today.getDate() && month === today.getMonth() && year === today.getFullYear();
const isSelected = selectedDate === dateStr; const isSelected = selectedDate === dateStr;
// Deduplicate colors so each prayer shows one dot per day
const uniqueColors = entries
? Array.from(new Map(entries.map(e => [e.prayer.id, prayerColor(e.prayer.id)])).values())
: [];
return ( return (
<div <div
key={day} key={day}
className={`cal-cell ${isToday ? 'cal-today' : ''} ${isSelected ? 'cal-selected' : ''} ${entries ? 'cal-has-prayers' : ''}`} className={`cal-cell ${isToday ? 'cal-today' : ''} ${isSelected ? 'cal-selected' : ''} ${entries ? 'cal-has-prayers' : ''}`}
onClick={() => setSelectedDate(isSelected ? null : dateStr)} onClick={() => entries && setSelectedDate(isSelected ? null : dateStr)}
> >
<span className="cal-day-num">{day}</span> <span className="cal-day-num">{day}</span>
{entries && ( {uniqueColors.length > 0 && (
<span className="cal-dot-row"> <span className="cal-dot-row">
{entries.slice(0, 3).map((_, di) => ( {uniqueColors.slice(0, 5).map((color, di) => (
<span key={di} className="cal-dot" /> <span key={di} className="cal-dot" style={{ background: color }} />
))} ))}
{entries.length > 3 && <span className="cal-dot-more">+{entries.length - 3}</span>} {uniqueColors.length > 5 && (
<span className="cal-dot-more">+{uniqueColors.length - 5}</span>
)}
</span> </span>
)} )}
</div> </div>
@@ -582,7 +646,7 @@ function CalendarView({ prayers, formatDate }: { prayers: Prayer[]; formatDate:
) : ( ) : (
<ul className="cal-detail-list"> <ul className="cal-detail-list">
{selectedEntries.map(({ prayer, entry }, i) => ( {selectedEntries.map(({ prayer, entry }, i) => (
<li key={i} className="cal-detail-entry"> <li key={i} className="cal-detail-entry" style={{ borderLeftColor: prayerColor(prayer.id) }}>
<div className="cal-detail-prayer"> <div className="cal-detail-prayer">
{prayer.person && <span className="person-name">{prayer.person} — </span>} {prayer.person && <span className="person-name">{prayer.person} — </span>}
<span className="request-text">{prayer.request}</span> <span className="request-text">{prayer.request}</span>
+1
View File
@@ -8,6 +8,7 @@ export interface Prayer {
person: string; person: string;
request: string; request: string;
answer: string; answer: string;
notes?: string;
status: 'active' | 'answered' | 'archived'; status: 'active' | 'answered' | 'archived';
createdAt: string; createdAt: string;
answeredAt?: string; answeredAt?: string;
+4
View File
@@ -65,6 +65,9 @@ export function usePrayers() {
const updateRequest = useCallback((id: string, request: string) => const updateRequest = useCallback((id: string, request: string) =>
patch(id, { request }), [patch]); patch(id, { request }), [patch]);
const updateNotes = useCallback((id: string, notes: string) =>
patch(id, { notes }), [patch]);
const logPrayed = useCallback(async (id: string, entry: PrayedEntry) => { const logPrayed = useCallback(async (id: string, entry: PrayedEntry) => {
const prayer = prayers.find(p => p.id === id); const prayer = prayers.find(p => p.id === id);
if (!prayer) return; if (!prayer) return;
@@ -88,6 +91,7 @@ export function usePrayers() {
archivePrayer, archivePrayer,
deletePrayer, deletePrayer,
updateRequest, updateRequest,
updateNotes,
logPrayed, logPrayed,
}; };
} }