mirror of
https://github.com/zoldar/jenot.git
synced 2026-01-03 14:32:54 +00:00
Implement basics of reminders UI and client-side state
This commit is contained in:
parent
97365447e1
commit
b9a6e8dbf0
6 changed files with 229 additions and 46 deletions
|
|
@ -72,8 +72,21 @@
|
|||
<button class="add">➕</button>
|
||||
<button class="tasklist-mode">☑️</button>
|
||||
<button class="note-mode hidden">📑</button>
|
||||
<button class="reminder">⏰ <span></span></button>
|
||||
<button class="remove">🗑️</button>
|
||||
</div>
|
||||
<reminder-picker class="hidden">
|
||||
<input class="date" type="date" />
|
||||
<input class="time" type="time" />
|
||||
<select class="repeat">
|
||||
<option value="">Does not repeat</option>
|
||||
<option value="day">Daily</option>
|
||||
<option value="week">Weekly</option>
|
||||
<option value="month">Monthly</option>
|
||||
<option value="year">Yearly</option>
|
||||
</select>
|
||||
<button class="clear">X</button>
|
||||
</reminder-picker>
|
||||
</div>
|
||||
</note-form>
|
||||
|
||||
|
|
@ -88,8 +101,21 @@
|
|||
<button class="save">💾</button>
|
||||
<button class="tasklist-mode">☑️</button>
|
||||
<button class="note-mode">📑</button>
|
||||
<button class="reminder">⏰ <span></span></button>
|
||||
<button class="remove">🗑️</button>
|
||||
</div>
|
||||
<reminder-picker class="hidden">
|
||||
<input class="date" type="date" />
|
||||
<input class="time" type="time" />
|
||||
<select class="repeat">
|
||||
<option value="">Does not repeat</option>
|
||||
<option value="day">Daily</option>
|
||||
<option value="week">Weekly</option>
|
||||
<option value="month">Monthly</option>
|
||||
<option value="year">Yearly</option>
|
||||
</select>
|
||||
<button class="clear">X</button>
|
||||
</reminder-picker>
|
||||
</div>
|
||||
</note-form>
|
||||
</dialog>
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
10
priv/static/js/reminders.js
Normal file
10
priv/static/js/reminders.js
Normal file
|
|
@ -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 "";
|
||||
}
|
||||
|
|
@ -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),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue