diff --git a/Dockerfile b/Dockerfile index 042c8c65..69183eae 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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"] diff --git a/my-app/firebase.js b/my-app/firebase.js index 11221da1..642f3f44 100644 --- a/my-app/firebase.js +++ b/my-app/firebase.js @@ -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); @@ -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); + }); } - diff --git a/my-app/src/model.js b/my-app/src/model.js index d98c2e5f..513b20d5 100644 --- a/my-app/src/model.js +++ b/my-app/src/model.js @@ -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; }, @@ -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]; @@ -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); - } }; diff --git a/my-app/src/pages/App.jsx b/my-app/src/pages/App.jsx index a0c0df32..e1d7e9c2 100644 --- a/my-app/src/pages/App.jsx +++ b/my-app/src/pages/App.jsx @@ -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 (
diff --git a/my-app/src/presenters/ListViewPresenter.jsx b/my-app/src/presenters/ListViewPresenter.jsx index d51fbff7..7a91ce72 100644 --- a/my-app/src/presenters/ListViewPresenter.jsx +++ b/my-app/src/presenters/ListViewPresenter.jsx @@ -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 = - const popup = setIsPopupOpen(false)} course={selectedCourse} prerequisiteTree={preP}/> + const popup = setIsPopupOpen(false)} course={selectedCourse} handleFavouriteClick={handleFavouriteClick} prerequisiteTree={preP}/> const addFavourite = (course) => { model.addFavourite(course); @@ -29,6 +37,7 @@ const ListViewPresenter = observer(({ model }) => { setIsPopupOpen={setIsPopupOpen} setSelectedCourse={setSelectedCourse} popUp={popup} + handleFavouriteClick={handleFavouriteClick} />; }); diff --git a/my-app/src/presenters/SearchbarPresenter.jsx b/my-app/src/presenters/SearchbarPresenter.jsx index d9a48e3d..82c16324 100644 --- a/my-app/src/presenters/SearchbarPresenter.jsx +++ b/my-app/src/presenters/SearchbarPresenter.jsx @@ -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) => { diff --git a/my-app/src/presenters/UploadTranscriptPresenter.jsx b/my-app/src/presenters/UploadTranscriptPresenter.jsx index 40fa1bb7..b55c5cb0 100644 --- a/my-app/src/presenters/UploadTranscriptPresenter.jsx +++ b/my-app/src/presenters/UploadTranscriptPresenter.jsx @@ -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 @@ -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 = []; diff --git a/my-app/src/views/Components/CoursePagePopup.jsx b/my-app/src/views/Components/CoursePagePopup.jsx index 8ca557f6..517e00a1 100644 --- a/my-app/src/views/Components/CoursePagePopup.jsx +++ b/my-app/src/views/Components/CoursePagePopup.jsx @@ -26,7 +26,7 @@ function CoursePagePopup({ isOpen, onClose, course, prerequisiteTree }) { className="text-yellow-500 cursor-pointer" onClick={(e) => { e.stopPropagation(); // prevent popup from opening - handleFavouriteClick(course.code); + PaymentResponse.handleFavouriteClick(course.code); }} > {/* {model.favouriteCourses.includes(course.code) diff --git a/my-app/src/views/Components/PrerequisiteTreeComponents/BoxTest.jsx b/my-app/src/views/Components/PrerequisiteTreeComponents/BoxTest.jsx index c008efc1..aa3c0e35 100644 --- a/my-app/src/views/Components/PrerequisiteTreeComponents/BoxTest.jsx +++ b/my-app/src/views/Components/PrerequisiteTreeComponents/BoxTest.jsx @@ -1,6 +1,6 @@ import React from 'react'; -function PrerequisiteTree(props) { +function PrerequisiteTree() { return (
diff --git a/my-app/src/views/Components/SideBarComponents/UploadField.jsx b/my-app/src/views/Components/SideBarComponents/UploadField.jsx index 26104c6b..495b393a 100644 --- a/my-app/src/views/Components/SideBarComponents/UploadField.jsx +++ b/my-app/src/views/Components/SideBarComponents/UploadField.jsx @@ -3,6 +3,9 @@ import CourseTranscriptList from './CourseTranscriptList'; //import * as scraper from '../../../../src/scripts/transcript-scraper/transcript-scraper.js'; import { useState } from "react"; +export default function UploadField(props) { + + const [isDragging, setIsDragging] = useState(false); const handleDragOver = (event) => { diff --git a/my-app/src/views/ListView.jsx b/my-app/src/views/ListView.jsx index de90c051..f8444c08 100644 --- a/my-app/src/views/ListView.jsx +++ b/my-app/src/views/ListView.jsx @@ -12,25 +12,11 @@ function ListView(props) { )); }; - const handleFavouriteClick = (course) => { - if (props.favouriteCourses.some(fav => fav.code === course.code)) { - props.removeFavourite(course); - } else { - props.addFavourite(course); - } - }; - - return (
{coursesToDisplay.length > 0 ? ( coursesToDisplay.map((course) => (
{ - console.log('Clicked:', course); // check browser console - props.setSelectedCourse(course); - props.setIsPopupOpen(true); - }} key={course.code} className="p-5 hover:bg-blue-100 flex items-center border border-b-black border-solid w-full rounded-lg cursor-pointer" > @@ -48,10 +34,7 @@ function ListView(props) { {course.description.length > 150 && ( { - e.stopPropagation(); // Prevent the event from bubbling up. - toggleReadMore(course.code); - }} + onClick={() => toggleReadMore(course.code)} > {readMoreState[course.code] ? @@ -63,10 +46,7 @@ function ListView(props) {
); } -export default ListView; +export default ListView; \ No newline at end of file