Implement indexedDB backed store making app offline friendly

This commit is contained in:
Adrian Gruntkowski 2024-11-15 23:00:18 +01:00
parent 274ef4c217
commit 599004206b
4 changed files with 156 additions and 26 deletions

View file

@ -1,6 +1,5 @@
Only immediate next things to do are listed here, without any far-fetching plans.
- Implement syncing local storage with service worker (either via postMessage or using IndexedDB)
- Implement reminders
- Implement masonry layout
- Implement color coding

139
js/db-store.js Normal file
View file

@ -0,0 +1,139 @@
export class DBNoteStore extends EventTarget {
constructor(dbName, storeName) {
super();
this.dbName = dbName;
this.storeName = storeName;
this.db = null;
}
async all() {
const that = this;
return this.#connect().then(
(db) =>
new Promise((resolve, reject) => {
db
.transaction([that.storeName], "readonly")
.objectStore(that.storeName)
.getAll().onsuccess = (data) => resolve(data.target.result);
}),
);
}
async get(id) {
const that = this;
let result;
return this.#connect().then(
(db) =>
new Promise(
(resolve, reject) =>
(db
.transaction([that.storeName], "readonly")
.objectStore(that.storeName)
.get(id).onsuccess = (data) => resolve(data.target.result)),
),
);
}
async add(note) {
const that = this;
const entry = {
id: "id_" + Date.now(),
type: note.type,
content: note.content,
created: new Date(),
};
return this.#connect().then(
(db) =>
new Promise(
(resolve, reject) =>
(db
.transaction([that.storeName], "readwrite")
.objectStore(that.storeName)
.add(entry).onsuccess = () => resolve(null)),
),
);
}
async reset() {
const that = this;
return this.#connect().then(
(db) =>
new Promise(
(resolve, reject) =>
(db
.transaction([that.storeName], "readwrite")
.objectStore(that.storeName)
.clear().onsuccess = () => resolve(null)),
),
);
}
async remove({ id }) {
const that = this;
return this.#connect().then(
(db) =>
new Promise(
(resolve, reject) =>
(db
.transaction([that.storeName], "readwrite")
.objectStore(that.storeName)
.delete(id).onsuccess = () => resolve(null)),
),
);
}
async update(note) {
const that = this;
return this.#connect().then(
(db) =>
new Promise(
(resolve, reject) =>
(db
.transaction([that.storeName], "readwrite")
.objectStore(that.storeName)
.put(note).onsuccess = () => resolve(null)),
),
);
}
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",
});
};
});
}
}

View file

@ -1,9 +1,14 @@
import "./service-worker.js";
import { renderText } from "./dom.js";
import { NoteStore } from "./store.js";
import { DBNoteStore } from "./db-store.js";
import "./components.js";
const Notes = new NoteStore("jenot-app");
const urlParams = new URLSearchParams(window.location.search);
const Notes = urlParams.has("localStorage")
? new NoteStore("jenot-app")
: new DBNoteStore("jenot-app", "notes");
const newNote = document.querySelector("#new-note");
const editNote = document.querySelector("#edit-note");
@ -12,28 +17,27 @@ Notes.addEventListener("save", render.bind(this));
render();
newNote.addEventListener("addNote", (e) => {
console.log(e.detail);
Notes.add(e.detail);
newNote.addEventListener("addNote", async (e) => {
await Notes.add(e.detail);
Notes.saveStorage();
});
editNote.addEventListener("updateNote", (e) => {
editNote.addEventListener("updateNote", async (e) => {
newNote.classList.remove("hidden");
editNote.classList.add("hidden");
Notes.update(e.detail);
await Notes.update(e.detail);
Notes.saveStorage();
});
editNote.addEventListener("deleteNote", (e) => {
editNote.addEventListener("deleteNote", async (e) => {
newNote.classList.remove("hidden");
editNote.classList.add("hidden");
Notes.remove(e.detail);
await Notes.remove(e.detail);
Notes.saveStorage();
});
function render() {
const notes = Notes.all();
async function render() {
const notes = await Notes.all();
const notesContainer = document.querySelector("#notes");
notesContainer.replaceChildren();
@ -64,10 +68,11 @@ function render() {
notesContainer.appendChild(container);
container.addEventListener("click", (e) => {
container.addEventListener("click", async (e) => {
newNote.classList.add("hidden");
editNote.classList.remove("hidden");
editNote.load(Notes.get(container.id));
const note = await Notes.get(container.id);
editNote.load(note);
});
});
}

View file

@ -1,7 +1,6 @@
export class NoteStore extends EventTarget {
localStorageKey;
notes = [];
editedNoteId = "none";
/*
Note structure:
@ -31,7 +30,6 @@ export class NoteStore extends EventTarget {
all = () => this.notes;
get = (id) => this.notes.find((note) => note.id === id);
getEditedNoteId = () => this.editedNoteId;
add(note) {
this.notes.unshift({
@ -54,19 +52,11 @@ export class NoteStore extends EventTarget {
this.notes = this.notes.map((n) => (n.id === note.id ? note : n));
}
setEditedNoteId(id) {
this.editedNoteId = id;
}
saveStorage() {
window.localStorage.setItem(
this.localStorageKey + "_notes",
JSON.stringify(this.notes),
);
window.localStorage.setItem(
this.localStorageKey + "_editedNoteId",
this.editedNoteId,
);
this.dispatchEvent(new CustomEvent("save"));
}
@ -74,8 +64,5 @@ export class NoteStore extends EventTarget {
this.notes = JSON.parse(
window.localStorage.getItem(this.localStorageKey + "_notes") || "[]",
);
this.editedNoteId =
window.localStorage.getItem(this.localStorageKey + "_editedNoteId") ||
"none";
}
}