- Copy config.py to config.example.py as template (tracked in git) - Add config.py to .gitignore (local config per developer) - Update README.md with configuration setup instructions - Fix outdated references (TinyDB -> SQLite, add missing files) - Persist goal selection across task and note pages
244 lines
8.0 KiB
JavaScript
244 lines
8.0 KiB
JavaScript
let notes = [];
|
|
let goals = [];
|
|
let editingNoteId = null;
|
|
let filterTimer = null;
|
|
let currentUser = null;
|
|
let savedGoalId = null;
|
|
|
|
async function loadNotes() {
|
|
const goalId = document.getElementById("filter-goal").value;
|
|
const search = document.getElementById("filter-search").value.trim();
|
|
|
|
let params = new URLSearchParams();
|
|
if (goalId) params.set("goal_id", goalId);
|
|
if (search) params.set("search", search);
|
|
|
|
try {
|
|
notes = await get(`/api/notes?${params.toString()}`);
|
|
renderNotes();
|
|
} catch (error) {
|
|
console.error("Failed to load notes:", error);
|
|
}
|
|
}
|
|
|
|
async function loadGoals() {
|
|
try {
|
|
goals = await get("/api/goals");
|
|
currentUser = await get("/api/auth/me");
|
|
savedGoalId = currentUser.selected_goal_id;
|
|
populateGoalSelectors();
|
|
} catch (error) {
|
|
console.error("Failed to load goals:", error);
|
|
}
|
|
}
|
|
|
|
function populateGoalSelectors() {
|
|
const filterSelect = document.getElementById("filter-goal");
|
|
const modalSelect = document.getElementById("note-goal");
|
|
|
|
const activated = goals.filter(g => g.activated);
|
|
const options = activated.map(g =>
|
|
`<option value="${g.id}">${escapeHtml(g.title)}</option>`
|
|
).join("");
|
|
|
|
const savedGoalExists = savedGoalId && activated.some(g => g.id === savedGoalId);
|
|
|
|
filterSelect.innerHTML = `<option value="">All Goals</option>` + options;
|
|
if (savedGoalExists) {
|
|
filterSelect.value = savedGoalId;
|
|
}
|
|
|
|
modalSelect.innerHTML = `<option value="">None</option>` + options;
|
|
}
|
|
|
|
function renderNotes() {
|
|
const container = document.getElementById("notes-list");
|
|
|
|
if (notes.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="empty-state">
|
|
<h3>No notes yet</h3>
|
|
<p>Create your first note to get started!</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = notes.map(note => {
|
|
const snippet = (note.content || "").replace(/[#*`\[\]()>|~_-]/g, "").substring(0, 120);
|
|
const time = formatTime(note.updated_at);
|
|
let link = "";
|
|
if (note.task_title) {
|
|
link = `<span class="note-card-link">${escapeHtml(note.goal_title)} / ${escapeHtml(note.task_title)}</span>`;
|
|
} else if (note.goal_title) {
|
|
link = `<span class="note-card-link">${escapeHtml(note.goal_title)}</span>`;
|
|
}
|
|
|
|
return `
|
|
<div class="note-card" onclick="openNote(${note.id})">
|
|
<div class="note-card-title">${escapeHtml(note.title)}</div>
|
|
<div class="note-card-meta">
|
|
<span>${time}</span>
|
|
${link}
|
|
</div>
|
|
<div class="note-card-snippet">${escapeHtml(snippet)}</div>
|
|
</div>
|
|
`;
|
|
}).join("");
|
|
}
|
|
|
|
async function openNote(noteId) {
|
|
editingNoteId = noteId;
|
|
const note = notes.find(n => n.id === noteId);
|
|
if (!note) return;
|
|
|
|
document.getElementById("note-modal-title").textContent = "Edit Note";
|
|
document.getElementById("note-id").value = note.id;
|
|
document.getElementById("note-title").value = note.title;
|
|
document.getElementById("note-content").value = note.content || "";
|
|
document.getElementById("note-goal").value = note.goal_id || "";
|
|
document.getElementById("note-error").textContent = "";
|
|
|
|
await populateTasks(note.goal_id);
|
|
document.getElementById("note-task").value = note.task_id || "";
|
|
|
|
document.getElementById("delete-note-btn").style.display = "inline-block";
|
|
updatePreview();
|
|
document.getElementById("note-modal").classList.add("active");
|
|
}
|
|
|
|
async function openNewNote() {
|
|
editingNoteId = null;
|
|
document.getElementById("note-modal-title").textContent = "New Note";
|
|
document.getElementById("note-id").value = "";
|
|
document.getElementById("note-title").value = "";
|
|
document.getElementById("note-content").value = "";
|
|
document.getElementById("note-goal").value = savedGoalId || "";
|
|
document.getElementById("note-error").textContent = "";
|
|
document.getElementById("delete-note-btn").style.display = "none";
|
|
updatePreview();
|
|
document.getElementById("note-modal").classList.add("active");
|
|
|
|
if (savedGoalId) {
|
|
await populateTasks(savedGoalId);
|
|
const goal = goals.find(g => g.id === savedGoalId);
|
|
if (goal && goal.selected_task_id) {
|
|
document.getElementById("note-task").value = goal.selected_task_id;
|
|
}
|
|
} else {
|
|
document.getElementById("note-task").innerHTML = '<option value="">None</option>';
|
|
}
|
|
}
|
|
|
|
function closeNoteModal() {
|
|
document.getElementById("note-modal").classList.remove("active");
|
|
editingNoteId = null;
|
|
}
|
|
|
|
async function populateTasks(goalId) {
|
|
const select = document.getElementById("note-task");
|
|
select.innerHTML = '<option value="">None</option>';
|
|
if (!goalId) return;
|
|
|
|
try {
|
|
const tasks = await get(`/api/tasks?goal_id=${goalId}`);
|
|
select.innerHTML += tasks.map(t =>
|
|
`<option value="${t.id}">${escapeHtml(t.title)}</option>`
|
|
).join("");
|
|
} catch (error) {
|
|
console.error("Failed to load tasks:", error);
|
|
}
|
|
}
|
|
|
|
function updatePreview() {
|
|
const content = document.getElementById("note-content").value;
|
|
const preview = document.getElementById("note-preview");
|
|
try {
|
|
preview.innerHTML = marked.parse(content || "");
|
|
} catch (e) {
|
|
preview.innerHTML = escapeHtml(content || "");
|
|
}
|
|
}
|
|
|
|
async function handleNoteSubmit(event) {
|
|
event.preventDefault();
|
|
const error = document.getElementById("note-error");
|
|
error.textContent = "";
|
|
|
|
const title = document.getElementById("note-title").value.trim();
|
|
if (!title) {
|
|
error.textContent = "Title is required";
|
|
return;
|
|
}
|
|
|
|
const goalId = parseInt(document.getElementById("note-goal").value) || null;
|
|
const taskId = parseInt(document.getElementById("note-task").value) || null;
|
|
const content = document.getElementById("note-content").value;
|
|
|
|
try {
|
|
if (editingNoteId) {
|
|
await put(`/api/notes/${editingNoteId}`, { title, content });
|
|
} else {
|
|
await post("/api/notes", { goal_id: goalId, task_id: taskId, title, content });
|
|
}
|
|
closeNoteModal();
|
|
await loadNotes();
|
|
} catch (err) {
|
|
error.textContent = err.message;
|
|
}
|
|
}
|
|
|
|
async function deleteNote() {
|
|
if (!editingNoteId) return;
|
|
if (!confirm("Delete this note?")) return;
|
|
|
|
try {
|
|
await del(`/api/notes/${editingNoteId}`);
|
|
closeNoteModal();
|
|
await loadNotes();
|
|
} catch (error) {
|
|
console.error("Failed to delete note:", error);
|
|
}
|
|
}
|
|
|
|
function formatTime(isoString) {
|
|
if (!isoString) return "";
|
|
const date = new Date(isoString);
|
|
return date.toLocaleString();
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
document.getElementById("create-note-btn").addEventListener("click", openNewNote);
|
|
document.getElementById("note-modal-close").addEventListener("click", closeNoteModal);
|
|
document.getElementById("note-modal-cancel").addEventListener("click", closeNoteModal);
|
|
document.getElementById("note-form").addEventListener("submit", handleNoteSubmit);
|
|
document.getElementById("delete-note-btn").addEventListener("click", deleteNote);
|
|
|
|
document.getElementById("note-content").addEventListener("input", updatePreview);
|
|
|
|
document.getElementById("note-goal").addEventListener("change", async (e) => {
|
|
const goalId = parseInt(e.target.value) || null;
|
|
savedGoalId = goalId;
|
|
await patch("/api/user/selected-goal", { goal_id: goalId });
|
|
populateTasks(goalId);
|
|
});
|
|
|
|
document.getElementById("filter-goal").addEventListener("change", () => {
|
|
loadNotes();
|
|
});
|
|
|
|
document.getElementById("filter-search").addEventListener("input", () => {
|
|
clearTimeout(filterTimer);
|
|
filterTimer = setTimeout(loadNotes, 300);
|
|
});
|
|
|
|
loadGoals();
|
|
loadNotes();
|
|
});
|