mirror of
https://github.com/zoldar/jenot.git
synced 2026-01-03 14:32:54 +00:00
357 lines
9.3 KiB
JavaScript
357 lines
9.3 KiB
JavaScript
import "./service-worker-init.js";
|
|
import { renderText } from "./dom.js";
|
|
import { SyncedNoteStore } from "./synced-store.js";
|
|
import {
|
|
notificationsAvailable,
|
|
authorizeNotifications,
|
|
notificationsEnabled,
|
|
sendNotification,
|
|
} from "./notifications.js";
|
|
import "./components.js";
|
|
import { reminderLabel } from "./reminders.js";
|
|
import { Router } from "./router.js";
|
|
|
|
// Cookie presence determines login state
|
|
const isLoggedIn = !!document.cookie
|
|
.split("; ")
|
|
.find((row) => row.startsWith("jenot_pub="));
|
|
|
|
// Notes storage configuration.
|
|
// The storage is a combination of IndexedDB + network sync.
|
|
// Network sync is only enabled is user is logged in.
|
|
const endpoint = isLoggedIn ? "/" : null;
|
|
const Notes = new SyncedNoteStore("jenot-app", endpoint);
|
|
|
|
let view = "home";
|
|
|
|
const routes = [
|
|
{
|
|
path: "index.html#home",
|
|
callback: () => {
|
|
view = "home";
|
|
render();
|
|
},
|
|
},
|
|
{
|
|
path: "index.html#reminders",
|
|
callback: () => {
|
|
view = "reminders";
|
|
render();
|
|
},
|
|
},
|
|
{ path: "*", callback: (router) => router.redirect("index.html#home") },
|
|
];
|
|
|
|
async function resetApp() {
|
|
await window.navigator.serviceWorker
|
|
.getRegistration()
|
|
.then((r) => r.unregister());
|
|
await caches
|
|
.keys()
|
|
.then((keys) => Promise.all(keys.map((k) => caches.delete(k))));
|
|
window.location.reload();
|
|
}
|
|
|
|
document.querySelector("#reset-app").addEventListener("click", resetApp);
|
|
|
|
const URL_PARAMS = new URLSearchParams(window.location.search);
|
|
|
|
await Notes.init();
|
|
|
|
// Reset metadata to force full sync
|
|
if (URL_PARAMS.has("reset-meta")) {
|
|
history.replaceState(
|
|
null,
|
|
"",
|
|
location.href.replace("&reset-meta", "").replace("?reset-meta", ""),
|
|
);
|
|
await Notes.setMeta({ lastSync: null });
|
|
}
|
|
|
|
// Very rudimentary periodic sync. It will be refactored into a more real-time
|
|
// solution using either websocket or long-polling, so that server can notify about
|
|
// new data to sync.
|
|
const sync = async () => {
|
|
await Notes.sync();
|
|
Notes.saveStorage();
|
|
};
|
|
|
|
if (isLoggedIn) {
|
|
setInterval(sync, 5000);
|
|
}
|
|
|
|
// New account provisioning and login/logout actions
|
|
|
|
const newAccountForm = document.querySelector("#new-account-form");
|
|
const loginLink = document.querySelector("#login-link");
|
|
const logoutForm = document.querySelector("#logout-form");
|
|
|
|
if (!isLoggedIn) {
|
|
loginLink.classList.remove("hidden");
|
|
newAccountForm.classList.remove("hidden");
|
|
} else {
|
|
logoutForm.classList.remove("hidden");
|
|
}
|
|
|
|
// Notifications API test - to be reused for push notifications later on
|
|
|
|
if (notificationsAvailable()) {
|
|
document.querySelector("#notifications-pane").classList.remove("hidden");
|
|
const notificationsButton = document.querySelector("#enable-notifications");
|
|
const notificationsTestButton = document.querySelector("#test-notifications");
|
|
|
|
if (!notificationsEnabled()) {
|
|
notificationsButton.classList.remove("hidden");
|
|
notificationsButton.addEventListener("click", () => {
|
|
authorizeNotifications(() => notificationsButton.classList.add("hidden"));
|
|
});
|
|
}
|
|
|
|
notificationsTestButton.addEventListener("click", () => {
|
|
setTimeout(() => {
|
|
sendNotification("reminder", "This is a test reminder!");
|
|
}, 8000);
|
|
});
|
|
}
|
|
|
|
// Search
|
|
|
|
let filter = "";
|
|
const search = document.querySelector("#search");
|
|
|
|
search.addEventListener("input", (e) => {
|
|
filter = e.target.value.trim().toLowerCase();
|
|
Notes.saveStorage();
|
|
});
|
|
|
|
function filterNotes(notes) {
|
|
if (filter !== "") {
|
|
return notes.filter(
|
|
(n) =>
|
|
n.title.toLowerCase().indexOf(filter) > -1 ||
|
|
textOf(n.content).toLowerCase().indexOf(filter) > -1,
|
|
);
|
|
}
|
|
|
|
return notes;
|
|
}
|
|
|
|
function textOf(content) {
|
|
if (typeof content === "object") {
|
|
return content.map((n) => n.content).join("");
|
|
} else {
|
|
return content;
|
|
}
|
|
}
|
|
|
|
// There are two note-form component instances - one for
|
|
// composing new notes and another one for editing existing notes.
|
|
|
|
const newNote = document.querySelector("#new-note");
|
|
const editNote = document.querySelector("#edit-note");
|
|
const editNoteDialog = document.querySelector("#edit-note-dialog");
|
|
|
|
// Load draft into new note
|
|
newNote.load(structuredClone(await Notes.getDraft()));
|
|
|
|
// Each save event originating from storage triggers a re-render
|
|
// of notes list.
|
|
Notes.addEventListener("save", render.bind(this));
|
|
|
|
// Initial notes render and initial sync.
|
|
render();
|
|
if (isLoggedIn) {
|
|
sync();
|
|
}
|
|
|
|
// note-form component specific event handlers
|
|
newNote.addEventListener("addNote", async (e) => {
|
|
await Notes.add(e.detail);
|
|
await Notes.clearDraft();
|
|
Notes.saveStorage();
|
|
});
|
|
|
|
newNote.addEventListener("updateDraft", async (e) => {
|
|
await Notes.setDraft(e.detail);
|
|
});
|
|
|
|
newNote.addEventListener("deleteNote", async (e) => {
|
|
await Notes.clearDraft();
|
|
});
|
|
|
|
editNote.addEventListener("updateNoteInProgress", async (e) => {
|
|
const note = e.detail;
|
|
note.updated = Date.now();
|
|
await Notes.update(note, true);
|
|
});
|
|
|
|
editNote.addEventListener("updateNote", async (e) => {
|
|
await Notes.update(e.detail);
|
|
Notes.saveStorage();
|
|
closeModal(editNoteDialog);
|
|
});
|
|
|
|
editNote.addEventListener("deleteNote", async (e) => {
|
|
await Notes.remove(e.detail);
|
|
Notes.saveStorage();
|
|
closeModal(editNoteDialog);
|
|
});
|
|
|
|
// The notes rendering routine is optimized to replace only
|
|
// nodes that actually changed.
|
|
|
|
let currentNotes = {};
|
|
|
|
function notesEqual(note1, note2) {
|
|
return note1.id === note2.id && note1.updated === note2.updated;
|
|
}
|
|
|
|
async function renderReminders() {
|
|
const allNotes = await Notes.all();
|
|
const reminderNotes = allNotes.filter((n) => n.reminder?.enabled);
|
|
const notes = filterNotes(reminderNotes).toSorted(
|
|
(a, b) => b.reminder.date - a.reminder.date,
|
|
);
|
|
const notesContainer = document.querySelector("#reminders");
|
|
}
|
|
|
|
async function renderHome() {
|
|
const allNotes = await Notes.all();
|
|
const notes = filterNotes(allNotes);
|
|
const notesContainer = document.querySelector("#notes");
|
|
|
|
let previousId = null;
|
|
let notePrecedence = {};
|
|
const ids = [];
|
|
|
|
notes.forEach((n) => {
|
|
notePrecedence[n.id] = previousId;
|
|
ids.push(n.id);
|
|
previousId = n.id;
|
|
});
|
|
|
|
Object.keys(currentNotes)
|
|
.filter((id) => !ids.includes(id))
|
|
.forEach((id) => {
|
|
delete currentNotes[id];
|
|
document.getElementById(id).remove();
|
|
});
|
|
|
|
notes.forEach((note) => {
|
|
const existingNote = currentNotes[note.id];
|
|
|
|
if (!existingNote) {
|
|
const noteElement = renderNote(note);
|
|
const beforeId = notePrecedence[note.id];
|
|
|
|
if (!beforeId) {
|
|
notesContainer.prepend(noteElement);
|
|
} else {
|
|
const before = document.getElementById(beforeId);
|
|
if (before) {
|
|
before.after(noteElement);
|
|
} else {
|
|
notesContainer.prepend(noteElement);
|
|
}
|
|
}
|
|
} else if (!notesEqual(existingNote, note)) {
|
|
const noteElement = renderNote(note);
|
|
const existing = document.getElementById(note.id);
|
|
existing.replaceWith(noteElement);
|
|
}
|
|
});
|
|
|
|
currentNotes = {};
|
|
notes.forEach((n) => (currentNotes[n.id] = structuredClone(n)));
|
|
}
|
|
|
|
async function render() {
|
|
switch (view) {
|
|
case "home":
|
|
renderHome();
|
|
break;
|
|
case "reminders":
|
|
renderReminders();
|
|
break;
|
|
default:
|
|
renderError();
|
|
}
|
|
}
|
|
|
|
function openModal(modal) {
|
|
document.body.style.top = `-${window.scrollY}px`;
|
|
document.body.style.position = "fixed";
|
|
|
|
modal.showModal();
|
|
}
|
|
|
|
function closeModal(modal) {
|
|
modal.close();
|
|
|
|
const scrollY = document.body.style.top;
|
|
document.body.style.position = "";
|
|
document.body.style.top = "";
|
|
window.scrollTo(0, parseInt(scrollY || "0") * -1);
|
|
}
|
|
|
|
function renderNote(note) {
|
|
const container = document.createElement("div");
|
|
container.id = note.id;
|
|
container.classList.add("note");
|
|
container.classList.add("readonly");
|
|
|
|
if (note.title && note.title !== "") {
|
|
const title = document.createElement("p");
|
|
title.classList.add("title");
|
|
title.replaceChildren(...renderText(note.title));
|
|
container.appendChild(title);
|
|
}
|
|
|
|
if (note.type === "note") {
|
|
const body = document.createElement("p");
|
|
body.classList.add("body");
|
|
body.replaceChildren(...renderText(note.content));
|
|
container.appendChild(body);
|
|
} else if (note.type === "tasklist") {
|
|
const list = document.createElement("ul");
|
|
list.classList.add("body");
|
|
|
|
note.content.forEach((task) => {
|
|
const item = document.createElement("li");
|
|
if (task.checked) {
|
|
item.classList.add("checked");
|
|
}
|
|
const check = document.createElement("p");
|
|
check.classList.add("checkbox");
|
|
check.textContent = task.checked ? "☑" : "☐";
|
|
item.appendChild(check);
|
|
const itemContent = document.createElement("p");
|
|
itemContent.classList.add("content");
|
|
itemContent.replaceChildren(...renderText(task.content));
|
|
item.appendChild(itemContent);
|
|
list.append(item);
|
|
});
|
|
|
|
container.appendChild(list);
|
|
}
|
|
|
|
if (note.reminder?.enabled) {
|
|
const labelText = reminderLabel(note.reminder);
|
|
const reminderBadge = document.createElement("span");
|
|
reminderBadge.classList.add("reminder-label");
|
|
reminderBadge.textContent = `⏲ ${labelText}`;
|
|
container.appendChild(reminderBadge);
|
|
}
|
|
|
|
container.addEventListener("click", async (e) => {
|
|
if (e.target.tagName !== "A") {
|
|
const note = await Notes.get(container.id);
|
|
editNote.load(structuredClone(note));
|
|
openModal(editNoteDialog);
|
|
}
|
|
});
|
|
|
|
return container;
|
|
}
|
|
|
|
const router = Router.init(routes);
|