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
|
// Divider paragraph with bottom border
|
||||||
paras.push(new Paragraph({
|
paras.push(new Paragraph({
|
||||||
children: [],
|
children: [],
|
||||||
|
|||||||
+16
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user