Skip to content

Commit 14ca255

Browse files
authored
Persistance III
* 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
1 parent 2170541 commit 14ca255

File tree

9 files changed

+156
-49
lines changed

9 files changed

+156
-49
lines changed

my-app/firebase.js

Lines changed: 55 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ 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";
6+
57
// Your web app's Firebase configuration
68
const firebaseConfig = {
79
apiKey: "AIzaSyCBckVI9nhAP62u5jZJW3F4SLulUv7znis",
@@ -24,53 +26,54 @@ googleProvider.addScope("email");
2426
let noUpload = false;
2527

2628
export function connectToFirebase(model) {
27-
loadCoursesFromCacheOrFirebase(model);
29+
loadCoursesFromCacheOrFirebase(model);
2830
onAuthStateChanged(auth, (user) => {
2931
if (user) {
3032
model.setUser(user); // Set the user ID once authenticated
31-
firebaseToModel(model); // Set up listeners for user-specific data
32-
syncModelToFirebase(model); // Start syncing changes to Firebase
33+
firebaseToModel(model); // Set up listeners for user-specific data
34+
syncModelToFirebase(model); // Start syncing changes to Firebase
35+
syncScrollPositionToFirebase(model);
3336
} else {
34-
model.setUser(null); // If no user, clear user-specific data
37+
model.setUser(null); // If no user, clear user-specific data
3538
}
3639
});
3740
}
3841

3942
// fetches all relevant information to create the model
4043
async function firebaseToModel(model) {
41-
if (!model.user)
42-
return;
44+
if (!model.user) return;
4345
const userRef = ref(db, `users/${model.user.uid}`);
4446
onValue(userRef, (snapshot) => {
45-
if (!snapshot.exists())
46-
return;
47+
if (!snapshot.exists()) return;
4748
const data = snapshot.val();
4849
noUpload = true;
49-
if (data.favourites)
50-
model.setFavourite(data.favourites);
51-
// if (data.currentSearch)
52-
// model.setCurrentSearch(data.currentSearch);
50+
if (data.favourites) model.setFavourite(data.favourites);
51+
if (data.currentSearchText)
52+
model.setCurrentSearchText(data.currentSearchText);
53+
if (data.scrollPosition)
54+
model.setScrollPosition(data.scrollPosition);
55+
// if (data.currentSearch)
56+
// model.setCurrentSearch(data.currentSearch);
5357
noUpload = false;
5458
});
5559
}
5660

57-
5861
export function syncModelToFirebase(model) {
5962
reaction(
6063
() => ({
6164
userId: model?.user.uid,
6265
favourites: toJS(model.favourites),
66+
currentSearchText: toJS(model.currentSearchText),
6367
// currentSearch: toJS(model.currentSearch),
6468
// Add more per-user attributes here
6569
}),
6670
// eslint-disable-next-line no-unused-vars
67-
({ userId, favourites, currentSearch }) => {
68-
if (noUpload || !userId)
69-
return;
71+
({ userId, favourites, currentSearchText }) => {
72+
if (noUpload || !userId) return;
7073
const userRef = ref(db, `users/${userId}`);
7174
const dataToSync = {
7275
favourites,
73-
//currentSearch,
76+
currentSearchText,
7477
};
7578

7679
set(userRef, dataToSync)
@@ -80,6 +83,34 @@ export function syncModelToFirebase(model) {
8083
);
8184
}
8285

86+
export function syncScrollPositionToFirebase(model, containerRef) {
87+
if (!containerRef?.current) return;
88+
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;
99+
// make a 100px threshold
100+
if (Math.abs(scrollTop - lastSavedPosition) < 100)
101+
return;
102+
103+
lastSavedPosition = scrollTop;
104+
model.setScrollPosition(scrollTop);
105+
localStorage.setItem("scrollPosition", scrollTop);
106+
// throttledSet(scrollTop);
107+
};
108+
109+
containerRef.current.addEventListener('scroll', handleScroll);
110+
return () => containerRef.current?.removeEventListener('scroll', handleScroll);
111+
}
112+
113+
83114
function saveCoursesInChunks(courses, timestamp) {
84115
const parts = 3; // Adjust this based on course size
85116
const chunkSize = Math.ceil(courses.length / parts);
@@ -170,23 +201,20 @@ export async function saveJSONCoursesToFirebase(model, data) {
170201
});
171202
}
172203

173-
174204
export async function addReviewForCourse(courseCode, review) {
175-
try {
176-
const reviewsRef = ref(db, `reviews/${courseCode}`);
177-
const newReviewRef = push(reviewsRef);
178-
await set(newReviewRef, review);
179-
} catch (error) {
180-
console.error("Error when adding a course to firebase:", error);
205+
try {
206+
const reviewsRef = ref(db, `reviews/${courseCode}`);
207+
const newReviewRef = push(reviewsRef);
208+
await set(newReviewRef, review);
209+
} catch (error) {
210+
console.error("Error when adding a course to firebase:", error);
181211
}
182212
}
183213

184-
185214
export async function getReviewsForCourse(courseCode) {
186215
const reviewsRef = ref(db, `reviews/${courseCode}`);
187216
const snapshot = await get(reviewsRef);
188217
if (!snapshot.exists()) return [];
189-
190218
const reviews = [];
191219
snapshot.forEach(childSnapshot => {
192220
reviews.push({
@@ -195,9 +223,4 @@ export async function getReviewsForCourse(courseCode) {
195223
});
196224
});
197225
return reviews;
198-
}
199-
200-
201-
202-
203-
226+
}

my-app/package-lock.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

my-app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"autoprefixer": "^10.4.21",
2020
"firebase": "^11.5.0",
2121
"ldrs": "^1.1.6",
22+
"lodash.throttle": "^4.1.1",
2223
"mobx": "^6.13.7",
2324
"mobx-react-lite": "^4.1.0",
2425
"pdfjs-dist": "^5.1.91",

my-app/src/index.jsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ configure({ enforceActions: "never" });
1515
const reactiveModel = makeAutoObservable(model);
1616
connectToFirebase(reactiveModel);
1717

18-
// ✅ Add /share route here
1918
export function makeRouter(reactiveModel) {
2019
return createHashRouter([
2120
{

my-app/src/model.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ export const model = {
66
//add searchChange: false, //this is for reworking the searchbar presenter, so that it triggers as a model,
77
//instead of passing searchcouses lambda function down into the searchbarview.
88
currentSearch: [],
9+
currentSearchText: "",
10+
scrollPosition: 0,
911
courses: [],
1012
favourites: [],
1113
isReady: false,
@@ -37,6 +39,14 @@ export const model = {
3739
this.currentSearch = searchResults;
3840
},
3941

42+
setCurrentSearchText(text){
43+
this.currentSearchText = text;
44+
},
45+
46+
setScrollPosition(position) {
47+
this.scrollPosition = position;
48+
},
49+
4050
setCourses(courses){
4151
this.courses = courses;
4252
},

my-app/src/presenters/ListViewPresenter.jsx

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,61 @@
11
import React from 'react';
22
import { observer } from "mobx-react-lite";
3-
import { useState } from 'react';
3+
import { useState, useEffect, useRef } from 'react';
44
import ListView from "../views/ListView.jsx";
55
import CoursePagePopup from '../views/Components/CoursePagePopup.jsx';
66
import PrerequisitePresenter from './PrerequisitePresenter.jsx';
77
import {ReviewPresenter} from "../presenters/ReviewPresenter.jsx"
8+
import {syncScrollPositionToFirebase} from "../../firebase.js"
89

910
const ListViewPresenter = observer(({ model }) => {
11+
const scrollContainerRef = useRef(null);
12+
let attempts = 0;
13+
const MAX_Depth = 49;
14+
15+
function persistantScrolling(fetchMoreCourses, hasMore){
16+
const container = scrollContainerRef.current;
17+
if (!container || !model.scrollPosition) return;
18+
19+
20+
21+
const attemptScroll = () => {
22+
23+
// refresh on significant change (same as in firebase)
24+
if (Math.abs(container.scrollTop - model.scrollPosition) < 100)
25+
return;
26+
27+
attempts++;
28+
if (attempts > MAX_Depth) {
29+
return;
30+
}
31+
const needsMoreCourses = container.scrollHeight < model.scrollPosition && hasMore;
32+
33+
if (needsMoreCourses) {
34+
fetchMoreCourses();
35+
setTimeout(attemptScroll, 100); // Add delay between attempts
36+
} else {
37+
container.scrollTop = model.scrollPosition;
38+
syncScrollPositionToFirebase(model, scrollContainerRef)
39+
}
40+
};
41+
attemptScroll();
42+
}
43+
44+
useEffect(() => {
45+
// Load initial scroll position
46+
const savedPosition = model.user
47+
? model.scrollPosition
48+
: localStorage.getItem("scrollPosition");
49+
if (savedPosition) {
50+
model.setScrollPosition(parseInt(savedPosition, 10));
51+
}
52+
}, [model.user]);
53+
54+
useEffect(() => {
55+
const cleanup = syncScrollPositionToFirebase(model, scrollContainerRef);
56+
return () => cleanup;
57+
}, [model.user, model.currentSearch, scrollContainerRef]);
58+
1059
const addFavourite = (course) => {
1160
model.addFavourite(course);
1261
}
@@ -41,15 +90,21 @@ const ListViewPresenter = observer(({ model }) => {
4190
return <ListView
4291
courses={model.courses}
4392
searchResults={model.currentSearch}
93+
currentSearchLenght={model.currentSearch.length}
94+
4495
favouriteCourses={model.favourites}
4596
addFavourite={addFavourite}
4697
removeFavourite={removeFavourite}
98+
handleFavouriteClick={handleFavouriteClick}
99+
47100
isPopupOpen={isPopupOpen}
48101
setIsPopupOpen={setIsPopupOpen}
49102
setSelectedCourse={setSelectedCourse}
50103
popup={popup}
51-
handleFavouriteClick={handleFavouriteClick}
52-
currentSearchLenght={model.currentSearch.length}
104+
105+
targetScroll={model.scrollPosition}
106+
scrollContainerRef={scrollContainerRef}
107+
persistantScrolling={persistantScrolling}
53108

54109
/>;
55110
});

my-app/src/presenters/SearchbarPresenter.jsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const SearchbarPresenter = observer(({ model }) => {
1616
course.name.toLowerCase().includes(query.toLowerCase()) ||
1717
course.description.toLowerCase().includes(query.toLowerCase())
1818
);
19+
model.setCurrentSearchText(query);
1920
model.setCurrentSearch(searchResults);
2021
console.log(model.currentSearch.length);
2122
};
@@ -36,6 +37,10 @@ const SearchbarPresenter = observer(({ model }) => {
3637
}
3738
};
3839

40+
function resetScoll(){
41+
model.setScrollPosition(0.01);
42+
}
43+
3944
const creditsSum = (favouriteCourses) => {
4045
return favouriteCourses.reduce((sum, course) => sum + parseFloat(course.credits), 0);
4146
};
@@ -60,6 +65,7 @@ const SearchbarPresenter = observer(({ model }) => {
6065
reviewPresenter={reviewPresenter}
6166
prerequisiteTree={preP}
6267
/>;
68+
6369

6470
if(model.filtersCalculated){
6571
searchCourses("");
@@ -68,7 +74,6 @@ const SearchbarPresenter = observer(({ model }) => {
6874

6975
return (
7076
<SearchbarView
71-
model={model}
7277
searchCourses={searchCourses}
7378
favouriteCourses={model.favourites}
7479
removeAllFavourites={removeAllFavourites}
@@ -80,6 +85,7 @@ const SearchbarPresenter = observer(({ model }) => {
8085
popup={popup}
8186
handleFavouriteClick={handleFavouriteClick}
8287
totalCredits={creditsSum(model.favourites)}
88+
resetScrollPosition={resetScoll}
8389
/>
8490
);
8591
});

0 commit comments

Comments
 (0)