From 414cf16d7b0f99a090dee568118b96d5a1a917ac Mon Sep 17 00:00:00 2001 From: Justus Kluge Date: Thu, 10 Apr 2025 08:26:06 +0200 Subject: [PATCH 01/16] Favourite Clear Button --- my-app/src/presenters/SearchbarPresenter.jsx | 5 +++ .../views/Components/FavouriteDropdown.jsx | 36 ++++++++++--------- my-app/src/views/SearchbarView.jsx | 1 + 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/my-app/src/presenters/SearchbarPresenter.jsx b/my-app/src/presenters/SearchbarPresenter.jsx index 82c16324..4f888e94 100644 --- a/my-app/src/presenters/SearchbarPresenter.jsx +++ b/my-app/src/presenters/SearchbarPresenter.jsx @@ -16,12 +16,17 @@ const SearchbarPresenter = observer(({ model }) => { model.removeFavourite(course); } + function removeAllFavourites(){ + model.setFavourite([]); + } + return ( ); }); diff --git a/my-app/src/views/Components/FavouriteDropdown.jsx b/my-app/src/views/Components/FavouriteDropdown.jsx index 855d4e87..858a7cac 100644 --- a/my-app/src/views/Components/FavouriteDropdown.jsx +++ b/my-app/src/views/Components/FavouriteDropdown.jsx @@ -4,26 +4,30 @@ function FavouritesDropdown(props) { return (
{props.favouriteCourses.length > 0 ? ( - props.favouriteCourses.map(course => ( -
-

- {course.name} -

- -
- )) - ) : ( + + props.favouriteCourses.map(course => ( +
+

+ {course.name} +

+ +
+ )) + + + ) : (
No favourites
)} + {props.favouriteCourses.length > 0 ? : ""}
); } diff --git a/my-app/src/views/SearchbarView.jsx b/my-app/src/views/SearchbarView.jsx index 9cc07b02..c888c902 100644 --- a/my-app/src/views/SearchbarView.jsx +++ b/my-app/src/views/SearchbarView.jsx @@ -71,6 +71,7 @@ function SearchbarView(props) { courses={props.courses} favouriteCourses={props.favouriteCourses} removeFavourite={props.removeFavourite} + removeAllFavourites={props.removeAllFavourites} /> )} From 97e47d53c31963923d95093b7814d0012d4b56e8 Mon Sep 17 00:00:00 2001 From: Justus Kluge Date: Thu, 10 Apr 2025 10:54:19 +0200 Subject: [PATCH 02/16] scrolling I --- my-app/src/model.js | 1 + 1 file changed, 1 insertion(+) diff --git a/my-app/src/model.js b/my-app/src/model.js index 513b20d5..3f0931b9 100644 --- a/my-app/src/model.js +++ b/my-app/src/model.js @@ -3,6 +3,7 @@ import { addCourse } from "../firebase"; export const model = { user: undefined, currentSearch: [], + currentScrollPosition : "", courses: [], favourites: [], From 174155a779ef2184afb9d79c4039d1d8125572b0 Mon Sep 17 00:00:00 2001 From: Justus Kluge Date: Thu, 10 Apr 2025 14:34:14 +0200 Subject: [PATCH 03/16] Allow to cache query --- my-app/firebase.js | 7 +++++-- my-app/src/model.js | 5 +++++ my-app/src/presenters/SearchbarPresenter.jsx | 1 + 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/my-app/firebase.js b/my-app/firebase.js index 99cceddd..db085087 100644 --- a/my-app/firebase.js +++ b/my-app/firebase.js @@ -48,6 +48,8 @@ async function firebaseToModel(model) { noUpload = true; if (data.favourites) model.setFavourite(data.favourites); + if(data.currentSearchText) + model.setCurrentSearchText(data.currentSearchText); // if (data.currentSearch) // model.setCurrentSearch(data.currentSearch); noUpload = false; @@ -60,17 +62,18 @@ export function syncModelToFirebase(model) { () => ({ userId: model?.user, favourites: toJS(model.favourites), + currentSearchText : toJS(model.currentSearchText), // currentSearch: toJS(model.currentSearch), // Add more per-user attributes here }), // eslint-disable-next-line no-unused-vars - ({ userId, favourites, currentSearch }) => { + ({ userId, favourites, currentSearchText }) => { if (noUpload || !userId) return; const userRef = ref(db, `users/${userId}`); const dataToSync = { favourites, - //currentSearch, + currentSearchText, }; set(userRef, dataToSync) diff --git a/my-app/src/model.js b/my-app/src/model.js index 8fe85f90..3875c762 100644 --- a/my-app/src/model.js +++ b/my-app/src/model.js @@ -4,6 +4,7 @@ import { addCourse, addReviewForCourse, getReviewsForCourse } from "../firebase" export const model = { user: undefined, currentSearch: [], + currentSearchText: "", currentScrollPosition : "", courses: [], favourites: [], @@ -17,6 +18,10 @@ export const model = { this.currentSearch = searchResults; }, + setCurrentSearchText(text){ + this.currentSearchText = text; + }, + setCourses(courses){ this.courses = courses; }, diff --git a/my-app/src/presenters/SearchbarPresenter.jsx b/my-app/src/presenters/SearchbarPresenter.jsx index 4f888e94..cb830396 100644 --- a/my-app/src/presenters/SearchbarPresenter.jsx +++ b/my-app/src/presenters/SearchbarPresenter.jsx @@ -9,6 +9,7 @@ const SearchbarPresenter = observer(({ model }) => { course.name.toLowerCase().includes(query.toLowerCase()) || course.description.toLowerCase().includes(query.toLowerCase()) ); + model.setCurrentSearchText(query); model.setCurrentSearch(searchResults); } From 30c34b1f5a21e859fc4695280e08a042bde13e9b Mon Sep 17 00:00:00 2001 From: Justus Kluge Date: Fri, 11 Apr 2025 11:05:34 +0200 Subject: [PATCH 04/16] Persistant Scrolling P.2 --- my-app/firebase.js | 32 +++++++++++++++++---- my-app/src/model.js | 6 +++- my-app/src/presenters/ListViewPresenter.jsx | 29 +++++++++++++++++-- my-app/src/views/ListView.jsx | 2 +- 4 files changed, 60 insertions(+), 9 deletions(-) diff --git a/my-app/firebase.js b/my-app/firebase.js index 9c7ff08d..6fd48a8c 100644 --- a/my-app/firebase.js +++ b/my-app/firebase.js @@ -30,6 +30,7 @@ export function connectToFirebase(model) { model.setUser(user); // Set the user ID once authenticated firebaseToModel(model); // Set up listeners for user-specific data syncModelToFirebase(model); // Start syncing changes to Firebase + syncScrollPositionToFirebase(model); } else { model.setUser(null); // If no user, clear user-specific data } @@ -83,6 +84,31 @@ export function syncModelToFirebase(model) { ); } +function syncScrollPositionToFirebase(model) { + reaction( + () => ({ + // Here we calculate a percentage based on window.scrollY and total scrollable height. + scrollPercentage: window.scrollY / document.documentElement.scrollHeight, + }), + ({ scrollPercentage }) => { + if (model?.user?.uid) { + const userRef = ref(db, `users/${model.user.uid}/scrollPosition`); + set(userRef, { scrollPercentage }) + .catch(console.error); + } + } + ); +} + +export async function getScrollPositionFromFirebase(userId) { + const scrollRef = ref(db, `users/${userId}/scrollPosition`); + const snapshot = await get(scrollRef); + if (snapshot.exists()) { + return snapshot.val().scrollPercentage || 0; + } + return 0; +} + function saveCoursesInChunks(courses, timestamp) { const parts = 3; // Adjust this based on course size const chunkSize = Math.ceil(courses.length / parts); @@ -199,8 +225,4 @@ export async function getReviewsForCourse(courseCode) { }); }); return reviews; -} - - - - +} \ No newline at end of file diff --git a/my-app/src/model.js b/my-app/src/model.js index 3875c762..7f47ad00 100644 --- a/my-app/src/model.js +++ b/my-app/src/model.js @@ -5,7 +5,7 @@ export const model = { user: undefined, currentSearch: [], currentSearchText: "", - currentScrollPosition : "", + scrollPosition: 0, courses: [], favourites: [], @@ -21,6 +21,10 @@ export const model = { setCurrentSearchText(text){ this.currentSearchText = text; }, + + setScrollPosition(position) { + this.scrollPosition = position; // This method updates the scroll position + }, setCourses(courses){ this.courses = courses; diff --git a/my-app/src/presenters/ListViewPresenter.jsx b/my-app/src/presenters/ListViewPresenter.jsx index defb8a66..7e544cf3 100644 --- a/my-app/src/presenters/ListViewPresenter.jsx +++ b/my-app/src/presenters/ListViewPresenter.jsx @@ -1,12 +1,38 @@ import React from 'react'; import { observer } from "mobx-react-lite"; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import ListView from "../views/ListView.jsx"; import CoursePagePopup from '../views/Components/CoursePagePopup.jsx'; import PrerequisitePresenter from './PrerequisitePresenter.jsx'; import {ReviewPresenter} from "../presenters/ReviewPresenter.jsx" const ListViewPresenter = observer(({ model }) => { + + useEffect(() => { + const handleScroll = () => { + // Calculate the percentage + const scrollPercentage = window.scrollY / document.documentElement.scrollHeight; + // Update the model's scroll position + model.setScrollPosition(scrollPercentage); + // If not logged in, also store in localStorage + if (!model.user) { + localStorage.setItem("scrollPercentage", scrollPercentage); + } + }; + + window.addEventListener("scroll", handleScroll); + return () => window.removeEventListener("scroll", handleScroll); + }, [model]); + + useEffect(() => { + if (!model.user) { + const stored = localStorage.getItem("scrollPercentage"); + if (stored) { + window.scrollTo(0, stored * document.documentElement.scrollHeight); + } + } + }, [model.user]); + const addFavourite = (course) => { model.addFavourite(course); } @@ -49,7 +75,6 @@ const ListViewPresenter = observer(({ model }) => { setSelectedCourse={setSelectedCourse} popup={popup} handleFavouriteClick={handleFavouriteClick} - />; }); diff --git a/my-app/src/views/ListView.jsx b/my-app/src/views/ListView.jsx index 289bcef2..b2ababda 100644 --- a/my-app/src/views/ListView.jsx +++ b/my-app/src/views/ListView.jsx @@ -24,7 +24,7 @@ function ListView(props) { props.addFavourite(course); } }; - + useEffect(() => { setIsLoading(true); const initialCourses = coursesToDisplay.slice(0, 10); From cd10da121f93b1852d5f1c854a004cc481b43f9e Mon Sep 17 00:00:00 2001 From: Justus Kluge Date: Fri, 11 Apr 2025 11:27:34 +0200 Subject: [PATCH 05/16] Scrolling Pt.3 --- my-app/firebase.js | 9 +++------ my-app/src/presenters/ListViewPresenter.jsx | 20 ++++++++++++-------- my-app/src/views/ListView.jsx | 13 +++++++++++++ 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/my-app/firebase.js b/my-app/firebase.js index 6fd48a8c..8838dd1d 100644 --- a/my-app/firebase.js +++ b/my-app/firebase.js @@ -86,14 +86,11 @@ export function syncModelToFirebase(model) { function syncScrollPositionToFirebase(model) { reaction( - () => ({ - // Here we calculate a percentage based on window.scrollY and total scrollable height. - scrollPercentage: window.scrollY / document.documentElement.scrollHeight, - }), - ({ scrollPercentage }) => { + () => window.scrollY, + (scrollPixel) => { if (model?.user?.uid) { const userRef = ref(db, `users/${model.user.uid}/scrollPosition`); - set(userRef, { scrollPercentage }) + set(userRef, { scrollPixel }) .catch(console.error); } } diff --git a/my-app/src/presenters/ListViewPresenter.jsx b/my-app/src/presenters/ListViewPresenter.jsx index 7e544cf3..c0788c1e 100644 --- a/my-app/src/presenters/ListViewPresenter.jsx +++ b/my-app/src/presenters/ListViewPresenter.jsx @@ -10,13 +10,11 @@ const ListViewPresenter = observer(({ model }) => { useEffect(() => { const handleScroll = () => { - // Calculate the percentage - const scrollPercentage = window.scrollY / document.documentElement.scrollHeight; - // Update the model's scroll position - model.setScrollPosition(scrollPercentage); - // If not logged in, also store in localStorage + const scrollPixel = window.scrollY; + model.setScrollPosition(scrollPixel); + // If not logged in, also save in localStorage (absolute pixel value) if (!model.user) { - localStorage.setItem("scrollPercentage", scrollPercentage); + localStorage.setItem("scrollPosition", scrollPixel); } }; @@ -24,11 +22,16 @@ const ListViewPresenter = observer(({ model }) => { return () => window.removeEventListener("scroll", handleScroll); }, [model]); + // When user is not logged in, restore scroll from localStorage on login state change. useEffect(() => { if (!model.user) { - const stored = localStorage.getItem("scrollPercentage"); + const stored = localStorage.getItem("scrollPosition"); if (stored) { - window.scrollTo(0, stored * document.documentElement.scrollHeight); + const target = Number(stored); + // Only scroll if the document is tall enough + if (document.documentElement.scrollHeight > target) { + window.scrollTo(0, target); + } } } }, [model.user]); @@ -75,6 +78,7 @@ const ListViewPresenter = observer(({ model }) => { setSelectedCourse={setSelectedCourse} popup={popup} handleFavouriteClick={handleFavouriteClick} + targetScroll={model.scrollPosition} />; }); diff --git a/my-app/src/views/ListView.jsx b/my-app/src/views/ListView.jsx index 8135a4be..534ec1bf 100644 --- a/my-app/src/views/ListView.jsx +++ b/my-app/src/views/ListView.jsx @@ -41,6 +41,19 @@ function ListView(props) { setHasMore(displayedCourses.length + nextItems.length < coursesToDisplay.length); }, [displayedCourses.length, coursesToDisplay, hasMore]); + useEffect(() => { + if (!props.targetScroll) return; // nothing to restore + + const currentHeight = document.documentElement.scrollHeight; + if (currentHeight < props.targetScroll && hasMore) { + // Load more courses, then try again. + fetchMoreCourses(); + } else { + // Once enough content has loaded, scroll to the target. + window.scrollTo(0, props.targetScroll); + } + }, [displayedCourses, hasMore, props.targetScroll, fetchMoreCourses]); + return (
{isLoading ? ( From d42cb2d783f0a293256c6013643f8efe30d50287 Mon Sep 17 00:00:00 2001 From: Justus Kluge Date: Fri, 11 Apr 2025 14:59:06 +0200 Subject: [PATCH 06/16] Persistant Scrolling --- my-app/firebase.js | 119 ++++++++++---------- my-app/package-lock.json | 7 ++ my-app/package.json | 1 + my-app/src/presenters/ListViewPresenter.jsx | 43 +++---- my-app/src/views/ListView.jsx | 32 +++--- 5 files changed, 104 insertions(+), 98 deletions(-) diff --git a/my-app/firebase.js b/my-app/firebase.js index 8838dd1d..efd9e7a3 100644 --- a/my-app/firebase.js +++ b/my-app/firebase.js @@ -2,6 +2,8 @@ import { initializeApp } from "firebase/app"; import { getAuth, GoogleAuthProvider, onAuthStateChanged } from "firebase/auth"; import { get, getDatabase, ref, set, onValue, push } from "firebase/database"; import { reaction, toJS } from "mobx"; +import throttle from "lodash.throttle"; + // Your web app's Firebase configuration const firebaseConfig = { apiKey: "AIzaSyCBckVI9nhAP62u5jZJW3F4SLulUv7znis", @@ -24,53 +26,50 @@ googleProvider.addScope("email"); let noUpload = false; export function connectToFirebase(model) { - loadCoursesFromCacheOrFirebase(model); + loadCoursesFromCacheOrFirebase(model); onAuthStateChanged(auth, (user) => { if (user) { model.setUser(user); // Set the user ID once authenticated - firebaseToModel(model); // Set up listeners for user-specific data - syncModelToFirebase(model); // Start syncing changes to Firebase + firebaseToModel(model); // Set up listeners for user-specific data + syncModelToFirebase(model); // Start syncing changes to Firebase syncScrollPositionToFirebase(model); } else { - model.setUser(null); // If no user, clear user-specific data + model.setUser(null); // If no user, clear user-specific data } }); } // fetches all relevant information to create the model async function firebaseToModel(model) { - if (!model.user) - return; + if (!model.user) return; const userRef = ref(db, `users/${model.user.uid}`); onValue(userRef, (snapshot) => { - if (!snapshot.exists()) - return; + if (!snapshot.exists()) return; const data = snapshot.val(); noUpload = true; - if (data.favourites) - model.setFavourite(data.favourites); - if(data.currentSearchText) + if (data.favourites) model.setFavourite(data.favourites); + if (data.currentSearchText) model.setCurrentSearchText(data.currentSearchText); - // if (data.currentSearch) - // model.setCurrentSearch(data.currentSearch); + if (data.scrollPosition) + model.setScrollPosition(data.scrollPosition); + // if (data.currentSearch) + // model.setCurrentSearch(data.currentSearch); noUpload = false; }); } - export function syncModelToFirebase(model) { reaction( () => ({ userId: model?.user.uid, favourites: toJS(model.favourites), - currentSearchText : toJS(model.currentSearchText), + currentSearchText: toJS(model.currentSearchText), // currentSearch: toJS(model.currentSearch), // Add more per-user attributes here }), // eslint-disable-next-line no-unused-vars ({ userId, favourites, currentSearchText }) => { - if (noUpload || !userId) - return; + if (noUpload || !userId) return; const userRef = ref(db, `users/${userId}`); const dataToSync = { favourites, @@ -84,28 +83,36 @@ export function syncModelToFirebase(model) { ); } -function syncScrollPositionToFirebase(model) { - reaction( - () => window.scrollY, - (scrollPixel) => { - if (model?.user?.uid) { - const userRef = ref(db, `users/${model.user.uid}/scrollPosition`); - set(userRef, { scrollPixel }) - .catch(console.error); - } +export function syncScrollPositionToFirebase(model, containerRef) { + if (!containerRef?.current) return; + + const throttledSet = throttle((scrollPixel) => { + if (model?.user?.uid) { + const userRef = ref(db, `users/${model.user.uid}/scrollPosition`); + set(userRef, scrollPixel).catch(console.error); } - ); -} + }, 500); -export async function getScrollPositionFromFirebase(userId) { - const scrollRef = ref(db, `users/${userId}/scrollPosition`); - const snapshot = await get(scrollRef); - if (snapshot.exists()) { - return snapshot.val().scrollPercentage || 0; - } - return 0; + const handleScroll = () => { + const scrollTop = containerRef.current.scrollTop; + model.setScrollPosition(scrollTop); + if (!model.user) { + localStorage.setItem("scrollPosition", scrollTop); + } + throttledSet(scrollTop); + }; + + containerRef.current.addEventListener('scroll', handleScroll); + + // Return cleanup function + return () => { + if (containerRef.current) { + containerRef.current.removeEventListener('scroll', handleScroll); + } + }; } + function saveCoursesInChunks(courses, timestamp) { const parts = 3; // Adjust this based on course size const chunkSize = Math.ceil(courses.length / parts); @@ -196,30 +203,28 @@ export async function saveJSONCoursesToFirebase(model, data) { }); } - export async function addReviewForCourse(courseCode, review) { - try { - const reviewsRef = ref(db, `reviews/${courseCode}`); - const newReviewRef = push(reviewsRef); - await set(newReviewRef, review); - } catch (error) { - console.error("Error when adding a course to firebase:", error); + try { + const reviewsRef = ref(db, `reviews/${courseCode}`); + const newReviewRef = push(reviewsRef); + await set(newReviewRef, review); + } catch (error) { + console.error("Error when adding a course to firebase:", error); } } - export async function getReviewsForCourse(courseCode) { - const reviewsRef = ref(db, `reviews/${courseCode}`); - const snapshot = await get(reviewsRef); - if (!snapshot.exists()) return []; - - const reviews = []; - snapshot.forEach(childSnapshot => { - reviews.push({ - id: childSnapshot.key, // Firebase-generated unique key - userName: childSnapshot.val().userName, - text: childSnapshot.val().text - }); - }); - return reviews; -} \ No newline at end of file + const reviewsRef = ref(db, `reviews/${courseCode}`); + const snapshot = await get(reviewsRef); + if (!snapshot.exists()) return []; + + const reviews = []; + snapshot.forEach((childSnapshot) => { + reviews.push({ + id: childSnapshot.key, // Firebase-generated unique key + userName: childSnapshot.val().userName, + text: childSnapshot.val().text, + }); + }); + return reviews; +} diff --git a/my-app/package-lock.json b/my-app/package-lock.json index 5f3cfa28..0926f626 100644 --- a/my-app/package-lock.json +++ b/my-app/package-lock.json @@ -12,6 +12,7 @@ "autoprefixer": "^10.4.21", "firebase": "^11.5.0", "ldrs": "^1.1.6", + "lodash.throttle": "^4.1.1", "mobx": "^6.13.7", "mobx-react-lite": "^4.1.0", "pdfjs-dist": "^5.1.91", @@ -4213,6 +4214,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "license": "MIT" + }, "node_modules/long": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/long/-/long-5.3.1.tgz", diff --git a/my-app/package.json b/my-app/package.json index b07d11bf..b4daf7fb 100644 --- a/my-app/package.json +++ b/my-app/package.json @@ -14,6 +14,7 @@ "autoprefixer": "^10.4.21", "firebase": "^11.5.0", "ldrs": "^1.1.6", + "lodash.throttle": "^4.1.1", "mobx": "^6.13.7", "mobx-react-lite": "^4.1.0", "pdfjs-dist": "^5.1.91", diff --git a/my-app/src/presenters/ListViewPresenter.jsx b/my-app/src/presenters/ListViewPresenter.jsx index c0788c1e..3325ca8f 100644 --- a/my-app/src/presenters/ListViewPresenter.jsx +++ b/my-app/src/presenters/ListViewPresenter.jsx @@ -1,41 +1,31 @@ import React from 'react'; import { observer } from "mobx-react-lite"; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import ListView from "../views/ListView.jsx"; import CoursePagePopup from '../views/Components/CoursePagePopup.jsx'; import PrerequisitePresenter from './PrerequisitePresenter.jsx'; import {ReviewPresenter} from "../presenters/ReviewPresenter.jsx" +import {syncScrollPositionToFirebase} from "../../firebase.js" const ListViewPresenter = observer(({ model }) => { + + const scrollContainerRef = useRef(null); useEffect(() => { - const handleScroll = () => { - const scrollPixel = window.scrollY; - model.setScrollPosition(scrollPixel); - // If not logged in, also save in localStorage (absolute pixel value) - if (!model.user) { - localStorage.setItem("scrollPosition", scrollPixel); - } - }; - - window.addEventListener("scroll", handleScroll); - return () => window.removeEventListener("scroll", handleScroll); - }, [model]); - - // When user is not logged in, restore scroll from localStorage on login state change. - useEffect(() => { - if (!model.user) { - const stored = localStorage.getItem("scrollPosition"); - if (stored) { - const target = Number(stored); - // Only scroll if the document is tall enough - if (document.documentElement.scrollHeight > target) { - window.scrollTo(0, target); - } - } + // Load initial scroll position + const savedPosition = model.user + ? model.scrollPosition + : localStorage.getItem("scrollPosition"); + if (savedPosition) { + model.setScrollPosition(parseInt(savedPosition, 10)); } }, [model.user]); - + + useEffect(() => { + const cleanup = syncScrollPositionToFirebase(model, scrollContainerRef); + return () => cleanup(); + }, [model.user, scrollContainerRef]); + const addFavourite = (course) => { model.addFavourite(course); } @@ -79,6 +69,7 @@ const ListViewPresenter = observer(({ model }) => { popup={popup} handleFavouriteClick={handleFavouriteClick} targetScroll={model.scrollPosition} + scrollContainerRef={scrollContainerRef} />; }); diff --git a/my-app/src/views/ListView.jsx b/my-app/src/views/ListView.jsx index 534ec1bf..a9767e7a 100644 --- a/my-app/src/views/ListView.jsx +++ b/my-app/src/views/ListView.jsx @@ -9,7 +9,7 @@ function ListView(props) { const [hasMore, setHasMore] = useState(true); const [readMore, setReadMore] = useState({}); const [isLoading, setIsLoading] = useState(true); - + const toggleReadMore = (courseCode) => { setReadMore(prevState => ({ ...prevState, @@ -35,25 +35,27 @@ function ListView(props) { const fetchMoreCourses = useCallback(() => { if (!hasMore) return; - const nextItems = coursesToDisplay.slice(displayedCourses.length, displayedCourses.length + 10); setDisplayedCourses(prevCourses => [...prevCourses, ...nextItems]); setHasMore(displayedCourses.length + nextItems.length < coursesToDisplay.length); }, [displayedCourses.length, coursesToDisplay, hasMore]); useEffect(() => { - if (!props.targetScroll) return; // nothing to restore - - const currentHeight = document.documentElement.scrollHeight; - if (currentHeight < props.targetScroll && hasMore) { - // Load more courses, then try again. - fetchMoreCourses(); - } else { - // Once enough content has loaded, scroll to the target. - window.scrollTo(0, props.targetScroll); - } - }, [displayedCourses, hasMore, props.targetScroll, fetchMoreCourses]); - + const container = props.scrollContainerRef.current; + if (!container || !props.targetScroll) return; + + const attemptScroll = () => { + if (container.scrollHeight >= props.targetScroll) { + container.scrollTop = props.targetScroll; + } else if (hasMore) { + fetchMoreCourses(); + setTimeout(attemptScroll, 100); + } + }; + + attemptScroll(); + }, [props.targetScroll, hasMore, displayedCourses.length]); + return (
{isLoading ? ( @@ -61,7 +63,7 @@ function ListView(props) {
) : ( -
+
Date: Fri, 11 Apr 2025 15:10:20 +0200 Subject: [PATCH 07/16] Scrolling is Persistant! --- my-app/src/presenters/ListViewPresenter.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/my-app/src/presenters/ListViewPresenter.jsx b/my-app/src/presenters/ListViewPresenter.jsx index 3325ca8f..ab9a97a5 100644 --- a/my-app/src/presenters/ListViewPresenter.jsx +++ b/my-app/src/presenters/ListViewPresenter.jsx @@ -23,7 +23,7 @@ const ListViewPresenter = observer(({ model }) => { useEffect(() => { const cleanup = syncScrollPositionToFirebase(model, scrollContainerRef); - return () => cleanup(); + return () => cleanup; }, [model.user, scrollContainerRef]); const addFavourite = (course) => { From 46240edf47134e5c43a3ab28a0d2655ae4f01de8 Mon Sep 17 00:00:00 2001 From: Justus Kluge Date: Wed, 16 Apr 2025 13:18:18 +0200 Subject: [PATCH 08/16] Scroll persistance with going back to the top on search finished --- my-app/firebase.js | 34 ++++++++--------- my-app/src/index.jsx | 1 - my-app/src/model.js | 2 +- my-app/src/presenters/ListViewPresenter.jsx | 40 ++++++++++++++++++-- my-app/src/presenters/SearchbarPresenter.jsx | 7 +++- my-app/src/views/ListView.jsx | 22 ++++------- my-app/src/views/SearchbarView.jsx | 1 + 7 files changed, 69 insertions(+), 38 deletions(-) diff --git a/my-app/firebase.js b/my-app/firebase.js index efd9e7a3..e83a838c 100644 --- a/my-app/firebase.js +++ b/my-app/firebase.js @@ -85,31 +85,29 @@ export function syncModelToFirebase(model) { export function syncScrollPositionToFirebase(model, containerRef) { if (!containerRef?.current) return; - - const throttledSet = throttle((scrollPixel) => { - if (model?.user?.uid) { - const userRef = ref(db, `users/${model.user.uid}/scrollPosition`); - set(userRef, scrollPixel).catch(console.error); - } - }, 500); + let lastSavedPosition = 0; + + // const throttledSet = throttle((scrollPixel) => { + // if (model?.user?.uid) { + // const userRef = ref(db, `users/${model.user.uid}/scrollPosition`); + // set(userRef, scrollPixel).catch(console.error); + // } + // }, 500); const handleScroll = () => { const scrollTop = containerRef.current.scrollTop; + // make a 100px threshold + if (Math.abs(scrollTop - lastSavedPosition) < 100) + return; + + lastSavedPosition = scrollTop; model.setScrollPosition(scrollTop); - if (!model.user) { - localStorage.setItem("scrollPosition", scrollTop); - } - throttledSet(scrollTop); + localStorage.setItem("scrollPosition", scrollTop); + // throttledSet(scrollTop); }; containerRef.current.addEventListener('scroll', handleScroll); - - // Return cleanup function - return () => { - if (containerRef.current) { - containerRef.current.removeEventListener('scroll', handleScroll); - } - }; + return () => containerRef.current?.removeEventListener('scroll', handleScroll); } diff --git a/my-app/src/index.jsx b/my-app/src/index.jsx index 5bfb1434..a9377d8a 100644 --- a/my-app/src/index.jsx +++ b/my-app/src/index.jsx @@ -15,7 +15,6 @@ configure({ enforceActions: "never" }); const reactiveModel = makeAutoObservable(model); connectToFirebase(reactiveModel); -// ✅ Add /share route here export function makeRouter(reactiveModel) { return createHashRouter([ { diff --git a/my-app/src/model.js b/my-app/src/model.js index 062f69c6..5e8419de 100644 --- a/my-app/src/model.js +++ b/my-app/src/model.js @@ -23,7 +23,7 @@ export const model = { }, setScrollPosition(position) { - this.scrollPosition = position; // This method updates the scroll position + this.scrollPosition = position; }, setCourses(courses){ diff --git a/my-app/src/presenters/ListViewPresenter.jsx b/my-app/src/presenters/ListViewPresenter.jsx index ab9a97a5..deb5a258 100644 --- a/my-app/src/presenters/ListViewPresenter.jsx +++ b/my-app/src/presenters/ListViewPresenter.jsx @@ -8,8 +8,38 @@ import {ReviewPresenter} from "../presenters/ReviewPresenter.jsx" import {syncScrollPositionToFirebase} from "../../firebase.js" const ListViewPresenter = observer(({ model }) => { - const scrollContainerRef = useRef(null); + let attempts = 0; + const MAX_Depth = 49; + + function persistantScrolling(fetchMoreCourses, hasMore){ + const container = scrollContainerRef.current; + if (!container || !model.scrollPosition) return; + + + + const attemptScroll = () => { + + // refresh on significant change (same as in firebase) + if (Math.abs(container.scrollTop - model.scrollPosition) < 100) + return; + + attempts++; + if (attempts > MAX_Depth) { + return; + } + const needsMoreCourses = container.scrollHeight < model.scrollPosition && hasMore; + + if (needsMoreCourses) { + fetchMoreCourses(); + setTimeout(attemptScroll, 100); // Add delay between attempts + } else { + container.scrollTop = model.scrollPosition; + syncScrollPositionToFirebase(model, scrollContainerRef) + } + }; + attemptScroll(); + } useEffect(() => { // Load initial scroll position @@ -24,7 +54,7 @@ const ListViewPresenter = observer(({ model }) => { useEffect(() => { const cleanup = syncScrollPositionToFirebase(model, scrollContainerRef); return () => cleanup; - }, [model.user, scrollContainerRef]); + }, [model.user, model.currentSearch, scrollContainerRef]); const addFavourite = (course) => { model.addFavourite(course); @@ -60,16 +90,20 @@ const ListViewPresenter = observer(({ model }) => { return ; }); diff --git a/my-app/src/presenters/SearchbarPresenter.jsx b/my-app/src/presenters/SearchbarPresenter.jsx index f0858f9a..b0dc5506 100644 --- a/my-app/src/presenters/SearchbarPresenter.jsx +++ b/my-app/src/presenters/SearchbarPresenter.jsx @@ -33,6 +33,10 @@ const SearchbarPresenter = observer(({ model }) => { } }; + function resetScoll(){ + model.setScrollPosition(0.01); + } + const creditsSum = (favouriteCourses) => { return favouriteCourses.reduce((sum, course) => sum + parseFloat(course.credits), 0); }; @@ -57,10 +61,10 @@ const SearchbarPresenter = observer(({ model }) => { reviewPresenter={reviewPresenter} prerequisiteTree={preP} />; + return ( { popup={popup} handleFavouriteClick={handleFavouriteClick} totalCredits={creditsSum(model.favourites)} + resetScrollPosition={resetScoll} /> ); }); diff --git a/my-app/src/views/ListView.jsx b/my-app/src/views/ListView.jsx index 4b615876..7b0e5e72 100644 --- a/my-app/src/views/ListView.jsx +++ b/my-app/src/views/ListView.jsx @@ -35,25 +35,18 @@ function ListView(props) { const fetchMoreCourses = useCallback(() => { if (!hasMore) return; - const nextItems = coursesToDisplay.slice(displayedCourses.length, displayedCourses.length + 10); + const nextItems = coursesToDisplay.slice(displayedCourses.length, displayedCourses.length + 50); setDisplayedCourses(prevCourses => [...prevCourses, ...nextItems]); setHasMore(displayedCourses.length + nextItems.length < coursesToDisplay.length); }, [displayedCourses.length, coursesToDisplay, hasMore]); + const [isRestoringScroll, setIsRestoringScroll] = useState(false); useEffect(() => { - const container = props.scrollContainerRef.current; - if (!container || !props.targetScroll) return; - - const attemptScroll = () => { - if (container.scrollHeight >= props.targetScroll) { - container.scrollTop = props.targetScroll; - } else if (hasMore) { - fetchMoreCourses(); - setTimeout(attemptScroll, 100); - } - }; - - attemptScroll(); + if (props.targetScroll > 0 && !isRestoringScroll) { + setIsRestoringScroll(true); + props.persistantScrolling(fetchMoreCourses, hasMore); + setIsRestoringScroll(false); + } }, [props.targetScroll, hasMore, displayedCourses.length]); return ( @@ -76,6 +69,7 @@ function ListView(props) { endMessage={

No more courses

} scrollThreshold={0.9} // 90% of the container height scrollableTarget="scrollableDiv" + initialScrollY={0} > {displayedCourses.map(course => (
{ + props.resetScrollPosition(); setSearchQuery(query); props.searchCourses(query); }; From c128036615e414ba13c93567d0174089c1a8f1db Mon Sep 17 00:00:00 2001 From: Justus Kluge Date: Wed, 16 Apr 2025 14:58:04 +0200 Subject: [PATCH 09/16] Make filters persistant --- my-app/firebase.js | 13 +++++++------ my-app/src/model.js | 6 +++++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/my-app/firebase.js b/my-app/firebase.js index 9aed74b3..49e7bdd1 100644 --- a/my-app/firebase.js +++ b/my-app/firebase.js @@ -50,10 +50,10 @@ async function firebaseToModel(model) { if (data.favourites) model.setFavourite(data.favourites); if (data.currentSearchText) model.setCurrentSearchText(data.currentSearchText); - if (data.scrollPosition) - model.setScrollPosition(data.scrollPosition); - // if (data.currentSearch) - // model.setCurrentSearch(data.currentSearch); + // if (data.scrollPosition) + // model.setScrollPosition(data.scrollPosition); + if (data.filterOptions) + model.setFilterOptions(data.filterOptions); noUpload = false; }); } @@ -64,16 +64,17 @@ export function syncModelToFirebase(model) { userId: model?.user.uid, favourites: toJS(model.favourites), currentSearchText: toJS(model.currentSearchText), - // currentSearch: toJS(model.currentSearch), + filterOptions: toJS(model.filterOptions), // Add more per-user attributes here }), // eslint-disable-next-line no-unused-vars - ({ userId, favourites, currentSearchText }) => { + ({ userId, favourites, currentSearchText, filterOptions }) => { if (noUpload || !userId) return; const userRef = ref(db, `users/${userId}`); const dataToSync = { favourites, currentSearchText, + filterOptions, }; set(userRef, dataToSync) diff --git a/my-app/src/model.js b/my-app/src/model.js index 533fa15b..1a2b77a3 100644 --- a/my-app/src/model.js +++ b/my-app/src/model.js @@ -11,7 +11,7 @@ export const model = { courses: [], favourites: [], isReady: false, - filtersChange: false, + filtersChange: false, // filtersCalculated: false, filteredCourses: [], filterOptions: { @@ -124,6 +124,10 @@ export const model = { setFiltersCalculated() { this.filtersCalculated = true; }, + + setFilterOptions(options){ + this.filterOptions = options; // do we want to set the flags? What about useEffect? + }, updateLevelFilter(level) { this.filterOptions.level = level; From 38ff3dae8ebf46bee50558829a2888e692d9339d Mon Sep 17 00:00:00 2001 From: Justus Kluge Date: Wed, 16 Apr 2025 15:23:19 +0200 Subject: [PATCH 10/16] Store and restore from local storage --- my-app/firebase.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/my-app/firebase.js b/my-app/firebase.js index 49e7bdd1..6e6601c4 100644 --- a/my-app/firebase.js +++ b/my-app/firebase.js @@ -41,7 +41,13 @@ export function connectToFirebase(model) { // fetches all relevant information to create the model async function firebaseToModel(model) { - if (!model.user) return; + if (!model.user){ + const options = localStorage.getItem("filterOptions"); + if(options){ + model.setFilterOptions(options); + console.log("Restore options from local storage") + } + return} const userRef = ref(db, `users/${model.user.uid}`); onValue(userRef, (snapshot) => { if (!snapshot.exists()) return; @@ -80,6 +86,9 @@ export function syncModelToFirebase(model) { set(userRef, dataToSync) .then(() => console.log("User model synced to Firebase")) .catch(console.error); + + // also save to local storage + localStorage.setItem("filterOptions",filterOptions); } ); } From 723133b0d383f64ab74bf9895fc5d21e85c8c3b7 Mon Sep 17 00:00:00 2001 From: Justus Kluge Date: Thu, 17 Apr 2025 08:39:08 +0200 Subject: [PATCH 11/16] Do caching via IndexedDB --- my-app/firebase.js | 152 ++++++++++++++++++++++++++++++--------------- 1 file changed, 102 insertions(+), 50 deletions(-) diff --git a/my-app/firebase.js b/my-app/firebase.js index 6e6601c4..dd24891c 100644 --- a/my-app/firebase.js +++ b/my-app/firebase.js @@ -41,13 +41,14 @@ export function connectToFirebase(model) { // fetches all relevant information to create the model async function firebaseToModel(model) { - if (!model.user){ + if (!model.user) { const options = localStorage.getItem("filterOptions"); - if(options){ + if (options) { model.setFilterOptions(options); - console.log("Restore options from local storage") + console.log("Restore options from local storage"); } - return} + return; + } const userRef = ref(db, `users/${model.user.uid}`); onValue(userRef, (snapshot) => { if (!snapshot.exists()) return; @@ -56,10 +57,9 @@ async function firebaseToModel(model) { if (data.favourites) model.setFavourite(data.favourites); if (data.currentSearchText) model.setCurrentSearchText(data.currentSearchText); - // if (data.scrollPosition) + // if (data.scrollPosition) // model.setScrollPosition(data.scrollPosition); - if (data.filterOptions) - model.setFilterOptions(data.filterOptions); + if (data.filterOptions) model.setFilterOptions(data.filterOptions); noUpload = false; }); } @@ -88,7 +88,7 @@ export function syncModelToFirebase(model) { .catch(console.error); // also save to local storage - localStorage.setItem("filterOptions",filterOptions); + localStorage.setItem("filterOptions", filterOptions); } ); } @@ -96,40 +96,60 @@ export function syncModelToFirebase(model) { export function syncScrollPositionToFirebase(model, containerRef) { if (!containerRef?.current) return; let lastSavedPosition = 0; - - // const throttledSet = throttle((scrollPixel) => { - // if (model?.user?.uid) { - // const userRef = ref(db, `users/${model.user.uid}/scrollPosition`); - // set(userRef, scrollPixel).catch(console.error); - // } - // }, 500); - - const handleScroll = () => { - const scrollTop = containerRef.current.scrollTop; + + // const throttledSet = throttle((scrollPixel) => { + // if (model?.user?.uid) { + // const userRef = ref(db, `users/${model.user.uid}/scrollPosition`); + // set(userRef, scrollPixel).catch(console.error); + // } + // }, 500); + + const handleScroll = () => { + const scrollTop = containerRef.current.scrollTop; // make a 100px threshold - if (Math.abs(scrollTop - lastSavedPosition) < 100) - return; + if (Math.abs(scrollTop - lastSavedPosition) < 100) return; lastSavedPosition = scrollTop; - model.setScrollPosition(scrollTop); - localStorage.setItem("scrollPosition", scrollTop); - // throttledSet(scrollTop); - }; + model.setScrollPosition(scrollTop); + localStorage.setItem("scrollPosition", scrollTop); + // throttledSet(scrollTop); + }; - containerRef.current.addEventListener('scroll', handleScroll); - return () => containerRef.current?.removeEventListener('scroll', handleScroll); + containerRef.current.addEventListener("scroll", handleScroll); + return () => + containerRef.current?.removeEventListener("scroll", handleScroll); } +function saveCoursesToCache(courses, timestamp) { + const request = indexedDB.open("CourseDB", 1); + + request.onupgradeneeded = (event) => { + const db = event.target.result; + if (!db.objectStoreNames.contains("courses")) { + db.createObjectStore("courses", { keyPath: "id" }); + } + if (!db.objectStoreNames.contains("metadata")) { + db.createObjectStore("metadata", { keyPath: "key" }); + } + }; -function saveCoursesInChunks(courses, timestamp) { - const parts = 3; // Adjust this based on course size - const chunkSize = Math.ceil(courses.length / parts); + request.onsuccess = (event) => { + const db = event.target.result; + const tx = db.transaction(["courses", "metadata"], "readwrite"); + const courseStore = tx.objectStore("courses"); + const metaStore = tx.objectStore("metadata"); - 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 })); + courseStore.clear(); + courses.forEach((course) => courseStore.put(course)); + metaStore.put({ key: "timestamp", value: timestamp }); + + tx.oncomplete = () => console.log("Saved courses to IndexedDB"); + tx.onerror = (e) => console.error("IndexedDB save error", e); + }; + + request.onerror = (e) => { + console.error("Failed to open IndexedDB", e); + }; } async function updateLastUpdatedTimestamp() { @@ -165,28 +185,60 @@ export async function fetchAllCourses() { } 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); + + const dbPromise = new Promise((resolve, reject) => { + const request = indexedDB.open("CourseDB", 1); + // check if courses and metadata dirs exist + request.onupgradeneeded = (event) => { + const db = event.target.result; + if (!db.objectStoreNames.contains("courses")) { + db.createObjectStore("courses", { keyPath: "id" }); + } + if (!db.objectStoreNames.contains("metadata")) { + db.createObjectStore("metadata", { keyPath: "key" }); + } + }; + + request.onsuccess = (event) => resolve(event.target.result); + request.onerror = (e) => reject(e); + }); + + try { + const db = await dbPromise; + const metaTx = db.transaction("metadata", "readonly"); + const metaStore = metaTx.objectStore("metadata"); + const metaReq = metaStore.get("timestamp"); + const cachedTimestamp = await new Promise((resolve) => { + metaReq.onsuccess = () => resolve(metaReq.result?.value ?? 0); + metaReq.onerror = () => resolve(0); + }); + + if (cachedTimestamp === firebaseTimestamp) { + console.log("Using cached courses from IndexedDB..."); + const courseTx = db.transaction("courses", "readonly"); + const courseStore = courseTx.objectStore("courses"); + const getAllReq = courseStore.getAll(); + const cachedCourses = await new Promise((resolve) => { + getAllReq.onsuccess = () => resolve(getAllReq.result); + getAllReq.onerror = () => resolve([]); + }); + model.setCourses(cachedCourses); + return; } - model.setCourses(mergedCourses); - return; + } catch (err) { + console.warn("IndexedDB unavailable, falling back to Firebase:", err); } - // Fetch if outdated or missing + // fallback: fetch from Firebase console.log("Fetching courses from Firebase..."); const courses = await fetchAllCourses(); model.setCourses(courses); - saveCoursesInChunks(courses, firebaseTimestamp); + saveCoursesToCache(courses, firebaseTimestamp); } + export async function saveJSONCoursesToFirebase(model, data) { if (!data || !model) { console.log("no model or data"); @@ -226,11 +278,11 @@ export async function getReviewsForCourse(courseCode) { const snapshot = await get(reviewsRef); if (!snapshot.exists()) return []; const reviews = []; - snapshot.forEach(childSnapshot => { + snapshot.forEach((childSnapshot) => { reviews.push({ id: childSnapshot.key, - ...childSnapshot.val() + ...childSnapshot.val(), }); }); return reviews; -} \ No newline at end of file +} From 709959067e75063bd1a1b025387ad74e95744ce3 Mon Sep 17 00:00:00 2001 From: Justus Kluge Date: Thu, 17 Apr 2025 09:06:07 +0200 Subject: [PATCH 12/16] Filter persistance --- my-app/firebase.js | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/my-app/firebase.js b/my-app/firebase.js index dd24891c..8e63b30c 100644 --- a/my-app/firebase.js +++ b/my-app/firebase.js @@ -27,6 +27,16 @@ let noUpload = false; export function connectToFirebase(model) { loadCoursesFromCacheOrFirebase(model); + + // setting missing + // also save filters to local storage + //localStorage.setItem("filterOptions", filterOptions); + const options = localStorage.getItem("filterOptions"); + if (options) { + model.setFilterOptions(options); + console.log("Restore options from local storage"); + } + onAuthStateChanged(auth, (user) => { if (user) { model.setUser(user); // Set the user ID once authenticated @@ -41,14 +51,6 @@ export function connectToFirebase(model) { // fetches all relevant information to create the model async function firebaseToModel(model) { - if (!model.user) { - const options = localStorage.getItem("filterOptions"); - if (options) { - model.setFilterOptions(options); - console.log("Restore options from local storage"); - } - return; - } const userRef = ref(db, `users/${model.user.uid}`); onValue(userRef, (snapshot) => { if (!snapshot.exists()) return; @@ -86,9 +88,6 @@ export function syncModelToFirebase(model) { set(userRef, dataToSync) .then(() => console.log("User model synced to Firebase")) .catch(console.error); - - // also save to local storage - localStorage.setItem("filterOptions", filterOptions); } ); } @@ -185,9 +184,7 @@ export async function fetchAllCourses() { } async function loadCoursesFromCacheOrFirebase(model) { - const firebaseTimestamp = await fetchLastUpdatedTimestamp(); - const dbPromise = new Promise((resolve, reject) => { const request = indexedDB.open("CourseDB", 1); // check if courses and metadata dirs exist @@ -236,6 +233,7 @@ async function loadCoursesFromCacheOrFirebase(model) { const courses = await fetchAllCourses(); model.setCourses(courses); saveCoursesToCache(courses, firebaseTimestamp); + } From 64213aada14ea431b10258dc10d616bc753cadeb Mon Sep 17 00:00:00 2001 From: Justus Kluge Date: Thu, 17 Apr 2025 11:29:26 +0200 Subject: [PATCH 13/16] Filters are persistant - they just don't update bc Deo needs to fix the UI... --- my-app/firebase.js | 26 +++++++++++++++--------- my-app/src/model.js | 49 +++++++++++++-------------------------------- 2 files changed, 31 insertions(+), 44 deletions(-) diff --git a/my-app/firebase.js b/my-app/firebase.js index 8e63b30c..7327d6af 100644 --- a/my-app/firebase.js +++ b/my-app/firebase.js @@ -30,12 +30,20 @@ export function connectToFirebase(model) { // setting missing // also save filters to local storage - //localStorage.setItem("filterOptions", filterOptions); - const options = localStorage.getItem("filterOptions"); - if (options) { - model.setFilterOptions(options); - console.log("Restore options from local storage"); - } + // + const options = JSON.parse(localStorage.getItem("filterOptions")); + if (options) { + model.setFilterOptions(options); + console.log("Restore options from local storage"); + } + + reaction( + () => ({filterOptions: JSON.stringify(model.filterOptions)}), + // eslint-disable-next-line no-unused-vars + ({filterOptions}) => { + localStorage.setItem("filterOptions", filterOptions); + } + ); onAuthStateChanged(auth, (user) => { if (user) { @@ -61,7 +69,7 @@ async function firebaseToModel(model) { model.setCurrentSearchText(data.currentSearchText); // if (data.scrollPosition) // model.setScrollPosition(data.scrollPosition); - if (data.filterOptions) model.setFilterOptions(data.filterOptions); + // if (data.filterOptions) model.setFilterOptions(data.filterOptions); noUpload = false; }); } @@ -72,7 +80,7 @@ export function syncModelToFirebase(model) { userId: model?.user.uid, favourites: toJS(model.favourites), currentSearchText: toJS(model.currentSearchText), - filterOptions: toJS(model.filterOptions), + // filterOptions: toJS(model.filterOptions), // Add more per-user attributes here }), // eslint-disable-next-line no-unused-vars @@ -82,7 +90,7 @@ export function syncModelToFirebase(model) { const dataToSync = { favourites, currentSearchText, - filterOptions, + // filterOptions, }; set(userRef, dataToSync) diff --git a/my-app/src/model.js b/my-app/src/model.js index 410225ce..1a2b77a3 100644 --- a/my-app/src/model.js +++ b/my-app/src/model.js @@ -18,7 +18,7 @@ export const model = { applyTranscriptFilter: true, eligibility: "weak", //the possible values for the string are: "weak"/"moderate"/"strong" applyLevelFilter: true, - level: ["PREPARATORY", "BASIC", "ADVANCED", "RESEARCH"], //the possible values for the array are: "PREPARATORY", "BASIC", "ADVANCED", "RESEARCH" + level: [], //the possible values for the array are: "PREPARATORY", "BASIC", "ADVANCED", "RESEARCH" applyLanguageFilter: true, language: "none", //the possible values for the string are: "none"/"english"/"swedish"/"both" applyLocationFilter:true, @@ -26,12 +26,8 @@ export const model = { applyCreditsFilter:true, creditMin: 0, creditMax: 45, - applyDepartmentFilter: true, - department: ["EECS/Computational Science and Technology", "EECS/Theoretical Computer Science", "EECS/Electric Power and Energy Systems", "EECS/Network and Systems Engineering", - "ITM/Learning in Engineering Sciences", "ITM/Industrial Economics and Management", "ITM/Energy Systems", "ITM/Integrated Product Development and Design", "ITM/SKD GRU", - "SCI/Mathematics", "SCI/Applied Physics", "SCI/Mechanics", "SCI/Aeronautical and Vehicle Engineering", - "ABE/Sustainability and Environmental Engineering", "ABE/Concrete Structures", "ABE/Structural Design & Bridges", "ABE/History of Science, Technology and Environment", ], - applyRemoveNullCourses: false + applyDepartmentFilter:false, + department: [] }, setUser(user) { @@ -88,17 +84,15 @@ export const model = { entries.forEach(entry => { const course = { code: entry[1].code, - name: entry[1]?.name ?? "null", - location: entry[1]?.location ?? "null", - department: entry[1]?.department ?? "null", - language: entry[1]?.language ?? "null", - description: entry[1]?.description ?? "null", - academicLevel: entry[1]?.academic_level ?? "null", - period: entry[1]?.period ?? "null", + 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, - prerequisites: entry[1]?.prerequisites ?? "null", - prerequisites_text: entry[1]?.prerequisites_text ?? "null", - learning_outcomes: entry[1]?.learning_outcomes ?? "null" + prerequisites: entry[1]?.prerequisites ?? "", }; this.addCourse(course); }); @@ -135,11 +129,6 @@ export const model = { this.filterOptions = options; // do we want to set the flags? What about useEffect? }, - setApplyRemoveNullCourses() { - this.filterOptions.applyRemoveNullCourses = !this.filterOptions.applyRemoveNullCourses; - this.setFiltersChange(); - }, - updateLevelFilter(level) { this.filterOptions.level = level; }, @@ -157,10 +146,6 @@ export const model = { this.filterOptions.eligibility = eligibility; }, - updateDepartmentFilter(department) { - this.filterOptions.department = department; - }, - //setters for the filter options setApplyTranscriptFilter(transcriptFilterState) { this.filterOptions.applyTranscriptFilter = transcriptFilterState; @@ -177,16 +162,10 @@ export const model = { setApplyCreditsFilter(creditsFilterState) { this.filterOptions.applyCreditsFilter = creditsFilterState; }, - setApplyDepartmentFilter(departmentFilterState) { - this.filterOptions.applyDepartmentFilter = departmentFilterState; - }, + // setApplyDepartmentFilter(departmentFilterState) { + // this.filterOptions.applyDepartmentFilter = departmentFilterState; + // }, - async getAverageRating(courseCode) { - const reviews = await getReviewsForCourse(courseCode); - if (!reviews || reviews.length === 0) return null; - const total = reviews.reduce((sum, review) => sum + (review.overallRating || 0), 0); - return (total / reviews.length).toFixed(1); - }, }; From 2ad4b82e74f5a3049aab981f70519b9f0403b4cd Mon Sep 17 00:00:00 2001 From: Justus Kluge Date: Thu, 17 Apr 2025 11:39:41 +0200 Subject: [PATCH 14/16] fixed dependencies --- my-app/firebase.js | 2 +- my-app/package-lock.json | 557 ++++++++++++++++++++++----------------- my-app/package.json | 5 +- 3 files changed, 323 insertions(+), 241 deletions(-) diff --git a/my-app/firebase.js b/my-app/firebase.js index 7327d6af..c2cb264d 100644 --- a/my-app/firebase.js +++ b/my-app/firebase.js @@ -2,7 +2,7 @@ import { initializeApp } from "firebase/app"; import { getAuth, GoogleAuthProvider, onAuthStateChanged } from "firebase/auth"; import { get, getDatabase, ref, set, onValue, push } from "firebase/database"; import { reaction, toJS } from "mobx"; -import throttle from "lodash.throttle"; +// import throttle from "lodash.throttle"; // Your web app's Firebase configuration const firebaseConfig = { diff --git a/my-app/package-lock.json b/my-app/package-lock.json index 1b4bd221..2f16fd62 100644 --- a/my-app/package-lock.json +++ b/my-app/package-lock.json @@ -11,7 +11,6 @@ "@dagrejs/dagre": "^1.1.4", "@headlessui/react": "^2.2.1", "@heroicons/react": "^2.2.0", - "@react-buddy/ide-toolbox": "^2.4.0", "@tailwindcss/vite": "^4.0.17", "@xyflow/react": "^12.5.5", "autoprefixer": "^10.4.21", @@ -21,8 +20,8 @@ "mobx": "^6.13.7", "mobx-react-lite": "^4.1.0", "pdfjs-dist": "^5.1.91", - "react": "^19.0.0", - "react-dom": "^19.0.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", "react-infinite-scroll-component": "^6.1.0", "react-router-dom": "^7.4.0", "reactflow": "^11.11.4", @@ -756,9 +755,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz", - "integrity": "sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.1.tgz", + "integrity": "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==", "dev": true, "license": "MIT", "dependencies": { @@ -2005,14 +2004,14 @@ "license": "BSD-3-Clause" }, "node_modules/@react-aria/focus": { - "version": "3.20.1", - "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.20.1.tgz", - "integrity": "sha512-lgYs+sQ1TtBrAXnAdRBQrBo0/7o5H6IrfDxec1j+VRpcXL0xyk0xPq+m3lZp8typzIghqDgpnKkJ5Jf4OrzPIw==", + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.20.2.tgz", + "integrity": "sha512-Q3rouk/rzoF/3TuH6FzoAIKrl+kzZi9LHmr8S5EqLAOyP9TXIKG34x2j42dZsAhrw7TbF9gA8tBKwnCNH4ZV+Q==", "license": "Apache-2.0", "dependencies": { - "@react-aria/interactions": "^3.24.1", - "@react-aria/utils": "^3.28.1", - "@react-types/shared": "^3.28.0", + "@react-aria/interactions": "^3.25.0", + "@react-aria/utils": "^3.28.2", + "@react-types/shared": "^3.29.0", "@swc/helpers": "^0.5.0", "clsx": "^2.0.0" }, @@ -2022,15 +2021,15 @@ } }, "node_modules/@react-aria/interactions": { - "version": "3.24.1", - "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.24.1.tgz", - "integrity": "sha512-OWEcIC6UQfWq4Td5Ptuh4PZQ4LHLJr/JL2jGYvuNL6EgL3bWvzPrRYIF/R64YbfVxIC7FeZpPSkS07sZ93/NoA==", + "version": "3.25.0", + "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.0.tgz", + "integrity": "sha512-GgIsDLlO8rDU/nFn6DfsbP9rfnzhm8QFjZkB9K9+r+MTSCn7bMntiWQgMM+5O6BiA8d7C7x4zuN4bZtc0RBdXQ==", "license": "Apache-2.0", "dependencies": { - "@react-aria/ssr": "^3.9.7", - "@react-aria/utils": "^3.28.1", - "@react-stately/flags": "^3.1.0", - "@react-types/shared": "^3.28.0", + "@react-aria/ssr": "^3.9.8", + "@react-aria/utils": "^3.28.2", + "@react-stately/flags": "^3.1.1", + "@react-types/shared": "^3.29.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { @@ -2039,9 +2038,9 @@ } }, "node_modules/@react-aria/ssr": { - "version": "3.9.7", - "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.7.tgz", - "integrity": "sha512-GQygZaGlmYjmYM+tiNBA5C6acmiDWF52Nqd40bBp0Znk4M4hP+LTmI0lpI1BuKMw45T8RIhrAsICIfKwZvi2Gg==", + "version": "3.9.8", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.8.tgz", + "integrity": "sha512-lQDE/c9uTfBSDOjaZUJS8xP2jCKVk4zjQeIlCH90xaLhHDgbpCdns3xvFpJJujfj3nI4Ll9K7A+ONUBDCASOuw==", "license": "Apache-2.0", "dependencies": { "@swc/helpers": "^0.5.0" @@ -2054,15 +2053,15 @@ } }, "node_modules/@react-aria/utils": { - "version": "3.28.1", - "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.28.1.tgz", - "integrity": "sha512-mnHFF4YOVu9BRFQ1SZSKfPhg3z+lBRYoW5mLcYTQihbKhz48+I1sqRkP7ahMITr8ANH3nb34YaMME4XWmK2Mgg==", + "version": "3.28.2", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.28.2.tgz", + "integrity": "sha512-J8CcLbvnQgiBn54eeEvQQbIOfBF3A1QizxMw9P4cl9MkeR03ug7RnjTIdJY/n2p7t59kLeAB3tqiczhcj+Oi5w==", "license": "Apache-2.0", "dependencies": { - "@react-aria/ssr": "^3.9.7", - "@react-stately/flags": "^3.1.0", - "@react-stately/utils": "^3.10.5", - "@react-types/shared": "^3.28.0", + "@react-aria/ssr": "^3.9.8", + "@react-stately/flags": "^3.1.1", + "@react-stately/utils": "^3.10.6", + "@react-types/shared": "^3.29.0", "@swc/helpers": "^0.5.0", "clsx": "^2.0.0" }, @@ -2071,28 +2070,19 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-buddy/ide-toolbox": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@react-buddy/ide-toolbox/-/ide-toolbox-2.4.0.tgz", - "integrity": "sha512-TWHX6gwa0Gop7215uHhjFMbYLLdjM/b9rr0wYE3E0m7GNJ56gbPpbZiq86w9uI8zksl827acqGeT437MkuO64w==", - "license": "Apache-2.0", - "peerDependencies": { - "react": "^17.0.0 || ^18.0.0" - } - }, "node_modules/@react-stately/flags": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.0.tgz", - "integrity": "sha512-KSHOCxTFpBtxhIRcKwsD1YDTaNxFtCYuAUb0KEihc16QwqZViq4hasgPBs2gYm7fHRbw7WYzWKf6ZSo/+YsFlg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.1.tgz", + "integrity": "sha512-XPR5gi5LfrPdhxZzdIlJDz/B5cBf63l4q6/AzNqVWFKgd0QqY5LvWJftXkklaIUpKSJkIKQb8dphuZXDtkWNqg==", "license": "Apache-2.0", "dependencies": { "@swc/helpers": "^0.5.0" } }, "node_modules/@react-stately/utils": { - "version": "3.10.5", - "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.5.tgz", - "integrity": "sha512-iMQSGcpaecghDIh3mZEpZfoFH3ExBwTtuBEcvZ2XnGzCgQjeYXcMdIUwAfVQLXFTdHUHGF6Gu6/dFrYsCzySBQ==", + "version": "3.10.6", + "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.6.tgz", + "integrity": "sha512-O76ip4InfTTzAJrg8OaZxKU4vvjMDOpfA/PGNOytiXwBbkct2ZeZwaimJ8Bt9W1bj5VsZ81/o/tW4BacbdDOMA==", "license": "Apache-2.0", "dependencies": { "@swc/helpers": "^0.5.0" @@ -2102,9 +2092,9 @@ } }, "node_modules/@react-types/shared": { - "version": "3.28.0", - "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.28.0.tgz", - "integrity": "sha512-9oMEYIDc3sk0G5rysnYvdNrkSg7B04yTKl50HHSZVbokeHpnU0yRmsDaWb9B/5RprcKj8XszEk5guBO8Sa/Q+Q==", + "version": "3.29.0", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.29.0.tgz", + "integrity": "sha512-IDQYu/AHgZimObzCFdNl1LpZvQW/xcfLt3v20sorl5qRucDVj4S9os98sVTZ4IRIBjmS+MkjqpR5E70xan7ooA==", "license": "Apache-2.0", "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" @@ -2213,9 +2203,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.39.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.39.0.tgz", - "integrity": "sha512-lGVys55Qb00Wvh8DMAocp5kIcaNzEFTmGhfFd88LfaogYTRKrdxgtlO5H6S49v2Nd8R2C6wLOal0qv6/kCkOwA==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz", + "integrity": "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==", "cpu": [ "arm" ], @@ -2226,9 +2216,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.39.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.39.0.tgz", - "integrity": "sha512-It9+M1zE31KWfqh/0cJLrrsCPiF72PoJjIChLX+rEcujVRCb4NLQ5QzFkzIZW8Kn8FTbvGQBY5TkKBau3S8cCQ==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.0.tgz", + "integrity": "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==", "cpu": [ "arm64" ], @@ -2239,9 +2229,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.39.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.39.0.tgz", - "integrity": "sha512-lXQnhpFDOKDXiGxsU9/l8UEGGM65comrQuZ+lDcGUx+9YQ9dKpF3rSEGepyeR5AHZ0b5RgiligsBhWZfSSQh8Q==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.0.tgz", + "integrity": "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==", "cpu": [ "arm64" ], @@ -2252,9 +2242,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.39.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.39.0.tgz", - "integrity": "sha512-mKXpNZLvtEbgu6WCkNij7CGycdw9cJi2k9v0noMb++Vab12GZjFgUXD69ilAbBh034Zwn95c2PNSz9xM7KYEAQ==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.0.tgz", + "integrity": "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==", "cpu": [ "x64" ], @@ -2265,9 +2255,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.39.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.39.0.tgz", - "integrity": "sha512-jivRRlh2Lod/KvDZx2zUR+I4iBfHcu2V/BA2vasUtdtTN2Uk3jfcZczLa81ESHZHPHy4ih3T/W5rPFZ/hX7RtQ==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.0.tgz", + "integrity": "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==", "cpu": [ "arm64" ], @@ -2278,9 +2268,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.39.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.39.0.tgz", - "integrity": "sha512-8RXIWvYIRK9nO+bhVz8DwLBepcptw633gv/QT4015CpJ0Ht8punmoHU/DuEd3iw9Hr8UwUV+t+VNNuZIWYeY7Q==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.0.tgz", + "integrity": "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==", "cpu": [ "x64" ], @@ -2291,9 +2281,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.39.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.39.0.tgz", - "integrity": "sha512-mz5POx5Zu58f2xAG5RaRRhp3IZDK7zXGk5sdEDj4o96HeaXhlUwmLFzNlc4hCQi5sGdR12VDgEUqVSHer0lI9g==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.0.tgz", + "integrity": "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==", "cpu": [ "arm" ], @@ -2304,9 +2294,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.39.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.39.0.tgz", - "integrity": "sha512-+YDwhM6gUAyakl0CD+bMFpdmwIoRDzZYaTWV3SDRBGkMU/VpIBYXXEvkEcTagw/7VVkL2vA29zU4UVy1mP0/Yw==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.0.tgz", + "integrity": "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==", "cpu": [ "arm" ], @@ -2317,9 +2307,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.39.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.39.0.tgz", - "integrity": "sha512-EKf7iF7aK36eEChvlgxGnk7pdJfzfQbNvGV/+l98iiMwU23MwvmV0Ty3pJ0p5WQfm3JRHOytSIqD9LB7Bq7xdQ==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.0.tgz", + "integrity": "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==", "cpu": [ "arm64" ], @@ -2330,9 +2320,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.39.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.39.0.tgz", - "integrity": "sha512-vYanR6MtqC7Z2SNr8gzVnzUul09Wi1kZqJaek3KcIlI/wq5Xtq4ZPIZ0Mr/st/sv/NnaPwy/D4yXg5x0B3aUUA==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.0.tgz", + "integrity": "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==", "cpu": [ "arm64" ], @@ -2343,9 +2333,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.39.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.39.0.tgz", - "integrity": "sha512-NMRUT40+h0FBa5fb+cpxtZoGAggRem16ocVKIv5gDB5uLDgBIwrIsXlGqYbLwW8YyO3WVTk1FkFDjMETYlDqiw==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.0.tgz", + "integrity": "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==", "cpu": [ "loong64" ], @@ -2356,9 +2346,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.39.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.39.0.tgz", - "integrity": "sha512-0pCNnmxgduJ3YRt+D+kJ6Ai/r+TaePu9ZLENl+ZDV/CdVczXl95CbIiwwswu4L+K7uOIGf6tMo2vm8uadRaICQ==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.0.tgz", + "integrity": "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==", "cpu": [ "ppc64" ], @@ -2369,9 +2359,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.39.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.39.0.tgz", - "integrity": "sha512-t7j5Zhr7S4bBtksT73bO6c3Qa2AV/HqiGlj9+KB3gNF5upcVkx+HLgxTm8DK4OkzsOYqbdqbLKwvGMhylJCPhQ==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.0.tgz", + "integrity": "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==", "cpu": [ "riscv64" ], @@ -2382,9 +2372,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.39.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.39.0.tgz", - "integrity": "sha512-m6cwI86IvQ7M93MQ2RF5SP8tUjD39Y7rjb1qjHgYh28uAPVU8+k/xYWvxRO3/tBN2pZkSMa5RjnPuUIbrwVxeA==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.0.tgz", + "integrity": "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==", "cpu": [ "riscv64" ], @@ -2395,9 +2385,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.39.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.39.0.tgz", - "integrity": "sha512-iRDJd2ebMunnk2rsSBYlsptCyuINvxUfGwOUldjv5M4tpa93K8tFMeYGpNk2+Nxl+OBJnBzy2/JCscGeO507kA==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.0.tgz", + "integrity": "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==", "cpu": [ "s390x" ], @@ -2408,9 +2398,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.39.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.39.0.tgz", - "integrity": "sha512-t9jqYw27R6Lx0XKfEFe5vUeEJ5pF3SGIM6gTfONSMb7DuG6z6wfj2yjcoZxHg129veTqU7+wOhY6GX8wmf90dA==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.0.tgz", + "integrity": "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==", "cpu": [ "x64" ], @@ -2421,9 +2411,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.39.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.39.0.tgz", - "integrity": "sha512-ThFdkrFDP55AIsIZDKSBWEt/JcWlCzydbZHinZ0F/r1h83qbGeenCt/G/wG2O0reuENDD2tawfAj2s8VK7Bugg==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.0.tgz", + "integrity": "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==", "cpu": [ "x64" ], @@ -2434,9 +2424,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.39.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.39.0.tgz", - "integrity": "sha512-jDrLm6yUtbOg2TYB3sBF3acUnAwsIksEYjLeHL+TJv9jg+TmTwdyjnDex27jqEMakNKf3RwwPahDIt7QXCSqRQ==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.0.tgz", + "integrity": "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==", "cpu": [ "arm64" ], @@ -2447,9 +2437,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.39.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.39.0.tgz", - "integrity": "sha512-6w9uMuza+LbLCVoNKL5FSLE7yvYkq9laSd09bwS0tMjkwXrmib/4KmoJcrKhLWHvw19mwU+33ndC69T7weNNjQ==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.0.tgz", + "integrity": "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==", "cpu": [ "ia32" ], @@ -2460,9 +2450,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.39.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.39.0.tgz", - "integrity": "sha512-yAkUOkIKZlK5dl7u6dg897doBgLXmUHhIINM2c+sND3DZwnrdQkkSiDh7N75Ll4mM4dxSkYfXqU9fW3lLkMFug==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.0.tgz", + "integrity": "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==", "cpu": [ "x64" ], @@ -2482,43 +2472,44 @@ } }, "node_modules/@tailwindcss/node": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.3.tgz", - "integrity": "sha512-H/6r6IPFJkCfBJZ2dKZiPJ7Ueb2wbL592+9bQEl2r73qbX6yGnmQVIfiUvDRB2YI0a3PWDrzUwkvQx1XW1bNkA==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.4.tgz", + "integrity": "sha512-MT5118zaiO6x6hNA04OWInuAiP1YISXql8Z+/Y8iisV5nuhM8VXlyhRuqc2PEviPszcXI66W44bCIk500Oolhw==", "license": "MIT", "dependencies": { "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.29.2", - "tailwindcss": "4.1.3" + "tailwindcss": "4.1.4" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.3.tgz", - "integrity": "sha512-t16lpHCU7LBxDe/8dCj9ntyNpXaSTAgxWm1u2XQP5NiIu4KGSyrDJJRlK9hJ4U9yJxx0UKCVI67MJWFNll5mOQ==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.4.tgz", + "integrity": "sha512-p5wOpXyOJx7mKh5MXh5oKk+kqcz8T+bA3z/5VWWeQwFrmuBItGwz8Y2CHk/sJ+dNb9B0nYFfn0rj/cKHZyjahQ==", "license": "MIT", "engines": { "node": ">= 10" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.3", - "@tailwindcss/oxide-darwin-arm64": "4.1.3", - "@tailwindcss/oxide-darwin-x64": "4.1.3", - "@tailwindcss/oxide-freebsd-x64": "4.1.3", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.3", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.3", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.3", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.3", - "@tailwindcss/oxide-linux-x64-musl": "4.1.3", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.3", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.3" + "@tailwindcss/oxide-android-arm64": "4.1.4", + "@tailwindcss/oxide-darwin-arm64": "4.1.4", + "@tailwindcss/oxide-darwin-x64": "4.1.4", + "@tailwindcss/oxide-freebsd-x64": "4.1.4", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.4", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.4", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.4", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.4", + "@tailwindcss/oxide-linux-x64-musl": "4.1.4", + "@tailwindcss/oxide-wasm32-wasi": "4.1.4", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.4", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.4" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.3.tgz", - "integrity": "sha512-cxklKjtNLwFl3mDYw4XpEfBY+G8ssSg9ADL4Wm6//5woi3XGqlxFsnV5Zb6v07dxw1NvEX2uoqsxO/zWQsgR+g==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.4.tgz", + "integrity": "sha512-xMMAe/SaCN/vHfQYui3fqaBDEXMu22BVwQ33veLc8ep+DNy7CWN52L+TTG9y1K397w9nkzv+Mw+mZWISiqhmlA==", "cpu": [ "arm64" ], @@ -2532,9 +2523,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.3.tgz", - "integrity": "sha512-mqkf2tLR5VCrjBvuRDwzKNShRu99gCAVMkVsaEOFvv6cCjlEKXRecPu9DEnxp6STk5z+Vlbh1M5zY3nQCXMXhw==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.4.tgz", + "integrity": "sha512-JGRj0SYFuDuAGilWFBlshcexev2hOKfNkoX+0QTksKYq2zgF9VY/vVMq9m8IObYnLna0Xlg+ytCi2FN2rOL0Sg==", "cpu": [ "arm64" ], @@ -2548,9 +2539,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.3.tgz", - "integrity": "sha512-7sGraGaWzXvCLyxrc7d+CCpUN3fYnkkcso3rCzwUmo/LteAl2ZGCDlGvDD8Y/1D3ngxT8KgDj1DSwOnNewKhmg==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.4.tgz", + "integrity": "sha512-sdDeLNvs3cYeWsEJ4H1DvjOzaGios4QbBTNLVLVs0XQ0V95bffT3+scptzYGPMjm7xv4+qMhCDrkHwhnUySEzA==", "cpu": [ "x64" ], @@ -2564,9 +2555,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.3.tgz", - "integrity": "sha512-E2+PbcbzIReaAYZe997wb9rId246yDkCwAakllAWSGqe6VTg9hHle67hfH6ExjpV2LSK/siRzBUs5wVff3RW9w==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.4.tgz", + "integrity": "sha512-VHxAqxqdghM83HslPhRsNhHo91McsxRJaEnShJOMu8mHmEj9Ig7ToHJtDukkuLWLzLboh2XSjq/0zO6wgvykNA==", "cpu": [ "x64" ], @@ -2580,9 +2571,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.3.tgz", - "integrity": "sha512-GvfbJ8wjSSjbLFFE3UYz4Eh8i4L6GiEYqCtA8j2Zd2oXriPuom/Ah/64pg/szWycQpzRnbDiJozoxFU2oJZyfg==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.4.tgz", + "integrity": "sha512-OTU/m/eV4gQKxy9r5acuesqaymyeSCnsx1cFto/I1WhPmi5HDxX1nkzb8KYBiwkHIGg7CTfo/AcGzoXAJBxLfg==", "cpu": [ "arm" ], @@ -2596,9 +2587,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.3.tgz", - "integrity": "sha512-35UkuCWQTeG9BHcBQXndDOrpsnt3Pj9NVIB4CgNiKmpG8GnCNXeMczkUpOoqcOhO6Cc/mM2W7kaQ/MTEENDDXg==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.4.tgz", + "integrity": "sha512-hKlLNvbmUC6z5g/J4H+Zx7f7w15whSVImokLPmP6ff1QqTVE+TxUM9PGuNsjHvkvlHUtGTdDnOvGNSEUiXI1Ww==", "cpu": [ "arm64" ], @@ -2612,9 +2603,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.3.tgz", - "integrity": "sha512-dm18aQiML5QCj9DQo7wMbt1Z2tl3Giht54uVR87a84X8qRtuXxUqnKQkRDK5B4bCOmcZ580lF9YcoMkbDYTXHQ==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.4.tgz", + "integrity": "sha512-X3As2xhtgPTY/m5edUtddmZ8rCruvBvtxYLMw9OsZdH01L2gS2icsHRwxdU0dMItNfVmrBezueXZCHxVeeb7Aw==", "cpu": [ "arm64" ], @@ -2628,9 +2619,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.3.tgz", - "integrity": "sha512-LMdTmGe/NPtGOaOfV2HuO7w07jI3cflPrVq5CXl+2O93DCewADK0uW1ORNAcfu2YxDUS035eY2W38TxrsqngxA==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.4.tgz", + "integrity": "sha512-2VG4DqhGaDSmYIu6C4ua2vSLXnJsb/C9liej7TuSO04NK+JJJgJucDUgmX6sn7Gw3Cs5ZJ9ZLrnI0QRDOjLfNQ==", "cpu": [ "x64" ], @@ -2644,9 +2635,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.3.tgz", - "integrity": "sha512-aalNWwIi54bbFEizwl1/XpmdDrOaCjRFQRgtbv9slWjmNPuJJTIKPHf5/XXDARc9CneW9FkSTqTbyvNecYAEGw==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.4.tgz", + "integrity": "sha512-v+mxVgH2kmur/X5Mdrz9m7TsoVjbdYQT0b4Z+dr+I4RvreCNXyCFELZL/DO0M1RsidZTrm6O1eMnV6zlgEzTMQ==", "cpu": [ "x64" ], @@ -2659,10 +2650,39 @@ "node": ">= 10" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.4.tgz", + "integrity": "sha512-2TLe9ir+9esCf6Wm+lLWTMbgklIjiF0pbmDnwmhR9MksVOq+e8aP3TSsXySnBDDvTTVd/vKu1aNttEGj3P6l8Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.0", + "@emnapi/runtime": "^1.4.0", + "@emnapi/wasi-threads": "^1.0.1", + "@napi-rs/wasm-runtime": "^0.2.8", + "@tybys/wasm-util": "^0.9.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.3.tgz", - "integrity": "sha512-PEj7XR4OGTGoboTIAdXicKuWl4EQIjKHKuR+bFy9oYN7CFZo0eu74+70O4XuERX4yjqVZGAkCdglBODlgqcCXg==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.4.tgz", + "integrity": "sha512-VlnhfilPlO0ltxW9/BgfLI5547PYzqBMPIzRrk4W7uupgCt8z6Trw/tAj6QUtF2om+1MH281Pg+HHUJoLesmng==", "cpu": [ "arm64" ], @@ -2676,9 +2696,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.3.tgz", - "integrity": "sha512-T8gfxECWDBENotpw3HR9SmNiHC9AOJdxs+woasRZ8Q/J4VHN0OMs7F+4yVNZ9EVN26Wv6mZbK0jv7eHYuLJLwA==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.4.tgz", + "integrity": "sha512-+7S63t5zhYjslUGb8NcgLpFXD+Kq1F/zt5Xv5qTv7HaFTG/DHyHD9GA6ieNAxhgyA4IcKa/zy7Xx4Oad2/wuhw==", "cpu": [ "x64" ], @@ -2692,14 +2712,14 @@ } }, "node_modules/@tailwindcss/vite": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.3.tgz", - "integrity": "sha512-lUI/QaDxLtlV52Lho6pu07CG9pSnRYLOPmKGIQjyHdTBagemc6HmgZxyjGAQ/5HMPrNeWBfTVIpQl0/jLXvWHQ==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.4.tgz", + "integrity": "sha512-4UQeMrONbvrsXKXXp/uxmdEN5JIJ9RkH7YVzs6AMxC/KC1+Np7WZBaNIco7TEjlkthqxZbt8pU/ipD+hKjm80A==", "license": "MIT", "dependencies": { - "@tailwindcss/node": "4.1.3", - "@tailwindcss/oxide": "4.1.3", - "tailwindcss": "4.1.3" + "@tailwindcss/node": "4.1.4", + "@tailwindcss/oxide": "4.1.4", + "tailwindcss": "4.1.4" }, "peerDependencies": { "vite": "^5.2.0 || ^6" @@ -3056,18 +3076,18 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.14.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz", - "integrity": "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==", + "version": "22.14.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", + "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, "node_modules/@types/react": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.0.tgz", - "integrity": "sha512-UaicktuQI+9UKyA4njtDOGBD/67t8YEBt2xdfqu8+gP9hqPUPsiXlNPcpS2gVdjmis5GKPG3fCxbQLVgxsQZ8w==", + "version": "19.1.2", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.2.tgz", + "integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==", "devOptional": true, "license": "MIT", "dependencies": { @@ -3085,17 +3105,17 @@ } }, "node_modules/@vitejs/plugin-react": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz", - "integrity": "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.0.tgz", + "integrity": "sha512-x/EztcTKVj+TDeANY1WjNeYsvZjZdfWRMP/KXi5Yn8BoTzpa13ZltaQqKfvWYbX8CE10GOHHdC5v86jY9x8i/g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.26.0", + "@babel/core": "^7.26.10", "@babel/plugin-transform-react-jsx-self": "^7.25.9", "@babel/plugin-transform-react-jsx-source": "^7.25.9", "@types/babel__core": "^7.20.5", - "react-refresh": "^0.14.2" + "react-refresh": "^0.17.0" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -3105,12 +3125,12 @@ } }, "node_modules/@xyflow/react": { - "version": "12.5.5", - "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.5.5.tgz", - "integrity": "sha512-mAtHuS4ktYBL1ph5AJt7X/VmpzzlmQBN3+OXxyT/1PzxwrVto6AKc3caerfxzwBsg3cA4J8lB63F3WLAuPMmHw==", + "version": "12.5.6", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.5.6.tgz", + "integrity": "sha512-a6lL0WoeMSp7AC9AQzWMRMuqk12Dn+lVjMDLL93SZvpWv5D2BSq9woCv21JCUdWQ31MNpJVfLaV3TycaH1tsYw==", "license": "MIT", "dependencies": { - "@xyflow/system": "0.0.55", + "@xyflow/system": "0.0.56", "classcat": "^5.0.3", "zustand": "^4.4.0" }, @@ -3120,9 +3140,9 @@ } }, "node_modules/@xyflow/system": { - "version": "0.0.55", - "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.55.tgz", - "integrity": "sha512-6cngWlE4oMXm+zrsbJxerP3wUNUFJcv/cE5kDfu0qO55OWK3fAeSOLW9td3xEVQlomjIW5knds1MzeMnBeCfqw==", + "version": "0.0.56", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.56.tgz", + "integrity": "sha512-Xc3LvEumjJD+CqPqlYkrlszJ4hWQ0DE+r5M4e5WpS/hKT4T6ktAjt7zeMNJ+vvTsXHulGnEoDRA8zbIfB6tPdQ==", "license": "MIT", "dependencies": { "@types/d3-drag": "^3.0.7", @@ -3305,9 +3325,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001713", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001713.tgz", - "integrity": "sha512-wCIWIg+A4Xr7NfhTuHdX+/FKh3+Op3LBbSp2N5Pfx6T/LhdQy3GTyoTg48BReaW/MyMNZAkTadsBtai3ldWK0Q==", + "version": "1.0.30001714", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001714.tgz", + "integrity": "sha512-mtgapdwDLSSBnCI3JokHM7oEQBLxiJKVRtg10AxM1AyeiKcM96f0Mkbqeq+1AbiCtvMcHRulAAEMu693JrSWqg==", "dev": true, "funding": [ { @@ -3574,9 +3594,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.135", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.135.tgz", - "integrity": "sha512-8gXUdEmvb+WCaYUhA0Svr08uSeRjM2w3x5uHOc1QbaEVzJXB8rgm5eptieXzyKoVEtinLvW6MtTcurA65PeS1Q==", + "version": "1.5.137", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.137.tgz", + "integrity": "sha512-/QSJaU2JyIuTbbABAo/crOs+SuAZLS+fVVS10PVrIT9hrRkmZl8Hb0xPSkKRUUWHQtYzXHpQUW3Dy5hwMzGZkA==", "dev": true, "license": "ISC" }, @@ -3872,6 +3892,20 @@ "node": ">=0.8.0" } }, + "node_modules/fdir": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", + "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==", + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -4149,7 +4183,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -4511,6 +4544,18 @@ "integrity": "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng==", "license": "Apache-2.0" }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -4719,6 +4764,18 @@ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/postcss": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", @@ -4765,9 +4822,9 @@ } }, "node_modules/protobufjs": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", - "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.0.tgz", + "integrity": "sha512-Z2E/kOY1QjoMlCytmexzYfDm/w5fKAiRwpSzGtdnXW1zC88Z2yXazHHrOtwCzn+7wSxyE8PYM4rvVcMphF9sOA==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { @@ -4799,24 +4856,28 @@ } }, "node_modules/react": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", - "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", - "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", "dependencies": { - "scheduler": "^0.26.0" + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" }, "peerDependencies": { - "react": "^19.1.0" + "react": "^18.3.1" } }, "node_modules/react-infinite-scroll-component": { @@ -4832,9 +4893,9 @@ } }, "node_modules/react-refresh": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", - "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", "dev": true, "license": "MIT", "engines": { @@ -4919,9 +4980,9 @@ } }, "node_modules/rollup": { - "version": "4.39.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.39.0.tgz", - "integrity": "sha512-thI8kNc02yNvnmJp8dr3fNWJ9tCONDhp6TV35X6HkKGGs9E6q7YWCHbe5vKiTa7TAiNcFEmXKj3X/pG2b3ci0g==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz", + "integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==", "license": "MIT", "dependencies": { "@types/estree": "1.0.7" @@ -4934,26 +4995,26 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.39.0", - "@rollup/rollup-android-arm64": "4.39.0", - "@rollup/rollup-darwin-arm64": "4.39.0", - "@rollup/rollup-darwin-x64": "4.39.0", - "@rollup/rollup-freebsd-arm64": "4.39.0", - "@rollup/rollup-freebsd-x64": "4.39.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.39.0", - "@rollup/rollup-linux-arm-musleabihf": "4.39.0", - "@rollup/rollup-linux-arm64-gnu": "4.39.0", - "@rollup/rollup-linux-arm64-musl": "4.39.0", - "@rollup/rollup-linux-loongarch64-gnu": "4.39.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.39.0", - "@rollup/rollup-linux-riscv64-gnu": "4.39.0", - "@rollup/rollup-linux-riscv64-musl": "4.39.0", - "@rollup/rollup-linux-s390x-gnu": "4.39.0", - "@rollup/rollup-linux-x64-gnu": "4.39.0", - "@rollup/rollup-linux-x64-musl": "4.39.0", - "@rollup/rollup-win32-arm64-msvc": "4.39.0", - "@rollup/rollup-win32-ia32-msvc": "4.39.0", - "@rollup/rollup-win32-x64-msvc": "4.39.0", + "@rollup/rollup-android-arm-eabi": "4.40.0", + "@rollup/rollup-android-arm64": "4.40.0", + "@rollup/rollup-darwin-arm64": "4.40.0", + "@rollup/rollup-darwin-x64": "4.40.0", + "@rollup/rollup-freebsd-arm64": "4.40.0", + "@rollup/rollup-freebsd-x64": "4.40.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.40.0", + "@rollup/rollup-linux-arm-musleabihf": "4.40.0", + "@rollup/rollup-linux-arm64-gnu": "4.40.0", + "@rollup/rollup-linux-arm64-musl": "4.40.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.40.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.40.0", + "@rollup/rollup-linux-riscv64-gnu": "4.40.0", + "@rollup/rollup-linux-riscv64-musl": "4.40.0", + "@rollup/rollup-linux-s390x-gnu": "4.40.0", + "@rollup/rollup-linux-x64-gnu": "4.40.0", + "@rollup/rollup-linux-x64-musl": "4.40.0", + "@rollup/rollup-win32-arm64-msvc": "4.40.0", + "@rollup/rollup-win32-ia32-msvc": "4.40.0", + "@rollup/rollup-win32-x64-msvc": "4.40.0", "fsevents": "~2.3.2" } }, @@ -4978,10 +5039,13 @@ "license": "MIT" }, "node_modules/scheduler": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", - "license": "MIT" + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } }, "node_modules/semver": { "version": "6.3.1", @@ -5090,9 +5154,9 @@ "license": "MIT" }, "node_modules/tailwindcss": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.3.tgz", - "integrity": "sha512-2Q+rw9vy1WFXu5cIxlvsabCwhU2qUwodGq03ODhLJ0jW4ek5BUtoCsnLB0qG+m8AHgEsSJcJGDSDe06FXlP74g==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.4.tgz", + "integrity": "sha512-1ZIUqtPITFbv/DxRmDr5/agPqJwF69d24m9qmM1939TJehgY539CtzeZRjbLt5G6fSy/7YqqYsfvoTEw9xUI2A==", "license": "MIT" }, "node_modules/tapable": { @@ -5113,6 +5177,22 @@ "node": ">=8" } }, + "node_modules/tinyglobby": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz", + "integrity": "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==", + "license": "MIT", + "dependencies": { + "fdir": "^6.4.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -5195,14 +5275,17 @@ } }, "node_modules/vite": { - "version": "6.2.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.6.tgz", - "integrity": "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.1.tgz", + "integrity": "sha512-kkzzkqtMESYklo96HKKPE5KKLkC1amlsqt+RjFMlX2AvbRB/0wghap19NdBxxwGZ+h/C6DLCrcEphPIItlGrRQ==", "license": "MIT", "dependencies": { "esbuild": "^0.25.0", + "fdir": "^6.4.3", + "picomatch": "^4.0.2", "postcss": "^8.5.3", - "rollup": "^4.30.1" + "rollup": "^4.34.9", + "tinyglobby": "^0.2.12" }, "bin": { "vite": "bin/vite.js" diff --git a/my-app/package.json b/my-app/package.json index a154503b..2c237a57 100644 --- a/my-app/package.json +++ b/my-app/package.json @@ -13,7 +13,6 @@ "@dagrejs/dagre": "^1.1.4", "@headlessui/react": "^2.2.1", "@heroicons/react": "^2.2.0", - "@react-buddy/ide-toolbox": "^2.4.0", "@tailwindcss/vite": "^4.0.17", "@xyflow/react": "^12.5.5", "autoprefixer": "^10.4.21", @@ -23,8 +22,8 @@ "mobx": "^6.13.7", "mobx-react-lite": "^4.1.0", "pdfjs-dist": "^5.1.91", - "react": "^19.0.0", - "react-dom": "^19.0.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", "react-infinite-scroll-component": "^6.1.0", "react-router-dom": "^7.4.0", "reactflow": "^11.11.4", From 163f942507870ebf70e3c2021f0b9e7302c85f62 Mon Sep 17 00:00:00 2001 From: Justus Kluge Date: Thu, 17 Apr 2025 11:51:15 +0200 Subject: [PATCH 15/16] Reverted model changes --- my-app/src/model.js | 55 ++++++++++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/my-app/src/model.js b/my-app/src/model.js index 1a2b77a3..d52f7ff5 100644 --- a/my-app/src/model.js +++ b/my-app/src/model.js @@ -11,14 +11,14 @@ export const model = { courses: [], favourites: [], isReady: false, - filtersChange: false, // + filtersChange: false, filtersCalculated: false, filteredCourses: [], filterOptions: { applyTranscriptFilter: true, eligibility: "weak", //the possible values for the string are: "weak"/"moderate"/"strong" applyLevelFilter: true, - level: [], //the possible values for the array are: "PREPARATORY", "BASIC", "ADVANCED", "RESEARCH" + level: ["PREPARATORY", "BASIC", "ADVANCED", "RESEARCH"], //the possible values for the array are: "PREPARATORY", "BASIC", "ADVANCED", "RESEARCH" applyLanguageFilter: true, language: "none", //the possible values for the string are: "none"/"english"/"swedish"/"both" applyLocationFilter:true, @@ -26,8 +26,12 @@ export const model = { applyCreditsFilter:true, creditMin: 0, creditMax: 45, - applyDepartmentFilter:false, - department: [] + applyDepartmentFilter: true, + department: ["EECS/Computational Science and Technology", "EECS/Theoretical Computer Science", "EECS/Electric Power and Energy Systems", "EECS/Network and Systems Engineering", + "ITM/Learning in Engineering Sciences", "ITM/Industrial Economics and Management", "ITM/Energy Systems", "ITM/Integrated Product Development and Design", "ITM/SKD GRU", + "SCI/Mathematics", "SCI/Applied Physics", "SCI/Mechanics", "SCI/Aeronautical and Vehicle Engineering", + "ABE/Sustainability and Environmental Engineering", "ABE/Concrete Structures", "ABE/Structural Design & Bridges", "ABE/History of Science, Technology and Environment", ], + applyRemoveNullCourses: false }, setUser(user) { @@ -84,15 +88,17 @@ export const model = { 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 ?? "", + name: entry[1]?.name ?? "null", + location: entry[1]?.location ?? "null", + department: entry[1]?.department ?? "null", + language: entry[1]?.language ?? "null", + description: entry[1]?.description ?? "null", + academicLevel: entry[1]?.academic_level ?? "null", + period: entry[1]?.period ?? "null", credits: entry[1]?.credits ?? 0, - prerequisites: entry[1]?.prerequisites ?? "", + prerequisites: entry[1]?.prerequisites ?? "null", + prerequisites_text: entry[1]?.prerequisites_text ?? "null", + learning_outcomes: entry[1]?.learning_outcomes ?? "null" }; this.addCourse(course); }); @@ -128,10 +134,20 @@ export const model = { setFilterOptions(options){ this.filterOptions = options; // do we want to set the flags? What about useEffect? }, + + setApplyRemoveNullCourses() { + this.filterOptions.applyRemoveNullCourses = !this.filterOptions.applyRemoveNullCourses; + this.setFiltersChange(); + }, updateLevelFilter(level) { this.filterOptions.level = level; }, + + updateDepartmentFilter(department) { + this.filterOptions.department = department; + }, + updateLanguageFilter(languages) { this.filterOptions.language = languages; }, @@ -162,10 +178,13 @@ export const model = { setApplyCreditsFilter(creditsFilterState) { this.filterOptions.applyCreditsFilter = creditsFilterState; }, - // setApplyDepartmentFilter(departmentFilterState) { - // this.filterOptions.applyDepartmentFilter = departmentFilterState; - // }, - - - + setApplyDepartmentFilter(departmentFilterState) { + this.filterOptions.applyDepartmentFilter = departmentFilterState; + }, + async getAverageRating(courseCode) { + const reviews = await getReviewsForCourse(courseCode); + if (!reviews || reviews.length === 0) return null; + const total = reviews.reduce((sum, review) => sum + (review.overallRating || 0), 0); + return (total / reviews.length).toFixed(1); + }, }; From 8568e60ffe352a5ca2cd03847b2302b3f753c88e Mon Sep 17 00:00:00 2001 From: Justus Kluge Date: Tue, 6 May 2025 20:01:02 +0200 Subject: [PATCH 16/16] Scroll Up Button --- my-app/src/presenters/ListViewPresenter.jsx | 5 +++++ my-app/src/views/ListView.jsx | 16 ++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/my-app/src/presenters/ListViewPresenter.jsx b/my-app/src/presenters/ListViewPresenter.jsx index bc0ef577..ed6518d5 100644 --- a/my-app/src/presenters/ListViewPresenter.jsx +++ b/my-app/src/presenters/ListViewPresenter.jsx @@ -70,6 +70,10 @@ const ListViewPresenter = observer(({ model }) => { } }; + const setTargetScroll = (position) =>{ + model.setScrollPosition(position); + } + const [isPopupOpen, setIsPopupOpen] = useState(false); const [selectedCourse, setSelectedCourse] = useState(null); const preP = { popup={popup} targetScroll={model.scrollPosition} + setTargetScroll={setTargetScroll} scrollContainerRef={scrollContainerRef} persistantScrolling={persistantScrolling} diff --git a/my-app/src/views/ListView.jsx b/my-app/src/views/ListView.jsx index 666c15cd..09c45e10 100644 --- a/my-app/src/views/ListView.jsx +++ b/my-app/src/views/ListView.jsx @@ -49,6 +49,12 @@ function ListView(props) { } }, [props.targetScroll, hasMore, displayedCourses.length]); + useEffect(() => { + if (props.targetScroll === 0 && props.scrollContainerRef?.current) { + props.scrollContainerRef.current.scrollTop = 0; + } +}, [props.targetScroll]); + if (!props.courses) { return (
@@ -170,6 +176,16 @@ function ListView(props) {
)} {props.popup} + {!isLoading && props.targetScroll > 1000 &&( + + )} +
); }