Add delete & edit for project panel entries

- deleteProjectEntry(id) and updateProjectEntry(id, text) DB helpers on project_entries store
- Edit toggle button beside Add Entry; switches to Done when active, resets on panel open/close
- In edit mode, panel-sourced entries show ✎ (inline edit) and × (delete) action buttons
- Delete re-renders timeline and updates breakout card count
- Inline edit saves on Enter/blur, cancels on Escape

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
JasonBrock
2026-04-28 13:32:08 -04:00
parent 5d35dcfe03
commit 72dc0a8d9a
+166 -1
View File
@@ -538,6 +538,7 @@
.side-panel-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 8px;
}
@@ -556,6 +557,7 @@
gap: 8px;
padding: 6px 0;
border-bottom: 1px solid var(--bg-accent);
align-items: flex-start;
}
.panel-entry:last-child { border-bottom: none; }
@@ -573,6 +575,60 @@
color: var(--text-primary);
line-height: 1.45;
word-break: break-word;
flex: 1;
}
.panel-entry-body textarea {
width: 100%;
background: var(--bg-accent);
border: 1px solid var(--accent);
border-radius: 4px;
color: var(--text-primary);
font-size: 0.85rem;
line-height: 1.45;
padding: 4px 6px;
resize: vertical;
min-height: 48px;
box-sizing: border-box;
}
.panel-entry-actions {
display: flex;
gap: 4px;
flex-shrink: 0;
padding-top: 1px;
}
.panel-entry-action-btn {
background: none;
border: none;
cursor: pointer;
padding: 2px 5px;
border-radius: 4px;
font-size: 0.9rem;
line-height: 1;
color: var(--text-secondary);
transition: background 0.15s, color 0.15s;
}
.panel-entry-action-btn:hover { background: var(--bg-accent); color: var(--text-primary); }
.panel-entry-action-btn.del:hover { color: #e05252; }
.panel-edit-mode-btn {
background: transparent;
border: 1px solid var(--text-secondary);
color: var(--text-secondary);
border-radius: 6px;
padding: 8px 14px;
font-size: 0.85rem;
cursor: pointer;
transition: border-color 0.15s, color 0.15s;
}
.panel-edit-mode-btn:hover,
.panel-edit-mode-btn.active {
border-color: var(--accent);
color: var(--accent);
}
/* ── Breakout card in main timeline ─────────────────────── */
@@ -1481,6 +1537,7 @@
<div class="side-panel-input">
<textarea id="panelEntryInput" placeholder="Add note to project… (Ctrl+Enter to save)"></textarea>
<div class="side-panel-actions">
<button id="panelEditModeBtn" class="panel-edit-mode-btn">Edit</button>
<button id="panelSubmitBtn" class="submit-btn">Add Entry</button>
</div>
</div>
@@ -2068,6 +2125,10 @@
async function openProjectPanel(projectId, projectName) {
activePanelProjectId = projectId;
panelEditMode = false;
const editBtn = document.getElementById('panelEditModeBtn');
editBtn.textContent = 'Edit';
editBtn.classList.remove('active');
document.getElementById('sidePanelTitle').textContent = '🔗 ' + projectName;
document.getElementById('sidePanel').style.display = 'flex';
document.querySelector('main').classList.add('main--panel-open');
@@ -2079,8 +2140,14 @@
document.getElementById('sidePanel').style.display = 'none';
document.querySelector('main').classList.remove('main--panel-open');
activePanelProjectId = null;
panelEditMode = false;
const editBtn = document.getElementById('panelEditModeBtn');
editBtn.textContent = 'Edit';
editBtn.classList.remove('active');
}
let panelEditMode = false;
async function renderProjectTimeline(projectId) {
const container = document.getElementById('sidePanelTimeline');
container.innerHTML = '<div style="color:var(--text-secondary);font-size:0.85rem;padding:8px 0">Loading…</div>';
@@ -2119,9 +2186,16 @@
const sourceLabel = e._source === 'timeline'
? `<span style="font-size:0.7rem;color:var(--text-secondary);margin-left:4px">[journal]</span>`
: '';
return `<div class="panel-entry">
const actions = (panelEditMode && e._source === 'panel')
? `<div class="panel-entry-actions">
<button class="panel-entry-action-btn" data-action="edit" title="Edit">✎</button>
<button class="panel-entry-action-btn del" data-action="delete" title="Delete">×</button>
</div>`
: '';
return `<div class="panel-entry" data-id="${e.id}" data-source="${e._source}" data-text="${escapeHtml(e.text)}">
<div class="panel-entry-time">${time}${sourceLabel}</div>
<div class="panel-entry-body">${mood}${escapeHtml(e.text)}${tags ? '<div style="margin-top:4px">'+tags+'</div>' : ''}</div>
${actions}
</div>`;
}).join('');
return `<div class="panel-date-group"><div class="panel-date-label">${label}</div>${rows}</div>`;
@@ -2277,6 +2351,33 @@
});
}
function deleteProjectEntry(id) {
return new Promise((resolve, reject) => {
const tx = db.transaction(PROJECT_ENTRIES_STORE, 'readwrite');
const store = tx.objectStore(PROJECT_ENTRIES_STORE);
const req = store.delete(id);
req.onsuccess = () => resolve();
req.onerror = () => reject(req.error);
});
}
function updateProjectEntry(id, text) {
return new Promise((resolve, reject) => {
const tx = db.transaction(PROJECT_ENTRIES_STORE, 'readwrite');
const store = tx.objectStore(PROJECT_ENTRIES_STORE);
const getReq = store.get(id);
getReq.onsuccess = () => {
const entry = getReq.result;
if (!entry) { reject(new Error('Entry not found')); return; }
entry.text = text;
const putReq = store.put(entry);
putReq.onsuccess = () => resolve();
putReq.onerror = () => reject(putReq.error);
};
getReq.onerror = () => reject(getReq.error);
});
}
function getProjectEntries(projectId) {
return new Promise((resolve, reject) => {
const tx = db.transaction(PROJECT_ENTRIES_STORE, 'readonly');
@@ -2906,6 +3007,70 @@
if (e.key === 'Enter' && e.ctrlKey) { e.preventDefault(); if (dbReady) panelSubmit(); }
});
document.getElementById('panelEditModeBtn').addEventListener('click', () => {
panelEditMode = !panelEditMode;
const btn = document.getElementById('panelEditModeBtn');
btn.textContent = panelEditMode ? 'Done' : 'Edit';
btn.classList.toggle('active', panelEditMode);
if (activePanelProjectId != null) renderProjectTimeline(activePanelProjectId);
});
document.getElementById('sidePanelTimeline').addEventListener('click', async e => {
const actionBtn = e.target.closest('[data-action]');
if (!actionBtn) return;
const entry = actionBtn.closest('.panel-entry');
if (!entry || entry.dataset.source !== 'panel') return;
const id = parseInt(entry.dataset.id);
if (actionBtn.dataset.action === 'delete') {
try {
await deleteProjectEntry(id);
await renderProjectTimeline(activePanelProjectId);
const countEl = document.querySelector(`.breakout-card[data-project-id="${activePanelProjectId}"] .breakout-card-count`);
if (countEl) {
const count = await getCombinedProjectEntryCount(activePanelProjectId);
countEl.textContent = `${count} ${count === 1 ? 'entry' : 'entries'}`;
}
showToast('Entry deleted');
} catch (err) {
console.error('Failed to delete project entry:', err);
showToast('Error deleting entry');
}
return;
}
if (actionBtn.dataset.action === 'edit') {
const body = entry.querySelector('.panel-entry-body');
const currentText = entry.dataset.text;
body.innerHTML = `<textarea>${escapeHtml(currentText)}</textarea>`;
const textarea = body.querySelector('textarea');
textarea.focus();
textarea.setSelectionRange(textarea.value.length, textarea.value.length);
const save = async () => {
const newText = textarea.value.trim();
if (!newText || newText === currentText) {
await renderProjectTimeline(activePanelProjectId);
return;
}
try {
await updateProjectEntry(id, newText);
await renderProjectTimeline(activePanelProjectId);
showToast('Entry updated');
} catch (err) {
console.error('Failed to update project entry:', err);
showToast('Error updating entry');
}
};
textarea.addEventListener('blur', save, { once: true });
textarea.addEventListener('keydown', ke => {
if (ke.key === 'Enter' && !ke.shiftKey) { ke.preventDefault(); textarea.removeEventListener('blur', save); save(); }
if (ke.key === 'Escape') { textarea.removeEventListener('blur', save); renderProjectTimeline(activePanelProjectId); }
});
}
});
init();
</script>
</body>