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:
1 parent
57937d986d
commit
96c4b541f9
5 files changed
+122
-10
No files matched your search
@@ -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: [],
|
||||
|
||||
+16
@@ -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;
|
||||
|
||||
+74
-10
@@ -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<string | null>(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 (
|
||||
<li className={`prayer-card ${prayer.status}`}>
|
||||
<li className={`prayer-card ${prayer.status}`} style={{ borderLeftColor: color }}>
|
||||
<div className="card-top" onClick={onToggle}>
|
||||
<div className="card-main">
|
||||
<span className={`status-dot ${prayer.status}`} />
|
||||
<span className="status-dot" style={{ background: color }} />
|
||||
<div className="card-text">
|
||||
{prayer.person && (
|
||||
<p className="person-name">{highlight(prayer.person, searchQuery)}</p>
|
||||
@@ -411,6 +435,39 @@ function PrayerCard({
|
||||
</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-header">
|
||||
<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 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 => {
|
||||
(prayer.prayedLog ?? []).forEach(entry => {
|
||||
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();
|
||||
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 (
|
||||
<div
|
||||
key={day}
|
||||
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>
|
||||
{entries && (
|
||||
{uniqueColors.length > 0 && (
|
||||
<span className="cal-dot-row">
|
||||
{entries.slice(0, 3).map((_, di) => (
|
||||
<span key={di} className="cal-dot" />
|
||||
{uniqueColors.slice(0, 5).map((color, di) => (
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
@@ -582,7 +646,7 @@ function CalendarView({ prayers, formatDate }: { prayers: Prayer[]; formatDate:
|
||||
) : (
|
||||
<ul className="cal-detail-list">
|
||||
{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">
|
||||
{prayer.person && <span className="person-name">{prayer.person} — </span>}
|
||||
<span className="request-text">{prayer.request}</span>
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface Prayer {
|
||||
person: string;
|
||||
request: string;
|
||||
answer: string;
|
||||
notes?: string;
|
||||
status: 'active' | 'answered' | 'archived';
|
||||
createdAt: string;
|
||||
answeredAt?: string;
|
||||
|
||||
@@ -65,6 +65,9 @@ export function usePrayers() {
|
||||
const updateRequest = useCallback((id: string, request: string) =>
|
||||
patch(id, { request }), [patch]);
|
||||
|
||||
const updateNotes = useCallback((id: string, notes: string) =>
|
||||
patch(id, { notes }), [patch]);
|
||||
|
||||
const logPrayed = useCallback(async (id: string, entry: PrayedEntry) => {
|
||||
const prayer = prayers.find(p => p.id === id);
|
||||
if (!prayer) return;
|
||||
@@ -88,6 +91,7 @@ export function usePrayers() {
|
||||
archivePrayer,
|
||||
deletePrayer,
|
||||
updateRequest,
|
||||
updateNotes,
|
||||
logPrayed,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user