Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ COPY my-app .
RUN npm ci
ARG CI=false
RUN if [ "$CI" = "true" ]; then npm run build; fi
RUN if [ "$CI" = "true" ]; then npm run lint; fi
EXPOSE 5173
CMD ["npm", "run", "dev"]
234 changes: 133 additions & 101 deletions my-app/firebase.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { initializeApp } from "firebase/app";
import { getAuth, GoogleAuthProvider, onAuthStateChanged } from "firebase/auth";
import { get, getDatabase, ref, set } from "firebase/database";

import { get, getDatabase, ref, set, onValue } from "firebase/database";
import { reaction, toJS } from "mobx";
// foo
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: "AIzaSyCBckVI9nhAP62u5jZJW3F4SLulUv7znis",
authDomain: "findmynextcourse.firebaseapp.com",
databaseURL: "https://findmynextcourse-default-rtdb.europe-west1.firebasedatabase.app",
projectId: "findmynextcourse",
storageBucket: "findmynextcourse.firebasestorage.app",
messagingSenderId: "893484115963",
appId: "1:893484115963:web:59ac087d280dec919ccd5e"
};
apiKey: "AIzaSyCBckVI9nhAP62u5jZJW3F4SLulUv7znis",
authDomain: "findmynextcourse.firebaseapp.com",
databaseURL:
"https://findmynextcourse-default-rtdb.europe-west1.firebasedatabase.app",
projectId: "findmynextcourse",
storageBucket: "findmynextcourse.firebasestorage.app",
messagingSenderId: "893484115963",
appId: "1:893484115963:web:59ac087d280dec919ccd5e",
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);
Expand All @@ -21,119 +22,150 @@ export const db = getDatabase(app);
export const googleProvider = new GoogleAuthProvider();
googleProvider.addScope("profile");
googleProvider.addScope("email");
let noUpload = false;

export function connectToFirebase(model) {
loadCoursesFromCacheOrFirebase(model);
onAuthStateChanged(auth, (user) => {
if (user) {
model.setUser(user.uid); // Set the user ID once authenticated
firebaseToModel(model); // Set up listeners for user-specific data
syncModelToFirebase(model); // Start syncing changes to Firebase
} else {
model.setUser(null); // If no user, clear user-specific data
}
});
}

// fetches all relevant information to create the model
async function firebaseToModel(model) {
// Load metadata from localStorage
const cachedMetadata = JSON.parse(localStorage.getItem("coursesMetadata"));
const firebaseTimestamp = await fetchLastUpdatedTimestamp();
// check if up to date
if (cachedMetadata && cachedMetadata.timestamp === firebaseTimestamp) {
console.log("Using cached courses...");
let mergedCourses = [];
for (let i = 0; i < cachedMetadata.parts; i++) {
const part = JSON.parse(localStorage.getItem(`coursesPart${i}`));
if (part)
mergedCourses = mergedCourses.concat(part);
}
model.setCourses(mergedCourses);
if (!model.user)
return;
}
const userRef = ref(db, `users/${model.user}`);
onValue(userRef, (snapshot) => {
if (!snapshot.exists())
return;
const data = snapshot.val();
noUpload = true;
if (data.favourites)
model.setFavourite(data.favourites);
if (data.currentSearch)
model.setCurrentSearch(data.currentSearch);
noUpload = false;
});
}

// Fetch if outdated or missing
console.log("Fetching courses from Firebase...");
const courses = await fetchAllCourses();
model.setCourses(courses);
saveCoursesInChunks(courses, firebaseTimestamp);

export function syncModelToFirebase(model) {
reaction(
() => ({
userId: model?.user,
favourites: toJS(model.favourites),
currentSearch: toJS(model.currentSearch),
// Add more per-user attributes here
}),
({ userId, favourites, currentSearch }) => {
if (noUpload || !userId)
return;
const userRef = ref(db, `users/${userId}`);
const dataToSync = {
favourites,
currentSearch,
};

set(userRef, dataToSync)
.then(() => console.log("User model synced to Firebase"))
.catch(console.error);
}
);
}

function saveCoursesInChunks(courses, timestamp) {
const parts = 3; // Adjust this based on course size
const chunkSize = Math.ceil(courses.length / parts);

for (let i = 0; i < parts; i++) {
const chunk = courses.slice(i * chunkSize, (i + 1) * chunkSize);
localStorage.setItem(`coursesPart${i}`, JSON.stringify(chunk));
}
localStorage.setItem("coursesMetadata", JSON.stringify({ parts, timestamp }));
const parts = 3; // Adjust this based on course size
const chunkSize = Math.ceil(courses.length / parts);

for (let i = 0; i < parts; i++) {
const chunk = courses.slice(i * chunkSize, (i + 1) * chunkSize);
localStorage.setItem(`coursesPart${i}`, JSON.stringify(chunk));
}
localStorage.setItem("coursesMetadata", JSON.stringify({ parts, timestamp }));
}

async function updateLastUpdatedTimestamp() {
const timestampRef = ref(db, "metadata/lastUpdated");
await set(timestampRef, Date.now());
const timestampRef = ref(db, "metadata/lastUpdated");
await set(timestampRef, Date.now());
}

async function fetchLastUpdatedTimestamp() {
const timestampRef = ref(db, "metadata/lastUpdated");
const snapshot = await get(timestampRef);
return snapshot.exists() ? snapshot.val() : 0;
const timestampRef = ref(db, "metadata/lastUpdated");
const snapshot = await get(timestampRef);
return snapshot.exists() ? snapshot.val() : 0;
}

export function connectToFirebase(model) {
onAuthStateChanged(auth, (user) => {
model.setUser(user);
});
firebaseToModel(model);
}

export async function addCourse(course){
if(!course?.code)
return;
const myRef = ref(db, `courses/${course.code}`);
await set(myRef, course);
updateLastUpdatedTimestamp();
export async function addCourse(course) {
if (!course?.code) return;
const myRef = ref(db, `courses/${course.code}`);
await set(myRef, course);
updateLastUpdatedTimestamp();
}

export async function fetchAllCourses() {
const myRef = ref(db, `courses`);
const snapshot = await get(myRef);

if (!snapshot.exists()) return [];
const myRef = ref(db, `courses`);
const snapshot = await get(myRef);
if (!snapshot.exists()) return [];

const value = snapshot.val(); // Firebase returns an object where keys are course IDs
const courses = [];
const value = snapshot.val(); // Firebase returns an object where keys are course IDs
const courses = [];

for (const id of Object.keys(value)) {
courses.push({ id, ...value[id] });
}

return courses;
for (const id of Object.keys(value)) {
courses.push({ id, ...value[id] });
}
return courses;
}

// Before: [ {courseCode: "CS101", name: "Intro to CS"}, {...} ]
// After: { "CS101": { name: "Intro to CS" }, "CS102": {...} }


export async function fetchCoursesSnapshot() {
const myRef = ref(db, `courses`);
const snapshot = await get(myRef);
if (!snapshot.exists()) return {}; // Return empty object instead of array

return snapshot.val(); // Return the object directly
async function loadCoursesFromCacheOrFirebase(model) {
// Load metadata from localStorage
const cachedMetadata = JSON.parse(localStorage.getItem("coursesMetadata"));
const firebaseTimestamp = await fetchLastUpdatedTimestamp();
// check if up to date
if (cachedMetadata && cachedMetadata.timestamp === firebaseTimestamp) {
console.log("Using cached courses...");
let mergedCourses = [];
for (let i = 0; i < cachedMetadata.parts; i++) {
const part = JSON.parse(localStorage.getItem(`coursesPart${i}`));
if (part) mergedCourses = mergedCourses.concat(part);
}
model.setCourses(mergedCourses);
return;
}

// Fetch if outdated or missing
console.log("Fetching courses from Firebase...");
const courses = await fetchAllCourses();
model.setCourses(courses);
saveCoursesInChunks(courses, firebaseTimestamp);
}

export async function saveJSONCoursesToFirebase(model, data){
if(!data || !model){
console.log("no model or data")
return;
}
const entries = Object.entries(data);
entries.forEach(entry => {
const course = {
code : entry[1].code ,
name: entry[1]?.name ?? "",
location: entry[1]?.location ?? "",
department: entry[1]?.department ?? "",
language: entry[1]?.language ?? "",
description: entry[1]?.description ?? "",
academicLevel: entry[1]?.academic_level ?? "",
period: entry[1]?.period ?? "",
credits: entry[1]?.credits ?? 0,
//lectureCount:entry[1].courseLectureCount,
//prerequisites:entry.coursePrerequisites
}
model.addCourse(course);

});
export async function saveJSONCoursesToFirebase(model, data) {
if (!data || !model) {
console.log("no model or data");
return;
}
const entries = Object.entries(data);
entries.forEach((entry) => {
const course = {
code: entry[1].code,
name: entry[1]?.name ?? "",
location: entry[1]?.location ?? "",
department: entry[1]?.department ?? "",
language: entry[1]?.language ?? "",
description: entry[1]?.description ?? "",
academicLevel: entry[1]?.academic_level ?? "",
period: entry[1]?.period ?? "",
credits: entry[1]?.credits ?? 0,
//lectureCount:entry[1].courseLectureCount,
//prerequisites:entry.coursePrerequisites
};
model.addCourse(course);
});
}

18 changes: 3 additions & 15 deletions my-app/src/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,15 @@ import { addCourse } from "../firebase";

export const model = {
user: undefined,
currentCourse: undefined,
currentSearch: [],
courses: [],
favourites: [],
isReady: false,

setUser(user) {
if (!this.user)
this.user = user;
},

setCurrentCourse(course){
this.currentCourse = course;
},

setCurrentSearch(searchResults){
this.currentSearch = searchResults;
},
Expand All @@ -33,6 +27,9 @@ export const model = {
console.error("Error adding course:", error);
}
},
setFavourite(favorites){
this.favourites = favorites;
},

addFavourite(course) {
this.favourites = [...this.favourites, course];
Expand Down Expand Up @@ -68,13 +65,4 @@ export const model = {
this.addCourse(course);
});
},

searchCourses(query) {
const searchResults = this.courses.filter(course =>
course.code.toLowerCase() === query.toLowerCase() ||
course.name.toLowerCase().includes(query.toLowerCase()) ||
course.description.toLowerCase().includes(query.toLowerCase())
);
this.setCurrentSearch(searchResults);
}
};
4 changes: 1 addition & 3 deletions my-app/src/pages/App.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import React, { useState } from 'react';
import React from 'react';
import { SidebarPresenter } from "../presenters/SidebarPresenter.jsx";
import { SearchbarPresenter } from "../presenters/SearchbarPresenter.jsx";
import { ListViewPresenter } from '../presenters/ListViewPresenter.jsx';
import {model} from '/src/model.js';

function App() {
const [isPopupOpen, setIsPopupOpen] = useState(false);

return (
<div className="flex h-screen w-screen">
<div className="flex-auto w-40% h-full bg-gradient-to-t from-[#4f3646] to-[#6747c0]">
Expand Down
11 changes: 10 additions & 1 deletion my-app/src/presenters/ListViewPresenter.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,18 @@ import PrerequisitePresenter from './PrerequisitePresenter.jsx';

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

const handleFavouriteClick = (course) => {
if (model.favourites.some(fav => fav.code === course.code)) {
model.removeFavourite(course);
} else {
model.addFavourite(course);
}
};

const [isPopupOpen, setIsPopupOpen] = useState(false);
const [selectedCourse, setSelectedCourse] = useState(null);
const preP = <PrerequisitePresenter model={model} selectedCourse={selectedCourse}/>
const popup = <CoursePagePopup isOpen={isPopupOpen} onClose={() => setIsPopupOpen(false)} course={selectedCourse} prerequisiteTree={preP}/>
const popup = <CoursePagePopup isOpen={isPopupOpen} onClose={() => setIsPopupOpen(false)} course={selectedCourse} handleFavouriteClick={handleFavouriteClick} prerequisiteTree={preP}/>

const addFavourite = (course) => {
model.addFavourite(course);
Expand All @@ -29,6 +37,7 @@ const ListViewPresenter = observer(({ model }) => {
setIsPopupOpen={setIsPopupOpen}
setSelectedCourse={setSelectedCourse}
popUp={popup}
handleFavouriteClick={handleFavouriteClick}

/>;
});
Expand Down
7 changes: 6 additions & 1 deletion my-app/src/presenters/SearchbarPresenter.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import SearchbarView from "../views/SearchbarView.jsx";

const SearchbarPresenter = observer(({ model }) => {
const searchCourses = (query) => {
model.searchCourses(query);
const searchResults = model.courses.filter(course =>
course.code.toLowerCase() === query.toLowerCase() ||
course.name.toLowerCase().includes(query.toLowerCase()) ||
course.description.toLowerCase().includes(query.toLowerCase())
);
model.setCurrentSearch(searchResults);
}

const removeFavourite = (course) => {
Expand Down
3 changes: 1 addition & 2 deletions my-app/src/presenters/UploadTranscriptPresenter.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import UploadField from '../views/Components/SideBarComponents/UploadField';

pdfjsLib.GlobalWorkerOptions.workerSrc = pdfWorker;

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

//this is our array we are going to work with
let textObjects = [];
Expand Down
Loading