Skip to content

Commit cd991a8

Browse files
authored
Persistence (#40)
* Small Fixes and Caching * Changes to favourites * Persistance * Lint Annotations * Even more lint fixes * Change xD * Eeeven more lint
1 parent aa38ae1 commit cd991a8

File tree

11 files changed

+163
-149
lines changed

11 files changed

+163
-149
lines changed

Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ COPY my-app .
44
RUN npm ci
55
ARG CI=false
66
RUN if [ "$CI" = "true" ]; then npm run build; fi
7+
RUN if [ "$CI" = "true" ]; then npm run lint; fi
78
EXPOSE 5173
89
CMD ["npm", "run", "dev"]

my-app/firebase.js

Lines changed: 133 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
import { initializeApp } from "firebase/app";
22
import { getAuth, GoogleAuthProvider, onAuthStateChanged } from "firebase/auth";
3-
import { get, getDatabase, ref, set } from "firebase/database";
4-
3+
import { get, getDatabase, ref, set, onValue } from "firebase/database";
4+
import { reaction, toJS } from "mobx";
5+
// foo
56
// Your web app's Firebase configuration
67
const firebaseConfig = {
7-
apiKey: "AIzaSyCBckVI9nhAP62u5jZJW3F4SLulUv7znis",
8-
authDomain: "findmynextcourse.firebaseapp.com",
9-
databaseURL: "https://findmynextcourse-default-rtdb.europe-west1.firebasedatabase.app",
10-
projectId: "findmynextcourse",
11-
storageBucket: "findmynextcourse.firebasestorage.app",
12-
messagingSenderId: "893484115963",
13-
appId: "1:893484115963:web:59ac087d280dec919ccd5e"
14-
};
15-
8+
apiKey: "AIzaSyCBckVI9nhAP62u5jZJW3F4SLulUv7znis",
9+
authDomain: "findmynextcourse.firebaseapp.com",
10+
databaseURL:
11+
"https://findmynextcourse-default-rtdb.europe-west1.firebasedatabase.app",
12+
projectId: "findmynextcourse",
13+
storageBucket: "findmynextcourse.firebasestorage.app",
14+
messagingSenderId: "893484115963",
15+
appId: "1:893484115963:web:59ac087d280dec919ccd5e",
16+
};
1617

1718
// Initialize Firebase
1819
const app = initializeApp(firebaseConfig);
@@ -21,119 +22,150 @@ export const db = getDatabase(app);
2122
export const googleProvider = new GoogleAuthProvider();
2223
googleProvider.addScope("profile");
2324
googleProvider.addScope("email");
25+
let noUpload = false;
26+
27+
export function connectToFirebase(model) {
28+
loadCoursesFromCacheOrFirebase(model);
29+
onAuthStateChanged(auth, (user) => {
30+
if (user) {
31+
model.setUser(user.uid); // Set the user ID once authenticated
32+
firebaseToModel(model); // Set up listeners for user-specific data
33+
syncModelToFirebase(model); // Start syncing changes to Firebase
34+
} else {
35+
model.setUser(null); // If no user, clear user-specific data
36+
}
37+
});
38+
}
2439

2540
// fetches all relevant information to create the model
2641
async function firebaseToModel(model) {
27-
// Load metadata from localStorage
28-
const cachedMetadata = JSON.parse(localStorage.getItem("coursesMetadata"));
29-
const firebaseTimestamp = await fetchLastUpdatedTimestamp();
30-
// check if up to date
31-
if (cachedMetadata && cachedMetadata.timestamp === firebaseTimestamp) {
32-
console.log("Using cached courses...");
33-
let mergedCourses = [];
34-
for (let i = 0; i < cachedMetadata.parts; i++) {
35-
const part = JSON.parse(localStorage.getItem(`coursesPart${i}`));
36-
if (part)
37-
mergedCourses = mergedCourses.concat(part);
38-
}
39-
model.setCourses(mergedCourses);
42+
if (!model.user)
4043
return;
41-
}
44+
const userRef = ref(db, `users/${model.user}`);
45+
onValue(userRef, (snapshot) => {
46+
if (!snapshot.exists())
47+
return;
48+
const data = snapshot.val();
49+
noUpload = true;
50+
if (data.favourites)
51+
model.setFavourite(data.favourites);
52+
if (data.currentSearch)
53+
model.setCurrentSearch(data.currentSearch);
54+
noUpload = false;
55+
});
56+
}
4257

43-
// Fetch if outdated or missing
44-
console.log("Fetching courses from Firebase...");
45-
const courses = await fetchAllCourses();
46-
model.setCourses(courses);
47-
saveCoursesInChunks(courses, firebaseTimestamp);
58+
59+
export function syncModelToFirebase(model) {
60+
reaction(
61+
() => ({
62+
userId: model?.user,
63+
favourites: toJS(model.favourites),
64+
currentSearch: toJS(model.currentSearch),
65+
// Add more per-user attributes here
66+
}),
67+
({ userId, favourites, currentSearch }) => {
68+
if (noUpload || !userId)
69+
return;
70+
const userRef = ref(db, `users/${userId}`);
71+
const dataToSync = {
72+
favourites,
73+
currentSearch,
74+
};
75+
76+
set(userRef, dataToSync)
77+
.then(() => console.log("User model synced to Firebase"))
78+
.catch(console.error);
79+
}
80+
);
4881
}
4982

5083
function saveCoursesInChunks(courses, timestamp) {
51-
const parts = 3; // Adjust this based on course size
52-
const chunkSize = Math.ceil(courses.length / parts);
53-
54-
for (let i = 0; i < parts; i++) {
55-
const chunk = courses.slice(i * chunkSize, (i + 1) * chunkSize);
56-
localStorage.setItem(`coursesPart${i}`, JSON.stringify(chunk));
57-
}
58-
localStorage.setItem("coursesMetadata", JSON.stringify({ parts, timestamp }));
84+
const parts = 3; // Adjust this based on course size
85+
const chunkSize = Math.ceil(courses.length / parts);
86+
87+
for (let i = 0; i < parts; i++) {
88+
const chunk = courses.slice(i * chunkSize, (i + 1) * chunkSize);
89+
localStorage.setItem(`coursesPart${i}`, JSON.stringify(chunk));
90+
}
91+
localStorage.setItem("coursesMetadata", JSON.stringify({ parts, timestamp }));
5992
}
6093

6194
async function updateLastUpdatedTimestamp() {
62-
const timestampRef = ref(db, "metadata/lastUpdated");
63-
await set(timestampRef, Date.now());
95+
const timestampRef = ref(db, "metadata/lastUpdated");
96+
await set(timestampRef, Date.now());
6497
}
6598

6699
async function fetchLastUpdatedTimestamp() {
67-
const timestampRef = ref(db, "metadata/lastUpdated");
68-
const snapshot = await get(timestampRef);
69-
return snapshot.exists() ? snapshot.val() : 0;
100+
const timestampRef = ref(db, "metadata/lastUpdated");
101+
const snapshot = await get(timestampRef);
102+
return snapshot.exists() ? snapshot.val() : 0;
70103
}
71104

72-
export function connectToFirebase(model) {
73-
onAuthStateChanged(auth, (user) => {
74-
model.setUser(user);
75-
});
76-
firebaseToModel(model);
77-
}
78-
79-
export async function addCourse(course){
80-
if(!course?.code)
81-
return;
82-
const myRef = ref(db, `courses/${course.code}`);
83-
await set(myRef, course);
84-
updateLastUpdatedTimestamp();
105+
export async function addCourse(course) {
106+
if (!course?.code) return;
107+
const myRef = ref(db, `courses/${course.code}`);
108+
await set(myRef, course);
109+
updateLastUpdatedTimestamp();
85110
}
86111

87112
export async function fetchAllCourses() {
88-
const myRef = ref(db, `courses`);
89-
const snapshot = await get(myRef);
90-
91-
if (!snapshot.exists()) return [];
113+
const myRef = ref(db, `courses`);
114+
const snapshot = await get(myRef);
115+
if (!snapshot.exists()) return [];
92116

93-
const value = snapshot.val(); // Firebase returns an object where keys are course IDs
94-
const courses = [];
117+
const value = snapshot.val(); // Firebase returns an object where keys are course IDs
118+
const courses = [];
95119

96-
for (const id of Object.keys(value)) {
97-
courses.push({ id, ...value[id] });
98-
}
99-
100-
return courses;
120+
for (const id of Object.keys(value)) {
121+
courses.push({ id, ...value[id] });
122+
}
123+
return courses;
101124
}
102125

103-
// Before: [ {courseCode: "CS101", name: "Intro to CS"}, {...} ]
104-
// After: { "CS101": { name: "Intro to CS" }, "CS102": {...} }
105-
106-
107-
export async function fetchCoursesSnapshot() {
108-
const myRef = ref(db, `courses`);
109-
const snapshot = await get(myRef);
110-
if (!snapshot.exists()) return {}; // Return empty object instead of array
111-
112-
return snapshot.val(); // Return the object directly
126+
async function loadCoursesFromCacheOrFirebase(model) {
127+
// Load metadata from localStorage
128+
const cachedMetadata = JSON.parse(localStorage.getItem("coursesMetadata"));
129+
const firebaseTimestamp = await fetchLastUpdatedTimestamp();
130+
// check if up to date
131+
if (cachedMetadata && cachedMetadata.timestamp === firebaseTimestamp) {
132+
console.log("Using cached courses...");
133+
let mergedCourses = [];
134+
for (let i = 0; i < cachedMetadata.parts; i++) {
135+
const part = JSON.parse(localStorage.getItem(`coursesPart${i}`));
136+
if (part) mergedCourses = mergedCourses.concat(part);
137+
}
138+
model.setCourses(mergedCourses);
139+
return;
140+
}
141+
142+
// Fetch if outdated or missing
143+
console.log("Fetching courses from Firebase...");
144+
const courses = await fetchAllCourses();
145+
model.setCourses(courses);
146+
saveCoursesInChunks(courses, firebaseTimestamp);
113147
}
114148

115-
export async function saveJSONCoursesToFirebase(model, data){
116-
if(!data || !model){
117-
console.log("no model or data")
118-
return;
119-
}
120-
const entries = Object.entries(data);
121-
entries.forEach(entry => {
122-
const course = {
123-
code : entry[1].code ,
124-
name: entry[1]?.name ?? "",
125-
location: entry[1]?.location ?? "",
126-
department: entry[1]?.department ?? "",
127-
language: entry[1]?.language ?? "",
128-
description: entry[1]?.description ?? "",
129-
academicLevel: entry[1]?.academic_level ?? "",
130-
period: entry[1]?.period ?? "",
131-
credits: entry[1]?.credits ?? 0,
132-
//lectureCount:entry[1].courseLectureCount,
133-
//prerequisites:entry.coursePrerequisites
134-
}
135-
model.addCourse(course);
136-
137-
});
149+
export async function saveJSONCoursesToFirebase(model, data) {
150+
if (!data || !model) {
151+
console.log("no model or data");
152+
return;
153+
}
154+
const entries = Object.entries(data);
155+
entries.forEach((entry) => {
156+
const course = {
157+
code: entry[1].code,
158+
name: entry[1]?.name ?? "",
159+
location: entry[1]?.location ?? "",
160+
department: entry[1]?.department ?? "",
161+
language: entry[1]?.language ?? "",
162+
description: entry[1]?.description ?? "",
163+
academicLevel: entry[1]?.academic_level ?? "",
164+
period: entry[1]?.period ?? "",
165+
credits: entry[1]?.credits ?? 0,
166+
//lectureCount:entry[1].courseLectureCount,
167+
//prerequisites:entry.coursePrerequisites
168+
};
169+
model.addCourse(course);
170+
});
138171
}
139-

my-app/src/model.js

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,15 @@ import { addCourse } from "../firebase";
22

33
export const model = {
44
user: undefined,
5-
currentCourse: undefined,
65
currentSearch: [],
76
courses: [],
87
favourites: [],
9-
isReady: false,
108

119
setUser(user) {
1210
if (!this.user)
1311
this.user = user;
1412
},
1513

16-
setCurrentCourse(course){
17-
this.currentCourse = course;
18-
},
19-
2014
setCurrentSearch(searchResults){
2115
this.currentSearch = searchResults;
2216
},
@@ -33,6 +27,9 @@ export const model = {
3327
console.error("Error adding course:", error);
3428
}
3529
},
30+
setFavourite(favorites){
31+
this.favourites = favorites;
32+
},
3633

3734
addFavourite(course) {
3835
this.favourites = [...this.favourites, course];
@@ -68,13 +65,4 @@ export const model = {
6865
this.addCourse(course);
6966
});
7067
},
71-
72-
searchCourses(query) {
73-
const searchResults = this.courses.filter(course =>
74-
course.code.toLowerCase() === query.toLowerCase() ||
75-
course.name.toLowerCase().includes(query.toLowerCase()) ||
76-
course.description.toLowerCase().includes(query.toLowerCase())
77-
);
78-
this.setCurrentSearch(searchResults);
79-
}
8068
};

my-app/src/pages/App.jsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
import React, { useState } from 'react';
1+
import React from 'react';
22
import { SidebarPresenter } from "../presenters/SidebarPresenter.jsx";
33
import { SearchbarPresenter } from "../presenters/SearchbarPresenter.jsx";
44
import { ListViewPresenter } from '../presenters/ListViewPresenter.jsx';
55
import {model} from '/src/model.js';
66

77
function App() {
8-
const [isPopupOpen, setIsPopupOpen] = useState(false);
9-
108
return (
119
<div className="flex h-screen w-screen">
1210
<div className="flex-auto w-40% h-full bg-gradient-to-t from-[#4f3646] to-[#6747c0]">

my-app/src/presenters/ListViewPresenter.jsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,18 @@ import PrerequisitePresenter from './PrerequisitePresenter.jsx';
77

88
const ListViewPresenter = observer(({ model }) => {
99

10+
const handleFavouriteClick = (course) => {
11+
if (model.favourites.some(fav => fav.code === course.code)) {
12+
model.removeFavourite(course);
13+
} else {
14+
model.addFavourite(course);
15+
}
16+
};
17+
1018
const [isPopupOpen, setIsPopupOpen] = useState(false);
1119
const [selectedCourse, setSelectedCourse] = useState(null);
1220
const preP = <PrerequisitePresenter model={model} selectedCourse={selectedCourse}/>
13-
const popup = <CoursePagePopup isOpen={isPopupOpen} onClose={() => setIsPopupOpen(false)} course={selectedCourse} prerequisiteTree={preP}/>
21+
const popup = <CoursePagePopup isOpen={isPopupOpen} onClose={() => setIsPopupOpen(false)} course={selectedCourse} handleFavouriteClick={handleFavouriteClick} prerequisiteTree={preP}/>
1422

1523
const addFavourite = (course) => {
1624
model.addFavourite(course);
@@ -29,6 +37,7 @@ const ListViewPresenter = observer(({ model }) => {
2937
setIsPopupOpen={setIsPopupOpen}
3038
setSelectedCourse={setSelectedCourse}
3139
popUp={popup}
40+
handleFavouriteClick={handleFavouriteClick}
3241

3342
/>;
3443
});

my-app/src/presenters/SearchbarPresenter.jsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ import SearchbarView from "../views/SearchbarView.jsx";
44

55
const SearchbarPresenter = observer(({ model }) => {
66
const searchCourses = (query) => {
7-
model.searchCourses(query);
7+
const searchResults = model.courses.filter(course =>
8+
course.code.toLowerCase() === query.toLowerCase() ||
9+
course.name.toLowerCase().includes(query.toLowerCase()) ||
10+
course.description.toLowerCase().includes(query.toLowerCase())
11+
);
12+
model.setCurrentSearch(searchResults);
813
}
914

1015
const removeFavourite = (course) => {

my-app/src/presenters/UploadTranscriptPresenter.jsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import UploadField from '../views/Components/SideBarComponents/UploadField';
77

88
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfWorker;
99

10-
const UploadTranscriptPresenter = observer(({ model }) => {
10+
const UploadTranscriptPresenter = observer(() => {
1111
const [errorMessage, setErrorMessage] = useState(""); // Stores error message
1212
const [errorVisibility, setErrorVisibility] = useState("hidden"); // Controls visibility
1313
const [fileInputValue, setFileInputValue] = useState(""); // Controls upload field state
@@ -32,7 +32,6 @@ const UploadTranscriptPresenter = observer(({ model }) => {
3232
const typedArray = new Uint8Array(arrayBuffer);
3333
try {
3434
const pdf = await pdfjsLib.getDocument({ data: typedArray }).promise;
35-
let extractedText = '';
3635

3736
//this is our array we are going to work with
3837
let textObjects = [];

0 commit comments

Comments
 (0)