diff --git a/.github/workflows/firebase-hosting-merge.yml b/.github/workflows/firebase-hosting-merge.yml
new file mode 100644
index 00000000..68c33d74
--- /dev/null
+++ b/.github/workflows/firebase-hosting-merge.yml
@@ -0,0 +1,20 @@
+# This file was auto-generated by the Firebase CLI
+# https://github.com/firebase/firebase-tools
+
+name: Deploy to Firebase Hosting on merge
+on:
+ push:
+ branches:
+ - main
+jobs:
+ build_and_deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - run: npm ci && npm run build
+ - uses: FirebaseExtended/action-hosting-deploy@v0
+ with:
+ repoToken: ${{ secrets.GITHUB_TOKEN }}
+ firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_FINDMYNEXTCOURSE }}
+ channelId: live
+ projectId: findmynextcourse
diff --git a/.github/workflows/firebase-hosting-pull-request.yml b/.github/workflows/firebase-hosting-pull-request.yml
new file mode 100644
index 00000000..09f4233d
--- /dev/null
+++ b/.github/workflows/firebase-hosting-pull-request.yml
@@ -0,0 +1,21 @@
+# This file was auto-generated by the Firebase CLI
+# https://github.com/firebase/firebase-tools
+
+name: Deploy to Firebase Hosting on PR
+on: pull_request
+permissions:
+ checks: write
+ contents: read
+ pull-requests: write
+jobs:
+ build_and_preview:
+ if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - run: npm ci && npm run build
+ - uses: FirebaseExtended/action-hosting-deploy@v0
+ with:
+ repoToken: ${{ secrets.GITHUB_TOKEN }}
+ firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_FINDMYNEXTCOURSE }}
+ projectId: findmynextcourse
diff --git a/my-app/package-lock.json b/my-app/package-lock.json
index 2f16fd62..43ca33fe 100644
--- a/my-app/package-lock.json
+++ b/my-app/package-lock.json
@@ -15,7 +15,9 @@
"@xyflow/react": "^12.5.5",
"autoprefixer": "^10.4.21",
"firebase": "^11.5.0",
+ "fuse.js": "^7.1.0",
"ldrs": "^1.1.6",
+ "lodash.debounce": "^4.0.8",
"lodash.throttle": "^4.1.1",
"mobx": "^6.13.7",
"mobx-react-lite": "^4.1.0",
@@ -177,9 +179,9 @@
}
},
"node_modules/@babel/helper-plugin-utils": {
- "version": "7.26.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz",
- "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
+ "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -354,6 +356,16 @@
"node": ">17.0.0"
}
},
+ "node_modules/@emnapi/runtime": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz",
+ "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz",
@@ -2797,12 +2809,6 @@
"@babel/types": "^7.20.7"
}
},
- "node_modules/@types/cookie": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
- "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
- "license": "MIT"
- },
"node_modules/@types/d3": {
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
@@ -3893,9 +3899,9 @@
}
},
"node_modules/fdir": {
- "version": "6.4.3",
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz",
- "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==",
+ "version": "6.4.4",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
+ "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
"license": "MIT",
"peerDependencies": {
"picomatch": "^3 || ^4"
@@ -4021,6 +4027,15 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
+ "node_modules/fuse.js": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz",
+ "integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -4525,6 +4540,12 @@
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
"license": "MIT"
},
+ "node_modules/lodash.debounce": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
+ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
+ "license": "MIT"
+ },
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -4903,12 +4924,11 @@
}
},
"node_modules/react-router": {
- "version": "7.5.0",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.0.tgz",
- "integrity": "sha512-estOHrRlDMKdlQa6Mj32gIks4J+AxNsYoE0DbTTxiMy2mPzZuWSDU+N85/r1IlNR7kGfznF3VCUlvc5IUO+B9g==",
+ "version": "7.5.3",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.3.tgz",
+ "integrity": "sha512-3iUDM4/fZCQ89SXlDa+Ph3MevBrozBAI655OAfWQlTm9nBR0IKlrmNwFow5lPHttbwvITZfkeeeZFP6zt3F7pw==",
"license": "MIT",
"dependencies": {
- "@types/cookie": "^0.6.0",
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0",
"turbo-stream": "2.4.0"
@@ -4927,12 +4947,12 @@
}
},
"node_modules/react-router-dom": {
- "version": "7.5.0",
- "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.5.0.tgz",
- "integrity": "sha512-fFhGFCULy4vIseTtH5PNcY/VvDJK5gvOWcwJVHQp8JQcWVr85ENhJ3UpuF/zP1tQOIFYNRJHzXtyhU1Bdgw0RA==",
+ "version": "7.5.3",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.5.3.tgz",
+ "integrity": "sha512-cK0jSaTyW4jV9SRKAItMIQfWZ/D6WEZafgHuuCb9g+SjhLolY78qc+De4w/Cz9ybjvLzShAmaIMEXt8iF1Cm+A==",
"license": "MIT",
"dependencies": {
- "react-router": "7.5.0"
+ "react-router": "7.5.3"
},
"engines": {
"node": ">=20.0.0"
@@ -5178,12 +5198,12 @@
}
},
"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==",
+ "version": "0.2.13",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
+ "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
"license": "MIT",
"dependencies": {
- "fdir": "^6.4.3",
+ "fdir": "^6.4.4",
"picomatch": "^4.0.2"
},
"engines": {
@@ -5275,17 +5295,17 @@
}
},
"node_modules/vite": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.1.tgz",
- "integrity": "sha512-kkzzkqtMESYklo96HKKPE5KKLkC1amlsqt+RjFMlX2AvbRB/0wghap19NdBxxwGZ+h/C6DLCrcEphPIItlGrRQ==",
+ "version": "6.3.5",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
+ "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
- "fdir": "^6.4.3",
+ "fdir": "^6.4.4",
"picomatch": "^4.0.2",
"postcss": "^8.5.3",
"rollup": "^4.34.9",
- "tinyglobby": "^0.2.12"
+ "tinyglobby": "^0.2.13"
},
"bin": {
"vite": "bin/vite.js"
@@ -5436,6 +5456,20 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/yaml": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz",
+ "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==",
+ "license": "ISC",
+ "optional": true,
+ "peer": true,
+ "bin": {
+ "yaml": "bin.mjs"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
diff --git a/my-app/package.json b/my-app/package.json
index 2c237a57..05512cb0 100644
--- a/my-app/package.json
+++ b/my-app/package.json
@@ -17,7 +17,9 @@
"@xyflow/react": "^12.5.5",
"autoprefixer": "^10.4.21",
"firebase": "^11.5.0",
+ "fuse.js": "^7.1.0",
"ldrs": "^1.1.6",
+ "lodash.debounce": "^4.0.8",
"lodash.throttle": "^4.1.1",
"mobx": "^6.13.7",
"mobx-react-lite": "^4.1.0",
diff --git a/my-app/src/presenters/SearchbarPresenter.jsx b/my-app/src/presenters/SearchbarPresenter.jsx
index 1f625be2..81716647 100644
--- a/my-app/src/presenters/SearchbarPresenter.jsx
+++ b/my-app/src/presenters/SearchbarPresenter.jsx
@@ -1,27 +1,39 @@
-import React, { useEffect } from 'react';
+import React, { useEffect, useCallback } from 'react';
import { observer } from "mobx-react-lite";
import { useState } from 'react';
import CoursePagePopup from '../views/Components/CoursePagePopup.jsx';
import PrerequisitePresenter from './PrerequisitePresenter.jsx';
import { ReviewPresenter } from "../presenters/ReviewPresenter.jsx";
import SearchbarView from "../views/SearchbarView.jsx";
+import Fuse from 'fuse.js'
+import debounce from 'lodash.debounce';
const SearchbarPresenter = observer(({ model }) => {
- const searchCourses = (query) => {
- //model.filteredCourses is essentially a smaller subset of model.courses, if theres no filters, it should be the same
- console.log("---------------search recalculated");
- console.log("filtered courses length: ", model.filteredCourses.length);
- const searchResults = model.filteredCourses.filter(course =>
- course.code.toLowerCase().includes(query.toLowerCase()) ||
- course.name.toLowerCase().includes(query.toLowerCase()) ||
- course.description?.toLowerCase().includes(query.toLowerCase())
- );
- model.setCurrentSearchText(query);
- model.setCurrentSearch(searchResults);
- console.log(model.currentSearch.length);
+ const [searchQuery, setSearchQuery] = useState("");
+
+ const fuseOptions = {
+ keys: [
+ { name: 'code', weight: 0.6 },
+ { name: 'name', weight: 0.3 },
+ { name: 'description', weight: 0.1 },
+ ],
+ threshold: 0.3, // adjust this for sensitivity
+ ignoreLocation: true,
+ minMatchCharLength: 2,
};
+ // Debounced search function
+ const searchCourses = useCallback(debounce((query) => {
+ if (!query.trim()) {
+ model.setCurrentSearch(model.filteredCourses);
+ } else {
+ const fuse = new Fuse(model.filteredCourses, fuseOptions);
+ const results = fuse.search(query).map((r) => r.item);
+ model.setCurrentSearch(results);
+ }
+ }, 500), []);
+
const addFavourite = (course) => {
model.addFavourite(course);
};
@@ -54,7 +66,6 @@ const SearchbarPresenter = observer(({ model }) => {
const [selectedCourse, setSelectedCourse] = useState(null);
const preP = ;
const reviewPresenter = ;
- const [searchQuery, setSearchQuery] = useState("");
const popup =