Skip to content

Commit c3201f5

Browse files
authored
Persistant Filters (#75)
* Favourite Clear Button * scrolling I * Allow to cache query * Persistant Scrolling P.2 * Scrolling Pt.3 * Persistant Scrolling * Scrolling is Persistant! * Scroll persistance with going back to the top on search finished * Make filters persistant * Store and restore from local storage * Do caching via IndexedDB * Filter persistance * Filters are persistant - they just don't update bc Deo needs to fix the UI... * fixed dependencies * Reverted model changes
1 parent c00d68b commit c3201f5

File tree

4 files changed

+451
-299
lines changed

4 files changed

+451
-299
lines changed

my-app/firebase.js

Lines changed: 118 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { initializeApp } from "firebase/app";
22
import { getAuth, GoogleAuthProvider, onAuthStateChanged } from "firebase/auth";
33
import { get, getDatabase, ref, set, onValue, push } from "firebase/database";
44
import { reaction, toJS } from "mobx";
5-
import throttle from "lodash.throttle";
5+
// import throttle from "lodash.throttle";
66

77
// Your web app's Firebase configuration
88
const firebaseConfig = {
@@ -27,6 +27,24 @@ let noUpload = false;
2727

2828
export function connectToFirebase(model) {
2929
loadCoursesFromCacheOrFirebase(model);
30+
31+
// setting missing
32+
// also save filters to local storage
33+
//
34+
const options = JSON.parse(localStorage.getItem("filterOptions"));
35+
if (options) {
36+
model.setFilterOptions(options);
37+
console.log("Restore options from local storage");
38+
}
39+
40+
reaction(
41+
() => ({filterOptions: JSON.stringify(model.filterOptions)}),
42+
// eslint-disable-next-line no-unused-vars
43+
({filterOptions}) => {
44+
localStorage.setItem("filterOptions", filterOptions);
45+
}
46+
);
47+
3048
onAuthStateChanged(auth, (user) => {
3149
if (user) {
3250
model.setUser(user); // Set the user ID once authenticated
@@ -41,7 +59,6 @@ export function connectToFirebase(model) {
4159

4260
// fetches all relevant information to create the model
4361
async function firebaseToModel(model) {
44-
if (!model.user) return;
4562
const userRef = ref(db, `users/${model.user.uid}`);
4663
onValue(userRef, (snapshot) => {
4764
if (!snapshot.exists()) return;
@@ -50,10 +67,9 @@ async function firebaseToModel(model) {
5067
if (data.favourites) model.setFavourite(data.favourites);
5168
if (data.currentSearchText)
5269
model.setCurrentSearchText(data.currentSearchText);
53-
if (data.scrollPosition)
54-
model.setScrollPosition(data.scrollPosition);
55-
// if (data.currentSearch)
56-
// model.setCurrentSearch(data.currentSearch);
70+
// if (data.scrollPosition)
71+
// model.setScrollPosition(data.scrollPosition);
72+
// if (data.filterOptions) model.setFilterOptions(data.filterOptions);
5773
noUpload = false;
5874
});
5975
}
@@ -64,16 +80,17 @@ export function syncModelToFirebase(model) {
6480
userId: model?.user.uid,
6581
favourites: toJS(model.favourites),
6682
currentSearchText: toJS(model.currentSearchText),
67-
// currentSearch: toJS(model.currentSearch),
83+
// filterOptions: toJS(model.filterOptions),
6884
// Add more per-user attributes here
6985
}),
7086
// eslint-disable-next-line no-unused-vars
71-
({ userId, favourites, currentSearchText }) => {
87+
({ userId, favourites, currentSearchText, filterOptions }) => {
7288
if (noUpload || !userId) return;
7389
const userRef = ref(db, `users/${userId}`);
7490
const dataToSync = {
7591
favourites,
7692
currentSearchText,
93+
// filterOptions,
7794
};
7895

7996
set(userRef, dataToSync)
@@ -86,40 +103,60 @@ export function syncModelToFirebase(model) {
86103
export function syncScrollPositionToFirebase(model, containerRef) {
87104
if (!containerRef?.current) return;
88105
let lastSavedPosition = 0;
89-
90-
// const throttledSet = throttle((scrollPixel) => {
91-
// if (model?.user?.uid) {
92-
// const userRef = ref(db, `users/${model.user.uid}/scrollPosition`);
93-
// set(userRef, scrollPixel).catch(console.error);
94-
// }
95-
// }, 500);
96-
97-
const handleScroll = () => {
98-
const scrollTop = containerRef.current.scrollTop;
106+
107+
// const throttledSet = throttle((scrollPixel) => {
108+
// if (model?.user?.uid) {
109+
// const userRef = ref(db, `users/${model.user.uid}/scrollPosition`);
110+
// set(userRef, scrollPixel).catch(console.error);
111+
// }
112+
// }, 500);
113+
114+
const handleScroll = () => {
115+
const scrollTop = containerRef.current.scrollTop;
99116
// make a 100px threshold
100-
if (Math.abs(scrollTop - lastSavedPosition) < 100)
101-
return;
117+
if (Math.abs(scrollTop - lastSavedPosition) < 100) return;
102118

103119
lastSavedPosition = scrollTop;
104-
model.setScrollPosition(scrollTop);
105-
localStorage.setItem("scrollPosition", scrollTop);
106-
// throttledSet(scrollTop);
107-
};
120+
model.setScrollPosition(scrollTop);
121+
localStorage.setItem("scrollPosition", scrollTop);
122+
// throttledSet(scrollTop);
123+
};
108124

109-
containerRef.current.addEventListener('scroll', handleScroll);
110-
return () => containerRef.current?.removeEventListener('scroll', handleScroll);
125+
containerRef.current.addEventListener("scroll", handleScroll);
126+
return () =>
127+
containerRef.current?.removeEventListener("scroll", handleScroll);
111128
}
112129

130+
function saveCoursesToCache(courses, timestamp) {
131+
const request = indexedDB.open("CourseDB", 1);
113132

114-
function saveCoursesInChunks(courses, timestamp) {
115-
const parts = 3; // Adjust this based on course size
116-
const chunkSize = Math.ceil(courses.length / parts);
133+
request.onupgradeneeded = (event) => {
134+
const db = event.target.result;
135+
if (!db.objectStoreNames.contains("courses")) {
136+
db.createObjectStore("courses", { keyPath: "id" });
137+
}
138+
if (!db.objectStoreNames.contains("metadata")) {
139+
db.createObjectStore("metadata", { keyPath: "key" });
140+
}
141+
};
117142

118-
for (let i = 0; i < parts; i++) {
119-
const chunk = courses.slice(i * chunkSize, (i + 1) * chunkSize);
120-
localStorage.setItem(`coursesPart${i}`, JSON.stringify(chunk));
121-
}
122-
localStorage.setItem("coursesMetadata", JSON.stringify({ parts, timestamp }));
143+
request.onsuccess = (event) => {
144+
const db = event.target.result;
145+
const tx = db.transaction(["courses", "metadata"], "readwrite");
146+
const courseStore = tx.objectStore("courses");
147+
const metaStore = tx.objectStore("metadata");
148+
149+
courseStore.clear();
150+
courses.forEach((course) => courseStore.put(course));
151+
metaStore.put({ key: "timestamp", value: timestamp });
152+
153+
tx.oncomplete = () => console.log("Saved courses to IndexedDB");
154+
tx.onerror = (e) => console.error("IndexedDB save error", e);
155+
};
156+
157+
request.onerror = (e) => {
158+
console.error("Failed to open IndexedDB", e);
159+
};
123160
}
124161

125162
async function updateLastUpdatedTimestamp() {
@@ -155,28 +192,59 @@ export async function fetchAllCourses() {
155192
}
156193

157194
async function loadCoursesFromCacheOrFirebase(model) {
158-
// Load metadata from localStorage
159-
const cachedMetadata = JSON.parse(localStorage.getItem("coursesMetadata"));
160195
const firebaseTimestamp = await fetchLastUpdatedTimestamp();
161-
// check if up to date
162-
if (cachedMetadata && cachedMetadata.timestamp === firebaseTimestamp) {
163-
console.log("Using cached courses...");
164-
let mergedCourses = [];
165-
for (let i = 0; i < cachedMetadata.parts; i++) {
166-
const part = JSON.parse(localStorage.getItem(`coursesPart${i}`));
167-
if (part) mergedCourses = mergedCourses.concat(part);
196+
const dbPromise = new Promise((resolve, reject) => {
197+
const request = indexedDB.open("CourseDB", 1);
198+
// check if courses and metadata dirs exist
199+
request.onupgradeneeded = (event) => {
200+
const db = event.target.result;
201+
if (!db.objectStoreNames.contains("courses")) {
202+
db.createObjectStore("courses", { keyPath: "id" });
203+
}
204+
if (!db.objectStoreNames.contains("metadata")) {
205+
db.createObjectStore("metadata", { keyPath: "key" });
206+
}
207+
};
208+
209+
request.onsuccess = (event) => resolve(event.target.result);
210+
request.onerror = (e) => reject(e);
211+
});
212+
213+
try {
214+
const db = await dbPromise;
215+
const metaTx = db.transaction("metadata", "readonly");
216+
const metaStore = metaTx.objectStore("metadata");
217+
const metaReq = metaStore.get("timestamp");
218+
const cachedTimestamp = await new Promise((resolve) => {
219+
metaReq.onsuccess = () => resolve(metaReq.result?.value ?? 0);
220+
metaReq.onerror = () => resolve(0);
221+
});
222+
223+
if (cachedTimestamp === firebaseTimestamp) {
224+
console.log("Using cached courses from IndexedDB...");
225+
const courseTx = db.transaction("courses", "readonly");
226+
const courseStore = courseTx.objectStore("courses");
227+
const getAllReq = courseStore.getAll();
228+
const cachedCourses = await new Promise((resolve) => {
229+
getAllReq.onsuccess = () => resolve(getAllReq.result);
230+
getAllReq.onerror = () => resolve([]);
231+
});
232+
model.setCourses(cachedCourses);
233+
return;
168234
}
169-
model.setCourses(mergedCourses);
170-
return;
235+
} catch (err) {
236+
console.warn("IndexedDB unavailable, falling back to Firebase:", err);
171237
}
172238

173-
// Fetch if outdated or missing
239+
// fallback: fetch from Firebase
174240
console.log("Fetching courses from Firebase...");
175241
const courses = await fetchAllCourses();
176242
model.setCourses(courses);
177-
saveCoursesInChunks(courses, firebaseTimestamp);
243+
saveCoursesToCache(courses, firebaseTimestamp);
244+
178245
}
179246

247+
180248
export async function saveJSONCoursesToFirebase(model, data) {
181249
if (!data || !model) {
182250
console.log("no model or data");
@@ -216,11 +284,11 @@ export async function getReviewsForCourse(courseCode) {
216284
const snapshot = await get(reviewsRef);
217285
if (!snapshot.exists()) return [];
218286
const reviews = [];
219-
snapshot.forEach(childSnapshot => {
287+
snapshot.forEach((childSnapshot) => {
220288
reviews.push({
221289
id: childSnapshot.key,
222-
...childSnapshot.val()
290+
...childSnapshot.val(),
223291
});
224292
});
225293
return reviews;
226-
}
294+
}

0 commit comments

Comments
 (0)