diff --git a/priv/static/index.html b/priv/static/index.html
index 2a926d2..c736c2c 100644
--- a/priv/static/index.html
+++ b/priv/static/index.html
@@ -72,8 +72,21 @@
+
+
+
+
+
+
+
@@ -88,8 +101,21 @@
+
+
+
+
+
+
+
diff --git a/priv/static/js/components.js b/priv/static/js/components.js
index a58d7ec..cf12b25 100644
--- a/priv/static/js/components.js
+++ b/priv/static/js/components.js
@@ -1,4 +1,99 @@
import { renderText, html } from "./dom.js";
+import { reminderLabel } from "./reminders.js";
+
+// reminder-picker component
+
+class ReminderPicker extends HTMLElement {
+ constructor() {
+ super();
+ this.reminder = this.#new();
+ }
+
+ connectedCallback() {
+ this.dateInput = this.querySelector(".date");
+ this.timeInput = this.querySelector(".time");
+ this.repeatInput = this.querySelector(".repeat");
+ this.clearButton = this.querySelector(".clear");
+
+ const that = this;
+
+ this.dateInput.addEventListener("change", () => {
+ that.reminder.date = that.dateInput.value;
+
+ this.#emitUpdate();
+ });
+
+ this.timeInput.addEventListener("change", () => {
+ that.reminder.time = that.timeInput.value;
+
+ this.#emitUpdate();
+ });
+
+ this.repeatInput.addEventListener("change", () => {
+ that.reminder.unit = that.repeatInput.value;
+
+ this.#emitUpdate();
+ });
+
+ this.clearButton.addEventListener("click", () => {
+ this.reminder = this.#new();
+ this.reminder.enabled = false;
+ this.#updateUI();
+ this.#emitUpdate();
+ });
+
+ this.#updateUI();
+ }
+
+ enableDefault() {
+ this.reminder = this.#new();
+ this.reminder.enabled = true;
+ this.#updateUI();
+ this.#emitUpdate();
+ }
+
+ set value(reminder) {
+ this.reminder = reminder || this.#new();
+ this.#updateUI();
+ }
+
+ get value() {
+ return this.reminder;
+ }
+
+ get label() {
+ return reminderLabel(this.reminder);
+ }
+
+ #updateUI() {
+ this.dateInput.value = this.reminder.date;
+ this.timeInput.value = this.reminder.time;
+ this.repeatInput.value = this.reminder.unit;
+ }
+
+ #new() {
+ const now = new Date().toISOString();
+ const [date, rest] = now.split("T");
+ const fullTime = rest.split(".")[0];
+ const [hour, minute, _minute] = fullTime.split(":");
+
+ return {
+ enabled: false,
+ count: 1,
+ date: date,
+ time: `${hour}:${minute}`,
+ unit: "",
+ };
+ }
+
+ #emitUpdate() {
+ if (this.reminder.date && this.reminder.time) {
+ this.dispatchEvent(new Event("reminderUpdate", { bubbles: true }));
+ }
+ }
+}
+
+customElements.define("reminder-picker", ReminderPicker);
// editable-area component
@@ -469,6 +564,9 @@ class NoteForm extends HTMLElement {
this.removeButton = this.querySelector(".remove");
this.addButton = this.querySelector(".add");
this.saveButton = this.querySelector(".save");
+ this.reminderButton = this.querySelector(".reminder");
+ this.reminderButtonLabel = this.querySelector(".reminder span");
+ this.reminderPicker = this.querySelector("reminder-picker");
this.addEventListener("click", (e) => {
const textareaInside = e.target.querySelector("textarea");
@@ -517,6 +615,25 @@ class NoteForm extends HTMLElement {
this.#reset();
});
+ this.reminderButton.addEventListener("click", (e) => {
+ e.preventDefault();
+ this.reminderPicker.classList.toggle("hidden");
+ // reset and enable the reminder when first clicking the
+ // reminder button and there's either no reminder set yet
+ // or it's disabled
+ if (
+ !this.note.reminder?.enabled &&
+ !this.reminderPicker.classList.contains("hidden")
+ ) {
+ this.reminderPicker.enableDefault();
+ }
+ });
+
+ this.addEventListener("reminderUpdate", () => {
+ this.note.reminder = this.reminderPicker.value;
+ this.#updateUI();
+ });
+
if (this.mode === "add") {
this.addButton.addEventListener("click", (e) => {
e.preventDefault();
@@ -551,6 +668,7 @@ class NoteForm extends HTMLElement {
load(note) {
this.note = note;
+ this.reminderPicker.value = note.reminder;
this.#updateUI();
this.#setContent();
}
@@ -559,13 +677,22 @@ class NoteForm extends HTMLElement {
this.note = {
type: "note",
content: "",
+ reminder: null,
};
+ this.reminderPicker.value = null;
+
this.#updateUI();
this.#setContent();
}
#updateUI() {
+ if (this.note.reminder?.enabled) {
+ this.reminderButtonLabel.textContent = reminderLabel(this.note.reminder);
+ } else {
+ this.reminderPicker.classList.add("hidden");
+ this.reminderButtonLabel.textContent = "";
+ }
if (this.note.type === "note") {
this.tasklistModeButton.classList.remove("hidden");
this.noteModeButton.classList.add("hidden");
diff --git a/priv/static/js/jenot.js b/priv/static/js/jenot.js
index 8f73415..ed99aca 100644
--- a/priv/static/js/jenot.js
+++ b/priv/static/js/jenot.js
@@ -8,6 +8,7 @@ import {
sendNotification,
} from "./notifications.js";
import "./components.js";
+import { reminderLabel } from "./reminders.js";
async function resetApp() {
await window.navigator.serviceWorker
@@ -32,7 +33,7 @@ const isLoggedIn = !!document.cookie
// 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", "notes", endpoint);
+const Notes = new SyncedNoteStore("jenot-app", endpoint);
// Reset metadata to force full sync
if (URL_PARAMS.has("reset-meta")) {
@@ -213,6 +214,14 @@ function renderNote(note) {
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) => {
const note = await Notes.get(container.id);
editNote.load(note);
diff --git a/priv/static/js/reminders.js b/priv/static/js/reminders.js
new file mode 100644
index 0000000..40304c8
--- /dev/null
+++ b/priv/static/js/reminders.js
@@ -0,0 +1,10 @@
+export function reminderLabel(reminder) {
+ if (reminder.enabled) {
+ const date = new Date(`${reminder.date}T${reminder.time}:00`);
+ const day = date.getDate();
+ const month = date.toLocaleString("en", { month: "short" });
+ return `${day} ${month} ${reminder.time}`;
+ }
+
+ return "";
+}
diff --git a/priv/static/js/synced-store-test.js b/priv/static/js/synced-store-test.js
index 681ac74..aa392b5 100644
--- a/priv/static/js/synced-store-test.js
+++ b/priv/static/js/synced-store-test.js
@@ -2,7 +2,7 @@ import { test, assert } from "./test-utils.js";
import { SyncedNoteStore } from "./synced-store.js";
test("synced store stores a note", async (_container, idx) => {
- const store = new SyncedNoteStore(`jenot-app-test-${idx}`, "notes", "/", {
+ const store = new SyncedNoteStore(`jenot-app-test-${idx}`, "/", {
add: () => null,
});
@@ -24,7 +24,7 @@ test("synced store stores a note", async (_container, idx) => {
test("synced store gets a note", async (_container, idx) => {
let addCalled = false;
- const store = new SyncedNoteStore(`jenot-app-test-${idx}`, "notes", "/", {
+ const store = new SyncedNoteStore(`jenot-app-test-${idx}`, "/", {
add: () => (addCalled = true),
});
@@ -44,7 +44,7 @@ test("synced store updates a note", async (_container, idx) => {
let addCalled = false,
updateCalled = false;
- const store = new SyncedNoteStore(`jenot-app-test-${idx}`, "notes", "/", {
+ const store = new SyncedNoteStore(`jenot-app-test-${idx}`, "/", {
add: () => (addCalled = true),
update: () => (updateCalled = true),
});
diff --git a/priv/static/js/synced-store.js b/priv/static/js/synced-store.js
index 6090dcd..5257a76 100644
--- a/priv/static/js/synced-store.js
+++ b/priv/static/js/synced-store.js
@@ -1,3 +1,35 @@
+let databases = {};
+
+async function connect(dbName) {
+ if (!databases[dbName]) {
+ databases[dbName] = await dbConnect(dbName);
+ }
+
+ return databases[dbName];
+}
+
+function dbConnect(dbName) {
+ return new Promise((resolve, reject) => {
+ const request = indexedDB.open(dbName, 1);
+
+ request.onsuccess = (e) => {
+ resolve(e.target.result);
+ };
+
+ request.onerror = (e) => {
+ console.error(`indexedDB error: ${e.target.errorCode}`);
+ };
+
+ request.onupgradeneeded = (e) => {
+ const db = e.target.result;
+
+ db.createObjectStore("notes", {
+ keyPath: "id",
+ });
+ };
+ });
+}
+
class WebNoteStore {
constructor(endpoint) {
this.endpoint = endpoint || "/";
@@ -70,26 +102,26 @@ class WebNoteStore {
}
export class SyncedNoteStore extends EventTarget {
- constructor(dbName, storeName, endpoint, webStore) {
+ constructor(dbName, endpoint, webStore) {
super();
this.dbName = dbName;
- this.storeName = storeName;
+ this.storeName = "notes";
this.db = null;
this.webStore = webStore || (endpoint && new WebNoteStore(endpoint));
}
async all(since, includeDeleted) {
const that = this;
- return this.#connect().then(
+ return connect(this.dbName).then(
(db) =>
new Promise((resolve, reject) => {
db
.transaction([that.storeName], "readonly")
.objectStore(that.storeName)
.getAll().onsuccess = (data) => {
- const results = data.target.result.filter(
- (n) => (includeDeleted || !n.deleted) && n.id !== "meta",
- ).toSorted((a, b) => b.created - a.created);
+ const results = data.target.result
+ .filter((n) => (includeDeleted || !n.deleted) && n.id !== "meta")
+ .toSorted((a, b) => b.created - a.created);
if (since > 0) {
return resolve(results.filter((n) => n.updated > since));
@@ -105,7 +137,7 @@ export class SyncedNoteStore extends EventTarget {
const that = this;
let result;
- return this.#connect().then(
+ return connect(this.dbName).then(
(db) =>
new Promise(
(resolve, reject) =>
@@ -134,11 +166,21 @@ export class SyncedNoteStore extends EventTarget {
id: "id_" + now,
type: note.type,
content: note.content,
+ reminder: note.reminder
+ ? {
+ enabled: note.reminder.enabled,
+ date: note.reminder.date,
+ time: note.reminder.time,
+ repeat: note.reminder.count,
+ unit: note.reminder.unit,
+ }
+ : null,
created: now,
updated: now,
deleted: null,
};
- return this.#connect()
+
+ return connect(this.dbName)
.then(
(db) =>
new Promise(
@@ -151,7 +193,7 @@ export class SyncedNoteStore extends EventTarget {
)
.then(() => {
(async () => this.webStore?.add(entry))();
- return null;
+ return entry;
});
}
@@ -170,7 +212,7 @@ export class SyncedNoteStore extends EventTarget {
note.updated = Date.now();
}
- return this.#connect()
+ return connect(this.dbName)
.then(
(db) =>
new Promise(
@@ -183,7 +225,7 @@ export class SyncedNoteStore extends EventTarget {
)
.then(() => {
(async () => (skipNetwork ? null : this.webStore?.add(note)))();
- return null;
+ return note;
});
}
@@ -224,35 +266,4 @@ export class SyncedNoteStore extends EventTarget {
saveStorage() {
this.dispatchEvent(new CustomEvent("save"));
}
-
- async #connect() {
- if (!this.db) {
- this.db = await this.#dbConnect();
- }
-
- return this.db;
- }
-
- #dbConnect() {
- const that = this;
-
- return new Promise((resolve, reject) => {
- const request = indexedDB.open(this.dbName, 1);
-
- request.onsuccess = (e) => {
- resolve(e.target.result);
- };
-
- request.onerror = (e) => {
- console.error(`indexedDB error: ${e.target.errorCode}`);
- };
-
- request.onupgradeneeded = (e) => {
- const db = e.target.result;
- db.createObjectStore(that.storeName, {
- keyPath: "id",
- });
- };
- });
- }
}