167 lines
6.8 KiB
JavaScript
167 lines
6.8 KiB
JavaScript
let journalCurrentDate = '';
|
|
|
|
async function renderJournal() {
|
|
const today = new Date().toISOString().split('T')[0];
|
|
journalCurrentDate = today;
|
|
const content = document.getElementById('pageContent');
|
|
content.innerHTML = `
|
|
<div class="page-header">
|
|
<div class="page-header-left">
|
|
<div class="page-title">Journal</div>
|
|
<div class="page-subtitle">Daily entries and notes — auto-saved to brain/journal/</div>
|
|
</div>
|
|
<div class="btn-group">
|
|
<input class="form-input" type="date" id="journalDatePicker" value="${today}" onchange="loadJournalEntry(this.value)" style="width:160px">
|
|
<button class="btn btn-ghost" onclick="renderJournal()">🔄 Refresh</button>
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-3" style="margin-bottom:16px">
|
|
<div class="metric-tile" style="flex:1">
|
|
<div class="metric-tile-value" id="journalTotalCount">0</div>
|
|
<div class="metric-tile-label">Total Entries</div>
|
|
</div>
|
|
<div class="metric-tile" style="flex:1">
|
|
<div class="metric-tile-value" id="journalWordCount">0</div>
|
|
<div class="metric-tile-label">Words Today</div>
|
|
</div>
|
|
<div class="metric-tile" style="flex:1">
|
|
<div class="metric-tile-value" id="journalStreak">0</div>
|
|
<div class="metric-tile-label">Day Streak</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-3" style="margin-bottom:16px">
|
|
<div style="flex:1;display:flex;gap:8px">
|
|
<input class="form-input" id="journalSearchInput" placeholder="Search entries..." style="flex:1">
|
|
<button class="btn btn-ghost" onclick="searchJournal()">🔍 Search</button>
|
|
</div>
|
|
</div>
|
|
<div class="journal-entry" id="journalEditor">
|
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px">
|
|
<div style="font-weight:600;font-size:14px" id="journalDateLabel">${formatDateDisplay(today)}</div>
|
|
<div>
|
|
<span class="badge badge-info" id="journalSaveStatus">Auto-save on</span>
|
|
</div>
|
|
</div>
|
|
<textarea id="journalText" placeholder="Write your daily entry here... Markdown supported." oninput="scheduleJournalSave()"></textarea>
|
|
</div>
|
|
<div id="journalEntriesList" style="margin-top:16px"></div>
|
|
`;
|
|
loadJournalEntry(today);
|
|
loadJournalEntries();
|
|
}
|
|
|
|
function formatDateDisplay(dateStr) {
|
|
const d = new Date(dateStr + 'T00:00:00');
|
|
return d.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
|
}
|
|
|
|
let journalSaveTimer = null;
|
|
function scheduleJournalSave() {
|
|
if (journalSaveTimer) clearTimeout(journalSaveTimer);
|
|
journalSaveTimer = setTimeout(saveJournalEntry, 2000);
|
|
const status = document.getElementById('journalSaveStatus');
|
|
if (status) status.textContent = 'Unsaved changes...';
|
|
}
|
|
|
|
async function saveJournalEntry() {
|
|
const textarea = document.getElementById('journalText');
|
|
if (!textarea) return;
|
|
const content = textarea.value;
|
|
const date = journalCurrentDate;
|
|
try {
|
|
await api.saveJournalEntry(date, content);
|
|
const status = document.getElementById('journalSaveStatus');
|
|
if (status) status.textContent = 'Saved ✓';
|
|
const words = content.trim() ? content.trim().split(/\s+/).length : 0;
|
|
const wc = document.getElementById('journalWordCount');
|
|
if (wc) wc.textContent = words;
|
|
} catch (err) {
|
|
const status = document.getElementById('journalSaveStatus');
|
|
if (status) status.textContent = 'Save failed: ' + err.message;
|
|
console.error('Journal save error:', err);
|
|
}
|
|
}
|
|
|
|
async function loadJournalEntry(date) {
|
|
journalCurrentDate = date;
|
|
const label = document.getElementById('journalDateLabel');
|
|
if (label) label.textContent = formatDateDisplay(date);
|
|
const textarea = document.getElementById('journalText');
|
|
if (!textarea) return;
|
|
try {
|
|
const data = await api.getJournalEntry(date);
|
|
textarea.value = data.content || '';
|
|
const words = data.content ? data.content.trim().split(/\s+/).length : 0;
|
|
const wc = document.getElementById('journalWordCount');
|
|
if (wc) wc.textContent = words;
|
|
const status = document.getElementById('journalSaveStatus');
|
|
if (status) status.textContent = 'Loaded ✓';
|
|
} catch (err) {
|
|
textarea.value = '';
|
|
}
|
|
}
|
|
|
|
async function loadJournalEntries() {
|
|
try {
|
|
const data = await api.getJournalEntries();
|
|
const entries = data.entries || [];
|
|
const totalEl = document.getElementById('journalTotalCount');
|
|
if (totalEl) totalEl.textContent = entries.length;
|
|
let streak = 0;
|
|
const today = new Date();
|
|
for (let i = 0; i < 365; i++) {
|
|
const d = new Date(today);
|
|
d.setDate(d.getDate() - i);
|
|
const ds = d.toISOString().split('T')[0];
|
|
if (entries.some(e => e.date === ds)) { streak++; }
|
|
else if (i > 0) break;
|
|
}
|
|
const streakEl = document.getElementById('journalStreak');
|
|
if (streakEl) streakEl.textContent = streak;
|
|
const list = document.getElementById('journalEntriesList');
|
|
if (!list) return;
|
|
if (entries.length === 0) {
|
|
list.innerHTML = `<div class="empty-state"><div class="empty-state-icon">📓</div><div class="empty-state-title">No journal entries yet</div><div class="empty-state-desc">Write your first entry above</div></div>`;
|
|
return;
|
|
}
|
|
list.innerHTML = `
|
|
<div class="section-title">Recent Entries</div>
|
|
<div style="display:flex;flex-wrap:wrap;gap:8px">
|
|
${entries.slice(0, 30).map(e => `
|
|
<button class="btn btn-sm ${e.date === journalCurrentDate ? 'btn-primary' : 'btn-ghost'}" onclick="loadJournalEntry('${e.date}');document.getElementById('journalDatePicker').value='${e.date}'">
|
|
${e.date}
|
|
</button>
|
|
`).join('')}
|
|
</div>
|
|
`;
|
|
} catch (err) {
|
|
showToast('Failed to load entries: ' + err.message, 'error');
|
|
}
|
|
}
|
|
|
|
async function searchJournal() {
|
|
const q = document.getElementById('journalSearchInput').value.trim();
|
|
if (!q) { loadJournalEntries(); return; }
|
|
try {
|
|
const data = await api.searchJournal(q);
|
|
const results = data.results || [];
|
|
const list = document.getElementById('journalEntriesList');
|
|
if (!list) return;
|
|
if (results.length === 0) {
|
|
list.innerHTML = `<div class="empty-state"><div class="empty-state-icon">🔍</div><div class="empty-state-title">No results for "${escapeHtml(q)}"</div></div>`;
|
|
return;
|
|
}
|
|
list.innerHTML = `
|
|
<div class="section-title">Results for "${escapeHtml(q)}"</div>
|
|
${results.map(r => `
|
|
<div class="card" style="cursor:pointer;margin-bottom:8px" onclick="loadJournalEntry('${r.date}');document.getElementById('journalDatePicker').value='${r.date}'">
|
|
<div style="font-size:13px;font-weight:600;margin-bottom:4px">${r.date}</div>
|
|
<div class="text-muted text-sm">${escapeHtml(r.preview)}</div>
|
|
</div>
|
|
`).join('')}
|
|
`;
|
|
} catch (err) {
|
|
showToast('Search failed: ' + err.message, 'error');
|
|
}
|
|
}
|