diff --git a/web/package.json b/web/package.json
index 7e2294b..2076c5c 100644
--- a/web/package.json
+++ b/web/package.json
@@ -15,6 +15,7 @@
"axios": "^1.7.4",
"element-tiptap": "1.27.1",
"element-ui": "^2.15.13",
+ "idb": "^8.0.1",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"prosemirror-tables": "^1.1.1",
diff --git a/web/src/components/DiaryEditor.vue b/web/src/components/DiaryEditor.vue
index 6f0dec9..59298d4 100644
--- a/web/src/components/DiaryEditor.vue
+++ b/web/src/components/DiaryEditor.vue
@@ -1,15 +1,16 @@
diff --git a/web/src/services/note.ts b/web/src/services/note.ts
new file mode 100644
index 0000000..049c80a
--- /dev/null
+++ b/web/src/services/note.ts
@@ -0,0 +1,132 @@
+// services/api.ts
+import axios from '../axiosConfig.js';
+
+import { openDB, DBSchema, IDBPDatabase } from 'idb';
+import type { DiaryEntry, QueuedRequest } from '../types.ts';
+
+interface MyDB extends DBSchema {
+ notes: {
+ key: string;
+ value: DiaryEntry;
+ indexes: { 'noteId': string }
+ }
+}
+
+let db: IDBPDatabase | null = null;
+
+const DB_NAME = 'logbook-db';
+
+const openDatabase = async () => {
+ if (db) return db;
+ db = await openDB(DB_NAME, 1, {
+ upgrade(db) {
+ db.createObjectStore('notes', { keyPath: 'noteId' })
+ }
+ });
+ return db;
+}
+
+const requestQueue: QueuedRequest[] = [];
+let isSyncing = false;
+
+// Function to process request queue
+const processQueue = async () => {
+ if (isSyncing || !navigator.onLine) {
+ return; // syncing process or offline, wait
+ }
+
+ isSyncing = true;
+
+ while (requestQueue.length > 0) {
+ const request = requestQueue.shift();
+ if (!request) continue;
+
+ try {
+ const response = await axios({
+ url: request.url,
+ method: request.method,
+ data: request.data
+ })
+ if (response.status >= 200 && response.status < 300) {
+ request.resolve && request.resolve(response.data)
+ } else {
+ console.error('request failed with status: ' + response.status)
+ requestQueue.unshift(request); // push back into the queue
+ request.reject && request.reject('request failed');
+ break;
+ }
+ }
+ catch (error) {
+ console.error("request failed", error)
+ requestQueue.unshift(request);
+ request.reject && request.reject('request failed');
+ break;
+ }
+ }
+
+ isSyncing = false;
+ if (requestQueue.length > 0) {
+ // if there are requests left, retry after a short delay
+ setTimeout(processQueue, 1000)
+ }
+};
+
+
+const enqueueRequest = (url: string, method: 'PUT' | 'GET', data: any): Promise => {
+ return new Promise((resolve, reject) => {
+ requestQueue.push({ url, method, data, resolve, reject });
+ processQueue();
+ });
+
+};
+
+const apiRequest = async (url: string, method: 'PUT' | 'GET', data: any) => {
+ if (navigator.onLine) {
+ // if online go straight to the network
+ try {
+ const response = await axios({ url, method, data });
+ return response.data
+ }
+ catch (error) {
+ throw error
+ }
+ }
+ else {
+ return enqueueRequest(url, method, data);
+ }
+};
+
+
+const saveNote = async (note: DiaryEntry) => {
+ const db = await openDatabase()
+ console.log("saving note", note);
+ await db.put('notes', note);
+
+ // if user online, immediately save to the server
+ return apiRequest(`/api/diary/${note.noteId}`, 'PUT', note);
+};
+
+const fetchNote = async (noteId: string): Promise => {
+ console.log("fetching note", noteId);
+ const db = await openDatabase();
+ let cachedNote = await db.get('notes', noteId);
+ if (navigator.onLine) {
+ try {
+ const response = await apiRequest(`/api/diary/${noteId}`, 'GET', null);
+ if (response) {
+ cachedNote = response;
+ db.put('notes', response);
+ }
+ } catch (error) {
+ console.log("fetch note failed, using local", error);
+ }
+ }
+ return cachedNote
+};
+
+window.addEventListener('online', () => {
+ processQueue(); //process the queue on network status change
+});
+
+export { saveNote, fetchNote };
+
diff --git a/web/src/types.ts b/web/src/types.ts
new file mode 100644
index 0000000..80eb131
--- /dev/null
+++ b/web/src/types.ts
@@ -0,0 +1,12 @@
+// types.ts
+export interface DiaryEntry {
+ noteId: string;
+ note: string;
+}
+export interface QueuedRequest {
+ url: string;
+ method: 'PUT' | 'GET';
+ data: any;
+ resolve?: (value: any) => void;
+ reject?: (reason?: any) => void;
+}
diff --git a/web/yarn.lock b/web/yarn.lock
index f1af09c..dfa4b8b 100644
--- a/web/yarn.lock
+++ b/web/yarn.lock
@@ -1449,6 +1449,11 @@ iconv-lite@^0.4.24:
dependencies:
safer-buffer ">= 2.1.2 < 3"
+idb@^8.0.1:
+ version "8.0.1"
+ resolved "https://registry.yarnpkg.com/idb/-/idb-8.0.1.tgz#15e8be673413d6caf4beefacf086c8902d785e1e"
+ integrity sha512-EkBCzUZSdhJV8PxMSbeEV//xguVKZu9hZZulM+2gHXI0t2hGVU3eYE6/XnH77DS6FM2FY8wl17aDcu9vXpvLWQ==
+
ignore@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
diff --git a/yarn.lock b/yarn.lock
new file mode 100644
index 0000000..08377cd
--- /dev/null
+++ b/yarn.lock
@@ -0,0 +1,8 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+idb@^8.0.1:
+ version "8.0.1"
+ resolved "https://registry.yarnpkg.com/idb/-/idb-8.0.1.tgz#15e8be673413d6caf4beefacf086c8902d785e1e"
+ integrity sha512-EkBCzUZSdhJV8PxMSbeEV//xguVKZu9hZZulM+2gHXI0t2hGVU3eYE6/XnH77DS6FM2FY8wl17aDcu9vXpvLWQ==