From 6bf9c8c7a001f03eaddd35ea98bf66d26c5a621a Mon Sep 17 00:00:00 2001 From: kexana Date: Fri, 28 Mar 2025 09:37:25 +0100 Subject: [PATCH 01/29] sidebar component add --- my-app/src/components/SideBar.jsx | 101 ++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 my-app/src/components/SideBar.jsx diff --git a/my-app/src/components/SideBar.jsx b/my-app/src/components/SideBar.jsx new file mode 100644 index 00000000..af5acc45 --- /dev/null +++ b/my-app/src/components/SideBar.jsx @@ -0,0 +1,101 @@ +import React, { useEffect, useState } from "react"; +import { AiOutlineCheckCircle, AiOutlineCloudUpload } from "react-icons/ai"; +import { MdClear } from "react-icons/md"; +import "./drag-drop.css"; + +const DragNdrop = ({ + onFilesSelected, + width, + height, +}) => { + const [files, setFiles] = useState([]); + + const handleFileChange = (event) => { + const selectedFiles = event.target.files; + if (selectedFiles && selectedFiles.length > 0) { + const newFiles = Array.from(selectedFiles); + setFiles((prevFiles) => [...prevFiles, ...newFiles]); + } + }; + const handleDrop = (event) => { + event.preventDefault(); + const droppedFiles = event.dataTransfer.files; + if (droppedFiles.length > 0) { + const newFiles = Array.from(droppedFiles); + setFiles((prevFiles) => [...prevFiles, ...newFiles]); + } + }; + + const handleRemoveFile = () => { + setFiles((prevFiles) => prevFiles.filter((_, i) => i !== index)); + }; + + useEffect(() => { + onFilesSelected(files); + }, [files, onFilesSelected]); + + return ( +
+
0 ? "upload-box active" : "upload-box" + }`} + onDrop={handleDrop} + onDragOver={(event) => event.preventDefault()} + > + <> +
+ +
+

Drag and drop your files here

+

+ Limit 15MB per file. Supported files: .PDF, .DOCX, .PPTX, .TXT, + .XLSX +

+
+
+ + + + + {files.length > 0 && ( +
+
+ {files.map((file, index) => ( +
+
+

{file.name}

+ {/*

{file.type}

*/} +
+
+ handleRemoveFile(index)} /> +
+
+ ))} +
+
+ )} + + {files.length > 0 && ( +
+ +

{files.length} file(s) selected

+
+ )} +
+
+ ); +}; + +export default DragNdrop; \ No newline at end of file From 8e291f20037ddc0b544871f66e8083292c3de228 Mon Sep 17 00:00:00 2001 From: kexana Date: Fri, 28 Mar 2025 11:36:41 +0100 Subject: [PATCH 02/29] tailwind workin, beggining page building --- my-app/firebase.js | 2 +- my-app/index.html | 3 +- my-app/package-lock.json | 114 ++++++++++++++++++ my-app/package.json | 3 + my-app/src/components/FileUpload.jsx | 9 ++ my-app/src/components/SideBar.jsx | 105 +--------------- my-app/src/index.jsx | 1 + my-app/src/pages/HomeRoot.jsx | 17 ++- .../src/views/{DummyView.jsx => MainView.jsx} | 10 +- 9 files changed, 152 insertions(+), 112 deletions(-) create mode 100644 my-app/src/components/FileUpload.jsx rename my-app/src/views/{DummyView.jsx => MainView.jsx} (68%) diff --git a/my-app/firebase.js b/my-app/firebase.js index e44ee539..e5e0a165 100644 --- a/my-app/firebase.js +++ b/my-app/firebase.js @@ -48,7 +48,7 @@ export async function fetchCourses() { const snapshot = await get(myRef); if (!snapshot.exists()) return []; const value = snapshot.val(); - const courses = []; + let courses = []; for (const id of Object.keys(courses)) { courses = [...courses, courses[id]]; } diff --git a/my-app/index.html b/my-app/index.html index e21f0f44..dc6f3991 100644 --- a/my-app/index.html +++ b/my-app/index.html @@ -3,8 +3,9 @@ + - Vite + React + Fins my course
diff --git a/my-app/package-lock.json b/my-app/package-lock.json index cb97c913..f33473f1 100644 --- a/my-app/package-lock.json +++ b/my-app/package-lock.json @@ -10,8 +10,11 @@ "dependencies": { "@tailwindcss/vite": "^4.0.17", "firebase": "^11.5.0", + "mobx": "^6.13.7", + "mobx-react-lite": "^4.1.0", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-router-dom": "^7.4.0", "tailwindcss": "^4.0.17" }, "devDependencies": { @@ -2224,6 +2227,12 @@ "@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/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", @@ -2503,6 +2512,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3494,6 +3512,41 @@ "node": "*" } }, + "node_modules/mobx": { + "version": "6.13.7", + "resolved": "https://registry.npmjs.org/mobx/-/mobx-6.13.7.tgz", + "integrity": "sha512-aChaVU/DO5aRPmk1GX8L+whocagUUpBQqoPtJk+cm7UOXUk87J4PeWCh6nNmTTIfEhiR9DI/+FnA8dln/hTK7g==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mobx" + } + }, + "node_modules/mobx-react-lite": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mobx-react-lite/-/mobx-react-lite-4.1.0.tgz", + "integrity": "sha512-QEP10dpHHBeQNv1pks3WnHRCem2Zp636lq54M2nKO2Sarr13pL4u6diQXf65yzXUn0mkk18SyIDCm9UOJYTi1w==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.4.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mobx" + }, + "peerDependencies": { + "mobx": "^6.9.0", + "react": "^16.8.0 || ^17 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3725,6 +3778,46 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.4.0.tgz", + "integrity": "sha512-Y2g5ObjkvX3VFeVt+0CIPuYd9PpgqCslG7ASSIdN73LwA1nNWzcMLaoMRJfP3prZFI92svxFwbn7XkLJ+UPQ6A==", + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0", + "turbo-stream": "2.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.4.0.tgz", + "integrity": "sha512-VlksBPf3n2bijPvnA7nkTsXxMAKOj+bWp4R9c3i+bnwlSOFAGOkJkKhzy/OsRkWaBMICqcAl1JDzh9ZSOze9CA==", + "license": "MIT", + "dependencies": { + "react-router": "7.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -3825,6 +3918,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3930,6 +4029,12 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/turbo-stream": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", + "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==", + "license": "ISC" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -3990,6 +4095,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", + "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/vite": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.3.tgz", diff --git a/my-app/package.json b/my-app/package.json index ad88064b..4602e0f8 100644 --- a/my-app/package.json +++ b/my-app/package.json @@ -12,8 +12,11 @@ "dependencies": { "@tailwindcss/vite": "^4.0.17", "firebase": "^11.5.0", + "mobx": "^6.13.7", + "mobx-react-lite": "^4.1.0", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-router-dom": "^7.4.0", "tailwindcss": "^4.0.17" }, "devDependencies": { diff --git a/my-app/src/components/FileUpload.jsx b/my-app/src/components/FileUpload.jsx new file mode 100644 index 00000000..0893f1a0 --- /dev/null +++ b/my-app/src/components/FileUpload.jsx @@ -0,0 +1,9 @@ +import React, { useEffect, useState } from "react"; +//import "./drag-drop.css"; + +export default function Testo () { + return (
+ + upload file +
); +} \ No newline at end of file diff --git a/my-app/src/components/SideBar.jsx b/my-app/src/components/SideBar.jsx index af5acc45..e5e2168a 100644 --- a/my-app/src/components/SideBar.jsx +++ b/my-app/src/components/SideBar.jsx @@ -1,101 +1,8 @@ import React, { useEffect, useState } from "react"; -import { AiOutlineCheckCircle, AiOutlineCloudUpload } from "react-icons/ai"; -import { MdClear } from "react-icons/md"; -import "./drag-drop.css"; +import Testo from "./FileUpload"; -const DragNdrop = ({ - onFilesSelected, - width, - height, -}) => { - const [files, setFiles] = useState([]); - - const handleFileChange = (event) => { - const selectedFiles = event.target.files; - if (selectedFiles && selectedFiles.length > 0) { - const newFiles = Array.from(selectedFiles); - setFiles((prevFiles) => [...prevFiles, ...newFiles]); - } - }; - const handleDrop = (event) => { - event.preventDefault(); - const droppedFiles = event.dataTransfer.files; - if (droppedFiles.length > 0) { - const newFiles = Array.from(droppedFiles); - setFiles((prevFiles) => [...prevFiles, ...newFiles]); - } - }; - - const handleRemoveFile = () => { - setFiles((prevFiles) => prevFiles.filter((_, i) => i !== index)); - }; - - useEffect(() => { - onFilesSelected(files); - }, [files, onFilesSelected]); - - return ( -
-
0 ? "upload-box active" : "upload-box" - }`} - onDrop={handleDrop} - onDragOver={(event) => event.preventDefault()} - > - <> -
- -
-

Drag and drop your files here

-

- Limit 15MB per file. Supported files: .PDF, .DOCX, .PPTX, .TXT, - .XLSX -

-
-
- - - - - {files.length > 0 && ( -
-
- {files.map((file, index) => ( -
-
-

{file.name}

- {/*

{file.type}

*/} -
-
- handleRemoveFile(index)} /> -
-
- ))} -
-
- )} - - {files.length > 0 && ( -
- -

{files.length} file(s) selected

-
- )} -
-
- ); -}; - -export default DragNdrop; \ No newline at end of file +export default function SideBar () { + return (
+ +
); +} \ No newline at end of file diff --git a/my-app/src/index.jsx b/my-app/src/index.jsx index a05e3c4f..f94d21a2 100644 --- a/my-app/src/index.jsx +++ b/my-app/src/index.jsx @@ -4,6 +4,7 @@ import { createRoot } from "react-dom/client"; import { connectToFirebase } from "../firebase"; import { model } from "./model" import { HomeRoot } from "./pages/HomeRoot"; +import "./styles.css"; configure({ enforceActions: "never" }); const reactiveModel = observable(model); diff --git a/my-app/src/pages/HomeRoot.jsx b/my-app/src/pages/HomeRoot.jsx index e685407b..f4c56128 100644 --- a/my-app/src/pages/HomeRoot.jsx +++ b/my-app/src/pages/HomeRoot.jsx @@ -1,13 +1,18 @@ import { observer } from "mobx-react-lite"; -import DummyView from "../views/DummyView" +import MainView from "../views/MainView" import { AddToDB } from "../presenters/AddToDB" export const HomeRoot = observer(function ArtistRoot({ model }) { - return ( -
-

This is a dummy website for our Homepage / Mainpage!

- {/*The view is very simple */} - {/*Presenter for more complex shit */} + return (
+
+
+ {/*The view is very simple */} +
+
+ {/*The view is very simple */} +
+
+ {/*Presenter for more complex shit */}
); }); diff --git a/my-app/src/views/DummyView.jsx b/my-app/src/views/MainView.jsx similarity index 68% rename from my-app/src/views/DummyView.jsx rename to my-app/src/views/MainView.jsx index 670206e5..6e47ca81 100644 --- a/my-app/src/views/DummyView.jsx +++ b/my-app/src/views/MainView.jsx @@ -1,4 +1,7 @@ import { useState } from 'react' +import Testo from '../components/FileUpload' +import SideBar from '../components/SideBar' + // dummy view, that returns text and a component state based counter. function App() { @@ -7,14 +10,11 @@ function App() { return ( <>
-

This is a dummy view, which has a counter

-

- +

+ ) } From 9492d09c36a112ad5643eeeb35c6a382e95f9634 Mon Sep 17 00:00:00 2001 From: Sami Al Saati Date: Fri, 28 Mar 2025 11:55:02 +0100 Subject: [PATCH 03/29] structure --- my-app/postcss.config.js | 5 +++++ my-app/src/pages/HomeRoot.jsx | 16 ++++++-------- my-app/src/presenters/AddToDB.jsx | 4 ++-- my-app/src/views/ListView.jsx | 0 my-app/src/views/MainView.jsx | 22 ------------------- .../{TextInputView.jsx => SearchbarView.jsx} | 9 ++++---- my-app/src/views/SidebarView.jsx | 12 ++++++++++ my-app/tailwind.config.js | 11 ++++++++++ 8 files changed, 42 insertions(+), 37 deletions(-) create mode 100644 my-app/postcss.config.js create mode 100644 my-app/src/views/ListView.jsx delete mode 100644 my-app/src/views/MainView.jsx rename my-app/src/views/{TextInputView.jsx => SearchbarView.jsx} (82%) create mode 100644 my-app/src/views/SidebarView.jsx create mode 100644 my-app/tailwind.config.js diff --git a/my-app/postcss.config.js b/my-app/postcss.config.js new file mode 100644 index 00000000..b6dc0349 --- /dev/null +++ b/my-app/postcss.config.js @@ -0,0 +1,5 @@ +export default { + plugins: { + autoprefixer: {}, + }, +} diff --git a/my-app/src/pages/HomeRoot.jsx b/my-app/src/pages/HomeRoot.jsx index f4c56128..01dd94b6 100644 --- a/my-app/src/pages/HomeRoot.jsx +++ b/my-app/src/pages/HomeRoot.jsx @@ -1,17 +1,15 @@ import { observer } from "mobx-react-lite"; -import MainView from "../views/MainView" +import MainView from "../views/SidebarView.jsx" import { AddToDB } from "../presenters/AddToDB" +import SidebarView from "../views/SidebarView.jsx"; export const HomeRoot = observer(function ArtistRoot({ model }) { - return (
-
-
- {/*The view is very simple */} -
-
- {/*The view is very simple */} + return ( +
+ +
+
-
{/*Presenter for more complex shit */}
); diff --git a/my-app/src/presenters/AddToDB.jsx b/my-app/src/presenters/AddToDB.jsx index 06ef553f..43d196b0 100644 --- a/my-app/src/presenters/AddToDB.jsx +++ b/my-app/src/presenters/AddToDB.jsx @@ -1,11 +1,11 @@ import { observer } from "mobx-react-lite"; import { useState } from "react"; -import { TextInputView } from "../views/TextInputView"; +import SearchbarView from "../views/SearchbarView.jsx"; // A dummy presenter, which adds the course supplied in the text field to the db. export const AddToDB = observer(function AddToDB({ model }) { const [text, setText] = useState(""); - return ; + return ; function onTextChanged(e){ setText(e.target.value); diff --git a/my-app/src/views/ListView.jsx b/my-app/src/views/ListView.jsx new file mode 100644 index 00000000..e69de29b diff --git a/my-app/src/views/MainView.jsx b/my-app/src/views/MainView.jsx deleted file mode 100644 index 6e47ca81..00000000 --- a/my-app/src/views/MainView.jsx +++ /dev/null @@ -1,22 +0,0 @@ -import { useState } from 'react' -import Testo from '../components/FileUpload' -import SideBar from '../components/SideBar' - - -// dummy view, that returns text and a component state based counter. -function App() { - const [count, setCount] = useState(0) // Ideally in presenter, if the logic gets more complex :)) - - return ( - <> -
-
-

- -

- - - ) -} - -export default App diff --git a/my-app/src/views/TextInputView.jsx b/my-app/src/views/SearchbarView.jsx similarity index 82% rename from my-app/src/views/TextInputView.jsx rename to my-app/src/views/SearchbarView.jsx index a5282478..1cf1d7db 100644 --- a/my-app/src/views/TextInputView.jsx +++ b/my-app/src/views/SearchbarView.jsx @@ -1,4 +1,5 @@ -export function TextInputView(props){ + +const SearchbarView = (props) =>{ return

This is another view, where the presenter submits the text field to the Database

+ Submit +
; -} \ No newline at end of file +} diff --git a/my-app/src/views/SidebarView.jsx b/my-app/src/views/SidebarView.jsx new file mode 100644 index 00000000..7170a7cc --- /dev/null +++ b/my-app/src/views/SidebarView.jsx @@ -0,0 +1,12 @@ +import React from 'react'; + +function SidebarView(props){ + return ( +
+ sdss +
+ ); +} + +export default SidebarView; + diff --git a/my-app/tailwind.config.js b/my-app/tailwind.config.js new file mode 100644 index 00000000..b49d3631 --- /dev/null +++ b/my-app/tailwind.config.js @@ -0,0 +1,11 @@ + +module.exports = { + content: [ + './index.html', + './src/**/*.{js,jsx,ts,tsx}', + ], + theme: { + extend: {}, + }, + plugins: [], +}; From d7c45ab629db575e9ea27890cc77f8019deeb5c0 Mon Sep 17 00:00:00 2001 From: Sami Al Saati Date: Fri, 28 Mar 2025 13:08:25 +0100 Subject: [PATCH 04/29] fixed app --- my-app/README.md | 2 +- my-app/src/components/FileUpload.jsx | 9 --------- my-app/src/components/SideBar.jsx | 8 -------- my-app/src/index.jsx | 4 ++-- my-app/src/model.js | 3 ++- my-app/src/pages/App.jsx | 13 +++++++++++++ my-app/src/pages/HomeRoot.jsx | 16 ---------------- my-app/src/views/ListView.jsx | 11 +++++++++++ my-app/src/views/SidebarView.jsx | 15 ++++++++++----- my-app/vite.config.js | 1 - 10 files changed, 39 insertions(+), 43 deletions(-) delete mode 100644 my-app/src/components/FileUpload.jsx delete mode 100644 my-app/src/components/SideBar.jsx create mode 100644 my-app/src/pages/App.jsx delete mode 100644 my-app/src/pages/HomeRoot.jsx diff --git a/my-app/README.md b/my-app/README.md index 41237a77..175520f3 100644 --- a/my-app/README.md +++ b/my-app/README.md @@ -14,7 +14,7 @@ Project Structure Draft: │   ├── index.jsx // the react router - routes between pages, all pages are inserted here │   ├── model.js // the model - handles prog. logic, that is either global or account specific │   ├── pages // pages combine the presenters to a webpage. mby obsolete for 1 page project -│   │   └── HomeRoot.jsx // The future homepage? +│   │   └── App.jsx // The future homepage? │   ├── presenters // Presenters link views and the model. │   | Hooks to modify the model & component state is defined here │   │   └── HandleSearchPresenter.jsx // An example presenter diff --git a/my-app/src/components/FileUpload.jsx b/my-app/src/components/FileUpload.jsx deleted file mode 100644 index 0893f1a0..00000000 --- a/my-app/src/components/FileUpload.jsx +++ /dev/null @@ -1,9 +0,0 @@ -import React, { useEffect, useState } from "react"; -//import "./drag-drop.css"; - -export default function Testo () { - return (
- - upload file -
); -} \ No newline at end of file diff --git a/my-app/src/components/SideBar.jsx b/my-app/src/components/SideBar.jsx deleted file mode 100644 index e5e2168a..00000000 --- a/my-app/src/components/SideBar.jsx +++ /dev/null @@ -1,8 +0,0 @@ -import React, { useEffect, useState } from "react"; -import Testo from "./FileUpload"; - -export default function SideBar () { - return (
- -
); -} \ No newline at end of file diff --git a/my-app/src/index.jsx b/my-app/src/index.jsx index f94d21a2..6e851aab 100644 --- a/my-app/src/index.jsx +++ b/my-app/src/index.jsx @@ -3,7 +3,7 @@ import { createHashRouter, RouterProvider } from "react-router-dom"; import { createRoot } from "react-dom/client"; import { connectToFirebase } from "../firebase"; import { model } from "./model" -import { HomeRoot } from "./pages/HomeRoot"; +import App from "./pages/App.jsx"; import "./styles.css"; configure({ enforceActions: "never" }); @@ -14,7 +14,7 @@ export function makeRouter(reactiveModel) { return createHashRouter([ { path: "/", - element: , + element: , }, ]); } diff --git a/my-app/src/model.js b/my-app/src/model.js index c43b9a14..43c4af84 100644 --- a/my-app/src/model.js +++ b/my-app/src/model.js @@ -7,7 +7,7 @@ export const model = { user: undefined, currentCourse: undefined, currentSearch: {}, - courses: {}, + courses: [], // Initialize as an array // sets the current user setUser(user) { @@ -48,3 +48,4 @@ export const model = { }, // etc. } + diff --git a/my-app/src/pages/App.jsx b/my-app/src/pages/App.jsx new file mode 100644 index 00000000..7741f101 --- /dev/null +++ b/my-app/src/pages/App.jsx @@ -0,0 +1,13 @@ +// src/App.jsx +import React from 'react'; +import SidebarView from "../views/SidebarView.jsx"; + +function App() { + return ( +
+ +
+ ); +} + +export default App; \ No newline at end of file diff --git a/my-app/src/pages/HomeRoot.jsx b/my-app/src/pages/HomeRoot.jsx deleted file mode 100644 index 01dd94b6..00000000 --- a/my-app/src/pages/HomeRoot.jsx +++ /dev/null @@ -1,16 +0,0 @@ -import { observer } from "mobx-react-lite"; -import MainView from "../views/SidebarView.jsx" -import { AddToDB } from "../presenters/AddToDB" -import SidebarView from "../views/SidebarView.jsx"; - -export const HomeRoot = observer(function ArtistRoot({ model }) { - return ( -
- -
- -
- {/*Presenter for more complex shit */} -
- ); -}); diff --git a/my-app/src/views/ListView.jsx b/my-app/src/views/ListView.jsx index e69de29b..ca3488d5 100644 --- a/my-app/src/views/ListView.jsx +++ b/my-app/src/views/ListView.jsx @@ -0,0 +1,11 @@ +import React from 'react'; + +function ListView(props) { + return ( +
+ +
+ ); +} + +export default ListView; diff --git a/my-app/src/views/SidebarView.jsx b/my-app/src/views/SidebarView.jsx index 7170a7cc..70277044 100644 --- a/my-app/src/views/SidebarView.jsx +++ b/my-app/src/views/SidebarView.jsx @@ -1,12 +1,17 @@ +// src/views/SidebarView.jsx import React from 'react'; -function SidebarView(props){ +function SidebarView(props) { return ( -
- sdss +
+

Sidebar

+
    +
  • Home
  • +
  • Courses
  • +
  • About
  • +
); } -export default SidebarView; - +export default SidebarView; \ No newline at end of file diff --git a/my-app/vite.config.js b/my-app/vite.config.js index 3df309eb..9b51b3f4 100644 --- a/my-app/vite.config.js +++ b/my-app/vite.config.js @@ -2,7 +2,6 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import tailwindcss from '@tailwindcss/vite' -// https://vite.dev/config/ export default defineConfig({ plugins: [react(), tailwindcss(), From 2ad2b5234436319e553cf911d835a9ce80965571 Mon Sep 17 00:00:00 2001 From: Sami Al Saati Date: Fri, 28 Mar 2025 13:57:25 +0100 Subject: [PATCH 05/29] Main design layout --- ...Royal_Institute_of_Technology_logo.svg.png | Bin 0 -> 82608 bytes my-app/src/assets/react.svg | 1 - my-app/src/pages/App.jsx | 18 ++++++++-- my-app/src/views/SearchbarView.jsx | 34 +++++++++++------- my-app/src/views/SidebarView.jsx | 8 ++--- my-app/tailwind.config.js | 6 +++- 6 files changed, 46 insertions(+), 21 deletions(-) create mode 100644 my-app/src/assets/KTH_Royal_Institute_of_Technology_logo.svg.png delete mode 100644 my-app/src/assets/react.svg diff --git a/my-app/src/assets/KTH_Royal_Institute_of_Technology_logo.svg.png b/my-app/src/assets/KTH_Royal_Institute_of_Technology_logo.svg.png new file mode 100644 index 0000000000000000000000000000000000000000..cdd91cd9c1ef1f01d90142adeef7d12406873715 GIT binary patch literal 82608 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 literal 0 HcmV?d00001 diff --git a/my-app/src/assets/react.svg b/my-app/src/assets/react.svg deleted file mode 100644 index 6c87de9b..00000000 --- a/my-app/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/my-app/src/pages/App.jsx b/my-app/src/pages/App.jsx index 7741f101..cfc802d8 100644 --- a/my-app/src/pages/App.jsx +++ b/my-app/src/pages/App.jsx @@ -1,11 +1,23 @@ -// src/App.jsx import React from 'react'; import SidebarView from "../views/SidebarView.jsx"; +import SearchbarView from "../views/SearchBarView.jsx"; +import ListView from "../views/ListView.jsx"; function App() { return ( -
- +
+
+ +
+
+ +
+ +
+
+ +
+
); } diff --git a/my-app/src/views/SearchbarView.jsx b/my-app/src/views/SearchbarView.jsx index 1cf1d7db..d3d7ec3a 100644 --- a/my-app/src/views/SearchbarView.jsx +++ b/my-app/src/views/SearchbarView.jsx @@ -1,15 +1,25 @@ +import React from 'react'; -const SearchbarView = (props) =>{ - return
+function SearchbarView(props){ + return ( +

This is another view, where the presenter submits the text field to the Database

- - -
; + +
+ +
+ + + +
+ ); } + +export default SearchbarView; \ No newline at end of file diff --git a/my-app/src/views/SidebarView.jsx b/my-app/src/views/SidebarView.jsx index 70277044..7bb4ac6b 100644 --- a/my-app/src/views/SidebarView.jsx +++ b/my-app/src/views/SidebarView.jsx @@ -3,11 +3,11 @@ import React from 'react'; function SidebarView(props) { return ( -
+

Sidebar

-
    -
  • Home
  • -
  • Courses
  • +
      +
    • Home
    • +
    • Courses
    • About
diff --git a/my-app/tailwind.config.js b/my-app/tailwind.config.js index b49d3631..9b70c370 100644 --- a/my-app/tailwind.config.js +++ b/my-app/tailwind.config.js @@ -5,7 +5,11 @@ module.exports = { './src/**/*.{js,jsx,ts,tsx}', ], theme: { - extend: {}, + extend: { + colors: { + 'custom-blue': '#49347E', // Custom color name + }, + }, }, plugins: [], }; From e504235a33298dd50c12f9a381805716bd5ef444 Mon Sep 17 00:00:00 2001 From: kexana Date: Wed, 2 Apr 2025 09:11:02 +0200 Subject: [PATCH 06/29] initial sidebar --- my-app/src/pages/App.jsx | 9 ++-- .../CourseViewComponents/SampleComponent.jsx | 0 .../SideBarComponents/ButtonGroupField.jsx | 47 +++++++++++++++++++ .../SideBarComponents/ToggleField.jsx | 24 ++++++++++ .../SideBarComponents/UploadField.jsx | 21 +++++++++ my-app/src/views/CourseView.jsx | 5 ++ my-app/src/views/SidebarView.jsx | 26 +++++++--- 7 files changed, 121 insertions(+), 11 deletions(-) create mode 100644 my-app/src/views/Components/CourseViewComponents/SampleComponent.jsx create mode 100644 my-app/src/views/Components/SideBarComponents/ButtonGroupField.jsx create mode 100644 my-app/src/views/Components/SideBarComponents/ToggleField.jsx create mode 100644 my-app/src/views/Components/SideBarComponents/UploadField.jsx create mode 100644 my-app/src/views/CourseView.jsx diff --git a/my-app/src/pages/App.jsx b/my-app/src/pages/App.jsx index cfc802d8..be1f92d4 100644 --- a/my-app/src/pages/App.jsx +++ b/my-app/src/pages/App.jsx @@ -5,13 +5,14 @@ import ListView from "../views/ListView.jsx"; function App() { return ( -
-
+
+
+ {/* bg-gradient-to-t from-blue-900 via-cyan-600 to-lime-100 */}
-
+
-
+
diff --git a/my-app/src/views/Components/CourseViewComponents/SampleComponent.jsx b/my-app/src/views/Components/CourseViewComponents/SampleComponent.jsx new file mode 100644 index 00000000..e69de29b diff --git a/my-app/src/views/Components/SideBarComponents/ButtonGroupField.jsx b/my-app/src/views/Components/SideBarComponents/ButtonGroupField.jsx new file mode 100644 index 00000000..a2893e06 --- /dev/null +++ b/my-app/src/views/Components/SideBarComponents/ButtonGroupField.jsx @@ -0,0 +1,47 @@ +import React from 'react'; + +export default function ButtonGroupField(props) { + return ( +
+
+
+

Other filter

+
+
+

- filter description

+
+
+
    +
  • +
    + + +
    +
  • +
  • +
    + + +
    +
  • +
  • +
    + + +
    +
  • +
+
+ + + +
+
+ ); +} \ No newline at end of file diff --git a/my-app/src/views/Components/SideBarComponents/ToggleField.jsx b/my-app/src/views/Components/SideBarComponents/ToggleField.jsx new file mode 100644 index 00000000..1d927cd9 --- /dev/null +++ b/my-app/src/views/Components/SideBarComponents/ToggleField.jsx @@ -0,0 +1,24 @@ +import React from 'react'; + +export default function ToggleField(props) { + return ( +
+
+ +
+

One filter

+
+
+

- filter description

+
+
+
+ +
+
+ ); +} \ No newline at end of file diff --git a/my-app/src/views/Components/SideBarComponents/UploadField.jsx b/my-app/src/views/Components/SideBarComponents/UploadField.jsx new file mode 100644 index 00000000..45fe28ff --- /dev/null +++ b/my-app/src/views/Components/SideBarComponents/UploadField.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +export default function UploadField(props) { + return ( +
+
+ +
+

Describe how the Transcript upload works

+
+ ); +} \ No newline at end of file diff --git a/my-app/src/views/CourseView.jsx b/my-app/src/views/CourseView.jsx new file mode 100644 index 00000000..db041d5b --- /dev/null +++ b/my-app/src/views/CourseView.jsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function ToggleField(props) { + return (
); +} \ No newline at end of file diff --git a/my-app/src/views/SidebarView.jsx b/my-app/src/views/SidebarView.jsx index 7bb4ac6b..670565bd 100644 --- a/my-app/src/views/SidebarView.jsx +++ b/my-app/src/views/SidebarView.jsx @@ -1,15 +1,27 @@ // src/views/SidebarView.jsx import React from 'react'; +import UploadField from "./Components/SideBarComponents/UploadField.jsx"; +import ToggleField from "./Components/SidebarComponents/ToggleField.jsx"; +import ButtonGroupField from "./Components/SideBarComponents/ButtonGroupField.jsx"; function SidebarView(props) { return ( -
-

Sidebar

-
    -
  • Home
  • -
  • Courses
  • -
  • About
  • -
+
+ +
+
+
+ Filters +
+ + +
+ +
); } From 368096ff68f910b40b22a8f05833ed0c49e61344 Mon Sep 17 00:00:00 2001 From: kexana Date: Wed, 2 Apr 2025 09:26:33 +0200 Subject: [PATCH 07/29] courseView init --- my-app/src/pages/App.jsx | 4 +++ .../SideBarComponents/ButtonGroupField.jsx | 26 +++---------------- .../SideBarComponents/ToggleField.jsx | 10 +++++++ my-app/src/views/CourseView.jsx | 2 +- 4 files changed, 18 insertions(+), 24 deletions(-) diff --git a/my-app/src/pages/App.jsx b/my-app/src/pages/App.jsx index be1f92d4..16ecc252 100644 --- a/my-app/src/pages/App.jsx +++ b/my-app/src/pages/App.jsx @@ -2,6 +2,7 @@ import React from 'react'; import SidebarView from "../views/SidebarView.jsx"; import SearchbarView from "../views/SearchBarView.jsx"; import ListView from "../views/ListView.jsx"; +import CourseView from "../views/CourseView.jsx"; function App() { return ( @@ -18,6 +19,9 @@ function App() {
+
+ +
); diff --git a/my-app/src/views/Components/SideBarComponents/ButtonGroupField.jsx b/my-app/src/views/Components/SideBarComponents/ButtonGroupField.jsx index a2893e06..e1e13fe7 100644 --- a/my-app/src/views/Components/SideBarComponents/ButtonGroupField.jsx +++ b/my-app/src/views/Components/SideBarComponents/ButtonGroupField.jsx @@ -11,34 +11,14 @@ export default function ButtonGroupField(props) {

- filter description

-
    -
  • -
    - - -
    -
  • -
  • -
    - - -
    -
  • -
  • -
    - - -
    -
  • -
- - -
diff --git a/my-app/src/views/Components/SideBarComponents/ToggleField.jsx b/my-app/src/views/Components/SideBarComponents/ToggleField.jsx index 1d927cd9..1b837c8e 100644 --- a/my-app/src/views/Components/SideBarComponents/ToggleField.jsx +++ b/my-app/src/views/Components/SideBarComponents/ToggleField.jsx @@ -19,6 +19,16 @@ export default function ToggleField(props) { Apple
+
+ + + Apple +
); } \ No newline at end of file diff --git a/my-app/src/views/CourseView.jsx b/my-app/src/views/CourseView.jsx index db041d5b..44508f96 100644 --- a/my-app/src/views/CourseView.jsx +++ b/my-app/src/views/CourseView.jsx @@ -1,5 +1,5 @@ import React from 'react'; export default function ToggleField(props) { - return (
); + return (
dsufhsiufh
); } \ No newline at end of file From bda6d94f3884128bece27ff8cf9d84a5c2216f51 Mon Sep 17 00:00:00 2001 From: Kacper Lisik Date: Wed, 2 Apr 2025 14:06:43 +0200 Subject: [PATCH 08/29] the course page initial tests --- my-app/index.html | 21 ++++--- my-app/src/pages/App.jsx | 55 +++++++++++++++---- .../CourseViewComponents/ModalComponent.jsx | 30 ++++++++++ my-app/src/views/CourseView.jsx | 6 +- 4 files changed, 90 insertions(+), 22 deletions(-) create mode 100644 my-app/src/views/Components/CourseViewComponents/ModalComponent.jsx diff --git a/my-app/index.html b/my-app/index.html index dc6f3991..e33e4c2d 100644 --- a/my-app/index.html +++ b/my-app/index.html @@ -1,12 +1,15 @@ - - - - - - Fins my course - -
- + + + + + + Find my course + + +
+ + + diff --git a/my-app/src/pages/App.jsx b/my-app/src/pages/App.jsx index 16ecc252..f9572bd4 100644 --- a/my-app/src/pages/App.jsx +++ b/my-app/src/pages/App.jsx @@ -1,30 +1,65 @@ -import React from 'react'; +import React, { useState } from "react"; import SidebarView from "../views/SidebarView.jsx"; import SearchbarView from "../views/SearchBarView.jsx"; import ListView from "../views/ListView.jsx"; import CourseView from "../views/CourseView.jsx"; function App() { + const [isPopupOpen, setIsPopupOpen] = useState(false); + return (
+ {/* Purple Sidebar */}
- {/* bg-gradient-to-t from-blue-900 via-cyan-600 to-lime-100 */}
-
- -
+ + {/* Main Content Area */} +
+ Banner + +
-
+ +
-
-
- + + + {/* Popup only overlays the black container */} + {isPopupOpen && ( +
setIsPopupOpen(false)} + > + {/* Wider modal container */} +
e.stopPropagation()} + > + {/* Container for CourseView which fills available space */} +
+ {/* */} +
+ +
+
+ )} +
); } -export default App; \ No newline at end of file +export default App; diff --git a/my-app/src/views/Components/CourseViewComponents/ModalComponent.jsx b/my-app/src/views/Components/CourseViewComponents/ModalComponent.jsx new file mode 100644 index 00000000..e5f50f2b --- /dev/null +++ b/my-app/src/views/Components/CourseViewComponents/ModalComponent.jsx @@ -0,0 +1,30 @@ +// Modal.jsx +import React from 'react'; +import ReactDOM from 'react-dom'; + +const Modal = ({ isOpen, onClose, children }) => { + if (!isOpen) return null; + + return ReactDOM.createPortal( +
+
e.stopPropagation()} + > + {children} + +
+
, + document.getElementById('modal-root') + ); +}; + +export default Modal; diff --git a/my-app/src/views/CourseView.jsx b/my-app/src/views/CourseView.jsx index 44508f96..ec0266ac 100644 --- a/my-app/src/views/CourseView.jsx +++ b/my-app/src/views/CourseView.jsx @@ -1,5 +1,5 @@ import React from 'react'; -export default function ToggleField(props) { - return (
dsufhsiufh
); -} \ No newline at end of file +export default function CourseView(props) { + return (
); +} From 80c665a66691fee80f8f80f1ca15c21228e68d07 Mon Sep 17 00:00:00 2001 From: Elias Tosteberg Date: Wed, 2 Apr 2025 16:36:10 +0200 Subject: [PATCH 09/29] Make it run on my comuter --- my-app/package-lock.json | 600 +++++++++++++++++++++++++- my-app/package.json | 2 + my-app/src/pages/App.jsx | 2 +- my-app/src/views/PrerequisiteTree.jsx | 12 + my-app/src/views/SidebarView.jsx | 2 +- 5 files changed, 606 insertions(+), 12 deletions(-) create mode 100644 my-app/src/views/PrerequisiteTree.jsx diff --git a/my-app/package-lock.json b/my-app/package-lock.json index f33473f1..83bf1037 100644 --- a/my-app/package-lock.json +++ b/my-app/package-lock.json @@ -9,12 +9,14 @@ "version": "0.0.0", "dependencies": { "@tailwindcss/vite": "^4.0.17", + "autoprefixer": "^10.4.21", "firebase": "^11.5.0", "mobx": "^6.13.7", "mobx-react-lite": "^4.1.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-router-dom": "^7.4.0", + "reactflow": "^11.11.4", "tailwindcss": "^4.0.17" }, "devDependencies": { @@ -1698,6 +1700,108 @@ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "license": "BSD-3-Clause" }, + "node_modules/@reactflow/background": { + "version": "11.3.14", + "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz", + "integrity": "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/controls": { + "version": "11.2.14", + "resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.14.tgz", + "integrity": "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/core": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.4.tgz", + "integrity": "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==", + "license": "MIT", + "dependencies": { + "@types/d3": "^7.4.0", + "@types/d3-drag": "^3.0.1", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/minimap": { + "version": "11.7.14", + "resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.14.tgz", + "integrity": "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-resizer": { + "version": "2.2.14", + "resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz", + "integrity": "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.4", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-toolbar": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz", + "integrity": "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.37.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.37.0.tgz", @@ -2233,6 +2337,259 @@ "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", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz", + "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", @@ -2240,6 +2597,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2260,7 +2623,7 @@ "version": "19.0.12", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.12.tgz", "integrity": "sha512-V6Ar115dBDrjbtXSrS+/Oruobc+qVbbUxDFC1RSbRqLt5SYvxxyIDrSC85RWml54g+jfNeEMZhEj7wW07ONQhA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -2367,6 +2730,43 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2389,7 +2789,6 @@ "version": "4.24.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", - "dev": true, "funding": [ { "type": "opencollective", @@ -2432,7 +2831,6 @@ "version": "1.0.30001707", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz", "integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==", - "dev": true, "funding": [ { "type": "opencollective", @@ -2466,6 +2864,12 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -2540,9 +2944,114 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, + "devOptional": true, "license": "MIT" }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -2581,7 +3090,6 @@ "version": "1.5.126", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.126.tgz", "integrity": "sha512-AtH1uLcTC72LA4vfYcEJJkrMk/MY/X0ub8Hv7QGAePW2JkeUFHEL/QfS4J77R6M87Sss8O0OcqReSaN1bpyA+Q==", - "dev": true, "license": "ISC" }, "node_modules/emoji-regex": { @@ -2963,6 +3471,19 @@ "dev": true, "license": "ISC" }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3583,9 +4104,17 @@ "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "dev": true, "license": "MIT" }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -3703,6 +4232,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -3818,6 +4353,24 @@ "react-dom": ">=18" } }, + "node_modules/reactflow": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz", + "integrity": "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==", + "license": "MIT", + "dependencies": { + "@reactflow/background": "11.3.14", + "@reactflow/controls": "11.2.14", + "@reactflow/core": "11.11.4", + "@reactflow/minimap": "11.7.14", + "@reactflow/node-resizer": "2.2.14", + "@reactflow/node-toolbar": "1.3.14" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -4058,7 +4611,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "dev": true, "funding": [ { "type": "opencollective", @@ -4105,9 +4657,9 @@ } }, "node_modules/vite": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.3.tgz", - "integrity": "sha512-IzwM54g4y9JA/xAeBPNaDXiBF8Jsgl3VBQ2YQ/wOY6fyW3xMdSoltIV3Bo59DErdqdE6RxUfv8W69DvUorE4Eg==", + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.4.tgz", + "integrity": "sha512-veHMSew8CcRzhL5o8ONjy8gkfmFJAd5Ac16oxBUjlwgX3Gq2Wqr+qNC3TjPIpy7TPV/KporLga5GT9HqdrCizw==", "license": "MIT", "dependencies": { "esbuild": "^0.25.0", @@ -4302,6 +4854,34 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zustand": { + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz", + "integrity": "sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } } } } diff --git a/my-app/package.json b/my-app/package.json index 4602e0f8..26653f3c 100644 --- a/my-app/package.json +++ b/my-app/package.json @@ -11,12 +11,14 @@ }, "dependencies": { "@tailwindcss/vite": "^4.0.17", + "autoprefixer": "^10.4.21", "firebase": "^11.5.0", "mobx": "^6.13.7", "mobx-react-lite": "^4.1.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-router-dom": "^7.4.0", + "reactflow": "^11.11.4", "tailwindcss": "^4.0.17" }, "devDependencies": { diff --git a/my-app/src/pages/App.jsx b/my-app/src/pages/App.jsx index f9572bd4..acc5986a 100644 --- a/my-app/src/pages/App.jsx +++ b/my-app/src/pages/App.jsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import SidebarView from "../views/SidebarView.jsx"; -import SearchbarView from "../views/SearchBarView.jsx"; +import SearchbarView from "../views/SearchbarView.jsx"; import ListView from "../views/ListView.jsx"; import CourseView from "../views/CourseView.jsx"; diff --git a/my-app/src/views/PrerequisiteTree.jsx b/my-app/src/views/PrerequisiteTree.jsx new file mode 100644 index 00000000..70654cc5 --- /dev/null +++ b/my-app/src/views/PrerequisiteTree.jsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { ReactFlow, Background, Controls } from '@xyflow/react'; + +function PrerequisiteTree(props) { + return ( +
+ +
+ ); +} + +export default PrerequisiteTree; \ No newline at end of file diff --git a/my-app/src/views/SidebarView.jsx b/my-app/src/views/SidebarView.jsx index 670565bd..80336cd3 100644 --- a/my-app/src/views/SidebarView.jsx +++ b/my-app/src/views/SidebarView.jsx @@ -1,7 +1,7 @@ // src/views/SidebarView.jsx import React from 'react'; import UploadField from "./Components/SideBarComponents/UploadField.jsx"; -import ToggleField from "./Components/SidebarComponents/ToggleField.jsx"; +import ToggleField from "./Components/SideBarComponents/ToggleField.jsx"; import ButtonGroupField from "./Components/SideBarComponents/ButtonGroupField.jsx"; function SidebarView(props) { From 6d271401699057ace90f5822887aeb2c4816b0b5 Mon Sep 17 00:00:00 2001 From: Elias Tosteberg Date: Thu, 3 Apr 2025 16:45:19 +0200 Subject: [PATCH 10/29] Tree without database integration --- my-app/src/pages/App.jsx | 4 + .../PrerequisiteTreeComponents/BoxTest.jsx | 11 +++ my-app/src/views/PrerequisiteTree.jsx | 82 +++++++++++++++++-- 3 files changed, 90 insertions(+), 7 deletions(-) create mode 100644 my-app/src/views/Components/PrerequisiteTreeComponents/BoxTest.jsx diff --git a/my-app/src/pages/App.jsx b/my-app/src/pages/App.jsx index acc5986a..11a535dc 100644 --- a/my-app/src/pages/App.jsx +++ b/my-app/src/pages/App.jsx @@ -3,6 +3,7 @@ import SidebarView from "../views/SidebarView.jsx"; import SearchbarView from "../views/SearchbarView.jsx"; import ListView from "../views/ListView.jsx"; import CourseView from "../views/CourseView.jsx"; +import PrerequisiteTree from "../views/PrerequisiteTree.jsx"; function App() { const [isPopupOpen, setIsPopupOpen] = useState(false); @@ -21,6 +22,9 @@ function App() {
+
+ +
diff --git a/my-app/src/views/Components/PrerequisiteTreeComponents/BoxTest.jsx b/my-app/src/views/Components/PrerequisiteTreeComponents/BoxTest.jsx new file mode 100644 index 00000000..c008efc1 --- /dev/null +++ b/my-app/src/views/Components/PrerequisiteTreeComponents/BoxTest.jsx @@ -0,0 +1,11 @@ +import React from 'react'; + +function PrerequisiteTree(props) { + return ( +
+ +
+ ); +} + +export default PrerequisiteTree; \ No newline at end of file diff --git a/my-app/src/views/PrerequisiteTree.jsx b/my-app/src/views/PrerequisiteTree.jsx index 70654cc5..5634abef 100644 --- a/my-app/src/views/PrerequisiteTree.jsx +++ b/my-app/src/views/PrerequisiteTree.jsx @@ -1,12 +1,80 @@ -import React from 'react'; -import { ReactFlow, Background, Controls } from '@xyflow/react'; +import React from "react"; +import ReactFlow, { MiniMap, Controls, Background } from "reactflow"; +import "reactflow/dist/style.css"; -function PrerequisiteTree(props) { - return ( -
+//contains the nodes and lines +const initialNodes = []; +const initialEdges = []; +//keeps track of how deep in each column we are. +let depth = []; + + +//Funktion that takes a node and its children and adds them to the tree. +function addChildNodes(origin, preR, leftOfset,) { + if (depth.length == leftOfset) { + depth.push(0); + } + + + let ofsett = 0; + for (let k = 0; k < preR.length; k++) { + let temp = preR[k]; + let cur = ""; + + //Adds the "or" node when needed + if (temp.length > 1) { + cur = "or " + origin + " " + k; + initialNodes.push({ id: "or " + origin + " " + k, data: { label: "or" }, position: { x: leftOfset * 400 + 200, y: ofsett + depth[leftOfset] }, sourcePosition: "left", targetPosition: "right" }); + initialEdges.push({ id: origin + "-" + "or " + origin + " " + k, source: "or " + origin + " " + k, target: origin, markerEnd: { type: "arrow" }, type: "default" },); + } else { + cur = origin; + } + + + //Adds a node + for (let i = 0; i < temp.length; i++) { + initialNodes.push({ id: temp[i], data: { label: temp[i] }, position: { x: leftOfset * 400 + 400, y: ofsett + depth[leftOfset] }, sourcePosition: "left", targetPosition: "right" }); + initialEdges.push({ id: cur + "-" + temp[i], source: temp[i], target: cur, markerEnd: { type: "arrow" }, type: "default" },); + ofsett += 50; + } + ofsett += 50; + } + depth[leftOfset] += ofsett; +} + +function loadTree(course) { + initialNodes.push({ id: course, data: { label: course }, position: { x: 0, y: 0 }, sourcePosition: "left", targetPosition: "right" }); + let column = 0; + + let start = "SF2526"; + let preR2 = [["DD1320"], ["SF1544", "SF1545"]]; + addChildNodes(start, preR2, column); + column++; + + start = "DD1320"; + preR2 = [["SF1546"], ["SF1000", "SF1001", "SF1002"], ["SF1003", "SF1004"]]; + addChildNodes(start, preR2, column); + + start = "SF1544"; + preR2 = [["SF1005"], ["SF1006", "SF1007"]]; + addChildNodes(start, preR2, column); + column++; + +} + +const PrerequisiteTree = () => { + loadTree("SF2526"); + + return ( +
+ + + + +
); -} +}; -export default PrerequisiteTree; \ No newline at end of file +export default PrerequisiteTree; From b9fbf6b9921e768fa65629738b3feef0950e1716 Mon Sep 17 00:00:00 2001 From: Elias Tosteberg Date: Fri, 4 Apr 2025 16:53:15 +0200 Subject: [PATCH 11/29] fixes --- my-app/src/views/PrerequisiteTree.jsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/my-app/src/views/PrerequisiteTree.jsx b/my-app/src/views/PrerequisiteTree.jsx index 5634abef..a0453c80 100644 --- a/my-app/src/views/PrerequisiteTree.jsx +++ b/my-app/src/views/PrerequisiteTree.jsx @@ -25,8 +25,8 @@ function addChildNodes(origin, preR, leftOfset,) { //Adds the "or" node when needed if (temp.length > 1) { cur = "or " + origin + " " + k; - initialNodes.push({ id: "or " + origin + " " + k, data: { label: "or" }, position: { x: leftOfset * 400 + 200, y: ofsett + depth[leftOfset] }, sourcePosition: "left", targetPosition: "right" }); - initialEdges.push({ id: origin + "-" + "or " + origin + " " + k, source: "or " + origin + " " + k, target: origin, markerEnd: { type: "arrow" }, type: "default" },); + initialNodes.push({ id: "or " + origin + " " + k, data: { label: "One of" }, position: { x: leftOfset * 400 + 300, y: ofsett + depth[leftOfset] }, style: { width: 60 }, sourcePosition: "left", targetPosition: "right" }); + initialEdges.push({ id: origin + "-" + "or " + origin + " " + k, source: "or " + origin + " " + k, target: origin, markerEnd: { type: "line", color: "black" }, type: "default", style: { stroke: "black" } }); } else { cur = origin; } @@ -35,14 +35,15 @@ function addChildNodes(origin, preR, leftOfset,) { //Adds a node for (let i = 0; i < temp.length; i++) { initialNodes.push({ id: temp[i], data: { label: temp[i] }, position: { x: leftOfset * 400 + 400, y: ofsett + depth[leftOfset] }, sourcePosition: "left", targetPosition: "right" }); - initialEdges.push({ id: cur + "-" + temp[i], source: temp[i], target: cur, markerEnd: { type: "arrow" }, type: "default" },); + initialEdges.push({ id: cur + "-" + temp[i], source: temp[i], target: cur, markerEnd: { type: "line", color: "black" }, type: "default", style: { stroke: "black" } }); ofsett += 50; } - ofsett += 50; + ofsett += 60; } depth[leftOfset] += ofsett; } + function loadTree(course) { initialNodes.push({ id: course, data: { label: course }, position: { x: 0, y: 0 }, sourcePosition: "left", targetPosition: "right" }); let column = 0; From 2f640541aecab389d552d36f10218dc6aaafef74 Mon Sep 17 00:00:00 2001 From: Elias Tosteberg Date: Wed, 9 Apr 2025 11:11:02 +0200 Subject: [PATCH 12/29] npm update --- my-app/package-lock.json | 30 ++++++++++++++++++++++++------ my-app/package.json | 1 - 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/my-app/package-lock.json b/my-app/package-lock.json index 28b04d75..62a5712e 100644 --- a/my-app/package-lock.json +++ b/my-app/package-lock.json @@ -18,6 +18,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-router-dom": "^7.4.0", + "reactflow": "^11.11.4", "tailwindcss": "^4.0.17" }, "devDependencies": { @@ -2528,12 +2529,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/geojson": { - "version": "7946.0.16", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", - "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", - "license": "MIT" - }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2721,6 +2716,7 @@ "version": "4.24.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "dev": true, "funding": [ { "type": "opencollective", @@ -2763,6 +2759,7 @@ "version": "1.0.30001707", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz", "integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==", + "dev": true, "funding": [ { "type": "opencollective", @@ -3022,6 +3019,7 @@ "version": "1.5.126", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.126.tgz", "integrity": "sha512-AtH1uLcTC72LA4vfYcEJJkrMk/MY/X0ub8Hv7QGAePW2JkeUFHEL/QfS4J77R6M87Sss8O0OcqReSaN1bpyA+Q==", + "dev": true, "license": "ISC" }, "node_modules/emoji-regex": { @@ -4043,6 +4041,7 @@ "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, "license": "MIT" }, "node_modules/normalize-range": { @@ -4306,6 +4305,24 @@ "react-dom": ">=18" } }, + "node_modules/reactflow": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz", + "integrity": "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==", + "license": "MIT", + "dependencies": { + "@reactflow/background": "11.3.14", + "@reactflow/controls": "11.2.14", + "@reactflow/core": "11.11.4", + "@reactflow/minimap": "11.7.14", + "@reactflow/node-resizer": "2.2.14", + "@reactflow/node-toolbar": "1.3.14" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -4546,6 +4563,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, "funding": [ { "type": "opencollective", diff --git a/my-app/package.json b/my-app/package.json index 76120ecc..1c11807d 100644 --- a/my-app/package.json +++ b/my-app/package.json @@ -21,7 +21,6 @@ "react-dom": "^19.0.0", "react-router-dom": "^7.4.0", "reactflow": "^11.11.4", - "tailwindcss": "^4.0.17" }, "devDependencies": { From dc1d23386c345fa54889a6523637732329ffe4c0 Mon Sep 17 00:00:00 2001 From: Elias Tosteberg Date: Wed, 9 Apr 2025 11:36:29 +0200 Subject: [PATCH 13/29] add to page --- my-app/src/views/CourseView.jsx | 37 ++++----------------------- my-app/src/views/PrerequisiteTree.jsx | 5 +++- 2 files changed, 9 insertions(+), 33 deletions(-) diff --git a/my-app/src/views/CourseView.jsx b/my-app/src/views/CourseView.jsx index 9debafb5..953d8699 100644 --- a/my-app/src/views/CourseView.jsx +++ b/my-app/src/views/CourseView.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import PrerequisiteTree from "./PrerequisiteTree.jsx"; export default function CourseView(props) { return ( @@ -9,38 +10,10 @@ export default function CourseView(props) {
{/* Description Section */} -
+

- The course is an introduction to networking, protocols and communication. - - We study how large international networks are constructed from the individual computers, via local area, city and national networks. We use the Internet as or working example of such a network. The aim of the course is to give insights into both the theory and practice of the area. - We study how large international networks are constructed from the individual computers, via local area, city and national networks. We use the Internet as or working example of such a network. The aim of the course is to give insights into both the theory and practice of the area. - We study how large international networks are constructed from the individual computers, via local area, city and national networks. We use the Internet as or working example of such a network. The aim of the course is to give insights into both the theory and practice of the area. - We study how large international networks are constructed from the individual computers, via local area, city and national networks. We use the Internet as or working example of such a network. The aim of the course is to give insights into both the theory and practice of the area. - We study how large international networks are constructed from the individual computers, via local area, city and national networks. We use the Internet as or working example of such a network. The aim of the course is to give insights into both the theory and practice of the area. - We study how large international networks are constructed from the individual computers, via local area, city and national networks. We use the Internet as or working example of such a network. The aim of the course is to give insights into both the theory and practice of the area. - We study how large international networks are constructed from the individual computers, via local area, city and national networks. We use the Internet as or working example of such a network. The aim of the course is to give insights into both the theory and practice of the area. - We study how large international networks are constructed from the individual computers, via local area, city and national networks. We use the Internet as or working example of such a network. The aim of the course is to give insights into both the theory and practice of the area. - We study how large international networks are constructed from the individual computers, via local area, city and national networks. We use the Internet as or working example of such a network. The aim of the course is to give insights into both the theory and practice of the area. - We study how large international networks are constructed from the individual computers, via local area, city and national networks. We use the Internet as or working example of such a network. The aim of the course is to give insights into both the theory and practice of the area. - We study how large international networks are constructed from the individual computers, via local area, city and national networks. We use the Internet as or working example of such a network. The aim of the course is to give insights into both the theory and practice of the area. - We study how large international networks are constructed from the individual computers, via local area, city and national networks. We use the Internet as or working example of such a network. The aim of the course is to give insights into both the theory and practice of the area. - We study how large international networks are constructed from the individual computers, via local area, city and national networks. We use the Internet as or working example of such a network. The aim of the course is to give insights into both the theory and practice of the area. - We study how large international networks are constructed from the individual computers, via local area, city and national networks. We use the Internet as or working example of such a network. The aim of the course is to give insights into both the theory and practice of the area. - We study how large international networks are constructed from the individual computers, via local area, city and national networks. We use the Internet as or working example of such a network. The aim of the course is to give insights into both the theory and practice of the area. - We study how large international networks are constructed from the individual computers, via local area, city and national networks. We use the Internet as or working example of such a network. The aim of the course is to give insights into both the theory and practice of the area. - We study how large international networks are constructed from the individual computers, via local area, city and national networks. We use the Internet as or working example of such a network. The aim of the course is to give insights into both the theory and practice of the area. - We study how large international networks are constructed from the individual computers, via local area, city and national networks. We use the Internet as or working example of such a network. The aim of the course is to give insights into both the theory and practice of the area. - We study how large international networks are constructed from the individual computers, via local area, city and national networks. We use the Internet as or working example of such a network. The aim of the course is to give insights into both the theory and practice of the area. - We study how large international networks are constructed from the individual computers, via local area, city and national networks. We use the Internet as or working example of such a network. The aim of the course is to give insights into both the theory and practice of the area. - We study how large international networks are constructed from the individual computers, via local area, city and national networks. We use the Internet as or working example of such a network. The aim of the course is to give insights into both the theory and practice of the area. - We study how large international networks are constructed from the individual computers, via local area, city and national networks. We use the Internet as or working example of such a network. The aim of the course is to give insights into both the theory and practice of the area. - We study how large international networks are constructed from the individual computers, via local area, city and national networks. We use the Internet as or working example of such a network. The aim of the course is to give insights into both the theory and practice of the area. - We study how large international networks are constructed from the individual computers, via local area, city and national networks. We use the Internet as or working example of such a network. The aim of the course is to give insights into both the theory and practice of the area. - We study how large international networks are constructed from the individual computers, via local area, city and national networks. We use the Internet as or working example of such a network. The aim of the course is to give insights into both the theory and practice of the area. - We study how large international networks are constructed from the individual computers, via local area, city and national networks. We use the Internet as or working example of such a network. The aim of the course is to give insights into both the theory and practice of the area. - -The focus of the course is on the protocols and algorithms used, and we will follow how they are used and implemented into the TCP/IP-stack - the basis of the Internet.

+ The course is an introduction to networking, protocols and communication. + The focus of the course is on the protocols and algorithms used
{/* Reviews Section */} @@ -54,7 +27,7 @@ The focus of the course is on the protocols and algorithms used, and we will fol

Prerequisite Graph Tree

{/* Placeholder for graph tree */} -

Graph tree or prerequisite info will go here...

+
); diff --git a/my-app/src/views/PrerequisiteTree.jsx b/my-app/src/views/PrerequisiteTree.jsx index a0453c80..06b74161 100644 --- a/my-app/src/views/PrerequisiteTree.jsx +++ b/my-app/src/views/PrerequisiteTree.jsx @@ -65,7 +65,10 @@ function loadTree(course) { } const PrerequisiteTree = () => { - loadTree("SF2526"); + if (initialNodes.length == 0){ + loadTree("SF2526"); + } + return (
From e235a7a5f275ba2d286c4014000811476412d30c Mon Sep 17 00:00:00 2001 From: Elias Tosteberg Date: Wed, 9 Apr 2025 14:12:52 +0200 Subject: [PATCH 14/29] Biiiiig Refactoring --- my-app/src/pages/App.jsx | 1 - my-app/src/presenters/ListViewPresenter.jsx | 16 ++- .../PrerequisitePresenter.jsx} | 32 ++---- .../src/views/Components/CoursePagePopup.jsx | 53 ++++++++- my-app/src/views/CourseView.jsx | 107 +++++++++--------- my-app/src/views/ListView.jsx | 16 +-- my-app/src/views/PrerequisiteTreeView.jsx | 17 +++ 7 files changed, 148 insertions(+), 94 deletions(-) rename my-app/src/{views/PrerequisiteTree.jsx => presenters/PrerequisitePresenter.jsx} (79%) create mode 100644 my-app/src/views/PrerequisiteTreeView.jsx diff --git a/my-app/src/pages/App.jsx b/my-app/src/pages/App.jsx index aa7aee09..a0c0df32 100644 --- a/my-app/src/pages/App.jsx +++ b/my-app/src/pages/App.jsx @@ -2,7 +2,6 @@ import React, { useState } from 'react'; import { SidebarPresenter } from "../presenters/SidebarPresenter.jsx"; import { SearchbarPresenter } from "../presenters/SearchbarPresenter.jsx"; import { ListViewPresenter } from '../presenters/ListViewPresenter.jsx'; -import CourseView from '../views/CourseView.jsx'; import {model} from '/src/model.js'; function App() { diff --git a/my-app/src/presenters/ListViewPresenter.jsx b/my-app/src/presenters/ListViewPresenter.jsx index 895df5ec..d51fbff7 100644 --- a/my-app/src/presenters/ListViewPresenter.jsx +++ b/my-app/src/presenters/ListViewPresenter.jsx @@ -1,8 +1,17 @@ import React from 'react'; -import { observer } from "mobx-react-lite"; +import { observer} from "mobx-react-lite"; +import { useState } from 'react'; import ListView from "../views/ListView.jsx"; +import CoursePagePopup from '../views/Components/CoursePagePopup.jsx'; +import PrerequisitePresenter from './PrerequisitePresenter.jsx'; const ListViewPresenter = observer(({ model }) => { + + const [isPopupOpen, setIsPopupOpen] = useState(false); + const [selectedCourse, setSelectedCourse] = useState(null); + const preP = + const popup = setIsPopupOpen(false)} course={selectedCourse} prerequisiteTree={preP}/> + const addFavourite = (course) => { model.addFavourite(course); } @@ -16,6 +25,11 @@ const ListViewPresenter = observer(({ model }) => { favouriteCourses={model.favourites} addFavourite={addFavourite} removeFavourite={removeFavourite} + isPopupOpen={isPopupOpen} + setIsPopupOpen={setIsPopupOpen} + setSelectedCourse={setSelectedCourse} + popUp={popup} + />; }); diff --git a/my-app/src/views/PrerequisiteTree.jsx b/my-app/src/presenters/PrerequisitePresenter.jsx similarity index 79% rename from my-app/src/views/PrerequisiteTree.jsx rename to my-app/src/presenters/PrerequisitePresenter.jsx index 06b74161..8d486a62 100644 --- a/my-app/src/views/PrerequisiteTree.jsx +++ b/my-app/src/presenters/PrerequisitePresenter.jsx @@ -1,8 +1,8 @@ -import React from "react"; -import ReactFlow, { MiniMap, Controls, Background } from "reactflow"; -import "reactflow/dist/style.css"; - +import { observer } from "mobx-react-lite"; +import PrerequisiteTreeView from "../views/PrerequisiteTreeView"; +export const PrerequisitePresenter = observer((props) => { + //contains the nodes and lines const initialNodes = []; const initialEdges = []; @@ -61,24 +61,12 @@ function loadTree(course) { preR2 = [["SF1005"], ["SF1006", "SF1007"]]; addChildNodes(start, preR2, column); column++; - } + loadTree(props.selectedCourse.code); + + return +}); + +export default PrerequisitePresenter; -const PrerequisiteTree = () => { - if (initialNodes.length == 0){ - loadTree("SF2526"); - } - - return ( -
- - - - - -
- ); -}; - -export default PrerequisiteTree; diff --git a/my-app/src/views/Components/CoursePagePopup.jsx b/my-app/src/views/Components/CoursePagePopup.jsx index 8a5239bf..8ca557f6 100644 --- a/my-app/src/views/Components/CoursePagePopup.jsx +++ b/my-app/src/views/Components/CoursePagePopup.jsx @@ -1,7 +1,6 @@ import React from 'react'; -import CourseView from '../CourseView'; -function CoursePagePopup({ isOpen, onClose, course }) { +function CoursePagePopup({ isOpen, onClose, course, prerequisiteTree }) { if (!isOpen || !course) return null; // Don't render if not open or course not selected return ( @@ -14,7 +13,55 @@ function CoursePagePopup({ isOpen, onClose, course }) { onClick={(e) => e.stopPropagation()} >
- +
+ {/* Course Title Section */} +
+

+ {course.code} - {course.name} + ({course.credits} Credits) {/* Display Credits */} +

+
+
+ +
+ + {/* Description Section */} +
+

Course Description

+
+
+ + {/* Prerequisite Graph Tree Section */} +
+

Prerequisite Graph Tree

+

Graph tree or prerequisite info will go here...

+
+ {/* Reviews Section */} +
+

Reviews

+

Here would be some reviews of the course...

+
+ + {/* Prerequisite Graph Tree Section */} +
+

Prerequisite Graph Tree

+ {/* Placeholder for graph tree */} + {prerequisiteTree} +
+
-
+// return ( +//
+// {/* Course Title Section */} +//
+//

+// {course.code} - {course.name} +// ({course.credits} Credits) {/* Display Credits */} +//

+//
+//
+// +//
- {/* Description Section */} -
-

Course Description

-
-
+// {/* Description Section */} +//
+//

Course Description

+//
+//
- {/* Prerequisite Graph Tree Section */} -
-

Prerequisite Graph Tree

-

Graph tree or prerequisite info will go here...

-
- {/* Reviews Section */} -
-

Reviews

-

Here would be some reviews of the course...

-
+// {/* Prerequisite Graph Tree Section */} +//
+//

Prerequisite Graph Tree

+//

Graph tree or prerequisite info will go here...

+//
+// {/* Reviews Section */} +//
+//

Reviews

+//

Here would be some reviews of the course...

+//
- {/* Prerequisite Graph Tree Section */} -
-

Prerequisite Graph Tree

- {/* Placeholder for graph tree */} - -
-
- ); -} +// {/* Prerequisite Graph Tree Section */} +//
+//

Prerequisite Graph Tree

+// {/* Placeholder for graph tree */} +//
+//
+// ); +// } diff --git a/my-app/src/views/ListView.jsx b/my-app/src/views/ListView.jsx index 330e20ab..de90c051 100644 --- a/my-app/src/views/ListView.jsx +++ b/my-app/src/views/ListView.jsx @@ -1,15 +1,10 @@ import React, { useState } from 'react'; import { Quantum } from 'ldrs/react'; import 'ldrs/react/Quantum.css'; -import CoursePagePopupComponent from '../views/Components/CoursePagePopup.jsx'; - function ListView(props) { const coursesToDisplay = props.searchResults.length > 0 ? props.searchResults : props.courses; const [readMoreState, setReadMoreState] = useState({}); - const [isPopupOpen, setIsPopupOpen] = useState(false); - const [selectedCourse, setSelectedCourse] = useState(null); - const toggleReadMore = (courseCode) => { setReadMoreState(prevState => ( @@ -33,8 +28,8 @@ function ListView(props) {
{ console.log('Clicked:', course); // check browser console - setSelectedCourse(course); - setIsPopupOpen(true); + props.setSelectedCourse(course); + props.setIsPopupOpen(true); }} key={course.code} className="p-5 hover:bg-blue-100 flex items-center border border-b-black border-solid w-full rounded-lg cursor-pointer" @@ -91,12 +86,7 @@ function ListView(props) { color="#000061" /> )} - setIsPopupOpen(false)} - - course={selectedCourse} - /> + {props.popUp}
); } diff --git a/my-app/src/views/PrerequisiteTreeView.jsx b/my-app/src/views/PrerequisiteTreeView.jsx new file mode 100644 index 00000000..849a2ad8 --- /dev/null +++ b/my-app/src/views/PrerequisiteTreeView.jsx @@ -0,0 +1,17 @@ +import React from "react"; +import ReactFlow, { MiniMap, Controls, Background } from "reactflow"; +import "reactflow/dist/style.css"; + +function PrerequisiteTreeView(props){ + return ( +
+ + + + + +
+ ); +}; + +export default PrerequisiteTreeView; From 266e07946ae9d8952368ca12af4e24aecd62ed2d Mon Sep 17 00:00:00 2001 From: Elias Tosteberg Date: Fri, 11 Apr 2025 08:18:06 +0200 Subject: [PATCH 15/29] Broken version of prereq tree --- my-app/package-lock.json | 309 +++++++++++++++ my-app/package.json | 2 + .../src/presenters/PrerequisitePresenter.jsx | 352 +++++++++++++++--- 3 files changed, 615 insertions(+), 48 deletions(-) diff --git a/my-app/package-lock.json b/my-app/package-lock.json index 62a5712e..4004c738 100644 --- a/my-app/package-lock.json +++ b/my-app/package-lock.json @@ -8,7 +8,9 @@ "name": "my-app", "version": "0.0.0", "dependencies": { + "@dagrejs/dagre": "^1.1.4", "@tailwindcss/vite": "^4.0.17", + "@xyflow/react": "^12.5.5", "autoprefixer": "^10.4.21", "firebase": "^11.5.0", "ldrs": "^1.1.6", @@ -330,6 +332,24 @@ "node": ">=6.9.0" } }, + "node_modules/@dagrejs/dagre": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@dagrejs/dagre/-/dagre-1.1.4.tgz", + "integrity": "sha512-QUTc54Cg/wvmlEUxB+uvoPVKFazM1H18kVHBQNmK2NbrDR5ihOCR6CXLnDSZzMcSQKJtabPUWridBOlJM3WkDg==", + "license": "MIT", + "dependencies": { + "@dagrejs/graphlib": "2.2.4" + } + }, + "node_modules/@dagrejs/graphlib": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@dagrejs/graphlib/-/graphlib-2.2.4.tgz", + "integrity": "sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw==", + "license": "MIT", + "engines": { + "node": ">17.0.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz", @@ -2522,6 +2542,259 @@ "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", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz", + "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", @@ -2529,6 +2802,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2585,6 +2864,36 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, + "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==", + "license": "MIT", + "dependencies": { + "@xyflow/system": "0.0.55", + "classcat": "^5.0.3", + "zustand": "^4.4.0" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "@types/d3-drag": "^3.0.7", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + } + }, "node_modules/acorn": { "version": "8.14.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", diff --git a/my-app/package.json b/my-app/package.json index 1c11807d..5bdaf4c1 100644 --- a/my-app/package.json +++ b/my-app/package.json @@ -10,7 +10,9 @@ "preview": "vite preview" }, "dependencies": { + "@dagrejs/dagre": "^1.1.4", "@tailwindcss/vite": "^4.0.17", + "@xyflow/react": "^12.5.5", "autoprefixer": "^10.4.21", "firebase": "^11.5.0", "ldrs": "^1.1.6", diff --git a/my-app/src/presenters/PrerequisitePresenter.jsx b/my-app/src/presenters/PrerequisitePresenter.jsx index 8d486a62..3cd1d4aa 100644 --- a/my-app/src/presenters/PrerequisitePresenter.jsx +++ b/my-app/src/presenters/PrerequisitePresenter.jsx @@ -1,70 +1,326 @@ import { observer } from "mobx-react-lite"; import PrerequisiteTreeView from "../views/PrerequisiteTreeView"; +import dagre from '@dagrejs/dagre'; +import { useCallback } from "react"; + +import { + Background, + ReactFlow, + addEdge, + ConnectionLineType, + useNodesState, + useEdgesState, +} from '@xyflow/react'; +import '@xyflow/react/dist/style.css'; + + export const PrerequisitePresenter = observer((props) => { - -//contains the nodes and lines -const initialNodes = []; -const initialEdges = []; -//keeps track of how deep in each column we are. -let depth = []; - - -//Funktion that takes a node and its children and adds them to the tree. -function addChildNodes(origin, preR, leftOfset,) { - if (depth.length == leftOfset) { - depth.push(0); - } - let ofsett = 0; - for (let k = 0; k < preR.length; k++) { - let temp = preR[k]; - let cur = ""; + let uniqueCounter = 0; + //let toAdd = []; + + const position = { x: 0, y: 0 }; + const edgeType = 'smoothstep'; + + const initialNodes = [ + /* { + id: '1', + type: 'input', + data: { label: 'input' }, + position, + }, + { + id: '2', + data: { label: 'node 2' }, + position, + }, + { + id: '2a', + data: { label: 'node 2a' }, + position, + }, + { + id: '2b', + data: { label: 'node 2b' }, + position, + }, + { + id: '2c', + data: { label: 'node 2c' }, + position, + }, + { + id: '2d', + data: { label: 'node 2d' }, + position, + }, + { + id: '3', + data: { label: 'node 3' }, + position, + }, + { + id: '4', + data: { label: 'node 4' }, + position, + }, + { + id: '5', + data: { label: 'node 5' }, + position, + }, + { + id: '6', + type: 'output', + data: { label: 'output' }, + position, + }, + { id: '7', type: 'output', data: { label: 'output' }, position }, */ + ]; + + const initialEdges = [ + /* { id: 'e12', source: '1', target: '2', type: edgeType, animated: true }, + { id: 'e13', source: '1', target: '3', type: edgeType, animated: true }, + { id: 'e22a', source: '2', target: '2a', type: edgeType, animated: true }, + { id: 'e22b', source: '2', target: '2b', type: edgeType, animated: true }, + { id: 'e22c', source: '2', target: '2c', type: edgeType, animated: true }, + { id: 'e2c2d', source: '2c', target: '2d', type: edgeType, animated: true }, + { id: 'e45', source: '4', target: '5', type: edgeType, animated: true }, + { id: 'e56', source: '5', target: '6', type: edgeType, animated: true }, + { id: 'e57', source: '5', target: '7', type: edgeType, animated: true }, */ + ]; + + const dagreGraph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})); + + const nodeWidth = 172; + const nodeHeight = 36; + + + + + + const getLayoutedElements = (nodes, edges, direction = 'LR') => { + const isHorizontal = direction === 'LR'; + dagreGraph.setGraph({ rankdir: direction }); + + nodes.forEach((node) => { + dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight }); + }); + + edges.forEach((edge) => { + dagreGraph.setEdge(edge.source, edge.target); + }); + + dagre.layout(dagreGraph); + + const newNodes = nodes.map((node) => { + const nodeWithPosition = dagreGraph.node(node.id); + return { + ...node, + targetPosition: isHorizontal ? 'left' : 'top', + sourcePosition: isHorizontal ? 'right' : 'bottom', + position: { + x: nodeWithPosition.x - nodeWidth / 2, + y: nodeWithPosition.y - nodeHeight / 2, + }, + }; + }); + + return { nodes: newNodes, edges }; + }; + + const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements( + initialNodes, + initialEdges, + 'LR' // force horizontal layout initially + ); + + + const Flow = () => { + + loadTree(); + console.log("arived") + + + const [nodes, setNodes, onNodesChange] = useNodesState(layoutedNodes); + const [edges, setEdges, onEdgesChange] = useEdgesState(layoutedEdges); + + const onConnect = useCallback( + (params) => + setEdges((eds) => + addEdge( + { ...params, type: ConnectionLineType.SmoothStep, animated: true }, + eds + ) + ), + [] + ); + + return ( +
+ + + + +
+ + ); + }; + + - //Adds the "or" node when needed - if (temp.length > 1) { - cur = "or " + origin + " " + k; - initialNodes.push({ id: "or " + origin + " " + k, data: { label: "One of" }, position: { x: leftOfset * 400 + 300, y: ofsett + depth[leftOfset] }, style: { width: 60 }, sourcePosition: "left", targetPosition: "right" }); - initialEdges.push({ id: origin + "-" + "or " + origin + " " + k, source: "or " + origin + " " + k, target: origin, markerEnd: { type: "line", color: "black" }, type: "default", style: { stroke: "black" } }); + + + + + + function containsArr(arr) { + let bool = false + arr.forEach(element => { + if (Array.isArray(element)) { + bool = true; + } + }); + return bool; + } + function createNode(id, name) { + if (id == "and" || id == "or") { + return { + id: id + uniqueCounter++, + type: 'input', + data: { label: name }, + position, + }; } else { - cur = origin; + return { + id: id, + type: 'input', + data: { label: name }, + position, + }; + } + } + function createEdge(s, t) { + return { id: s + " " + t, source: s, target: s, type: edgeType, animated: true }; + } + + function loadCource(parent, obj) { + console.log(obj); + if (typeof obj === 'object') { + for (let i = 0; i < obj.arr.length; i++) { + if (obj.arr[i].type == "and") { + + console.log("Create and node") + + let newN = createNode("and", "All of"); + initialNodes.push(newN); + initialEdges.push(createEdge(parent.id, newN.id)); + for (let i = 0; i < obj.arr[i].length; i++) { + let nCource = loadCource(obj.arr[i]); + initialNodes.push(nCource); + initialEdges.push(createEdge(newN.id, nCource.id)); + } + return newN; + + } else if (obj.arr[i].type == "or") { + + console.log("Create or node") + + let newN = createNode("or", "One of"); + initialNodes.push(newN); + initialEdges.push(createEdge(parent.id, newN.id)); + for (let i = 0; i < obj.arr[i].length; i++) { + let nCource = loadCource(obj.arr[i]); + initialNodes.push(nCource); + initialEdges.push(createEdge(newN.id, nCource.id)); + } + return newN; - //Adds a node - for (let i = 0; i < temp.length; i++) { - initialNodes.push({ id: temp[i], data: { label: temp[i] }, position: { x: leftOfset * 400 + 400, y: ofsett + depth[leftOfset] }, sourcePosition: "left", targetPosition: "right" }); - initialEdges.push({ id: cur + "-" + temp[i], source: temp[i], target: cur, markerEnd: { type: "line", color: "black" }, type: "default", style: { stroke: "black" } }); - ofsett += 50; + } + } + } else { + console.log("Create text node") + console.log(obj); + + + + + let newN = createNode(obj, obj); + initialNodes.push(newN); + return newN; } - ofsett += 60; } - depth[leftOfset] += ofsett; -} -function loadTree(course) { - initialNodes.push({ id: course, data: { label: course }, position: { x: 0, y: 0 }, sourcePosition: "left", targetPosition: "right" }); - let column = 0; + function loadTree(course) { + let start = createNode(props.selectedCourse.code, props.selectedCourse.code) + initialNodes.push(start); + let preR = loadPrer(props.selectedCourse); + loadCource(start, preR); + } + + //loads the prerequistes + function load(cur) { + if (cur.and != null) { + let temp = { type: "and", arr: [] }; - let start = "SF2526"; - let preR2 = [["DD1320"], ["SF1544", "SF1545"]]; - addChildNodes(start, preR2, column); - column++; + for (let i = 0; i < cur.and.length; i++) { + temp.arr.push(load(cur.and[i])); + } + return temp; + } else if (cur.or != null) { + let temp = { type: "or", arr: [] }; - start = "DD1320"; - preR2 = [["SF1546"], ["SF1000", "SF1001", "SF1002"], ["SF1003", "SF1004"]]; - addChildNodes(start, preR2, column); + for (let i = 0; i < cur.or.length; i++) { + temp.arr.push(load(cur.or[i])); + } + return temp; + } + return cur; + } + + function dataFixer(obj) { + let fixed; + if (typeof obj === 'object') { + if (Array.isArray(obj.arr)) { + if (Array.isArray(obj.arr[0]) && obj.arr.length == 1) { + obj.arr = obj.arr[0]; + } + for (let i = 0; i < obj.arr.length; i++) { + fixed = dataFixer(obj.arr[i]); + } + } + } else { + return obj; + } + return fixed; + } + + //does the loading and cleaning of prerequisites + function loadPrer(course) { + let prereq = course.prerequisites; + let arrP = load(prereq) + dataFixer(arrP); + return arrP; + } - start = "SF1544"; - preR2 = [["SF1005"], ["SF1006", "SF1007"]]; - addChildNodes(start, preR2, column); - column++; -} - loadTree(props.selectedCourse.code); + loadTree(props.selectedCourse.code); - return + /* return */ + return }); export default PrerequisitePresenter; From 45f12476ba2e10ab35b00e0e5ec49856bad0b0c8 Mon Sep 17 00:00:00 2001 From: Elias Tosteberg Date: Fri, 11 Apr 2025 08:56:09 +0200 Subject: [PATCH 16/29] Fixed not rendering anything. Still broken --- .../src/presenters/PrerequisitePresenter.jsx | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/my-app/src/presenters/PrerequisitePresenter.jsx b/my-app/src/presenters/PrerequisitePresenter.jsx index 3cd1d4aa..c96a1f6a 100644 --- a/my-app/src/presenters/PrerequisitePresenter.jsx +++ b/my-app/src/presenters/PrerequisitePresenter.jsx @@ -17,6 +17,7 @@ import '@xyflow/react/dist/style.css'; export const PrerequisitePresenter = observer((props) => { + let uniqueCounter = 0; //let toAdd = []; @@ -24,7 +25,9 @@ export const PrerequisitePresenter = observer((props) => { const position = { x: 0, y: 0 }; const edgeType = 'smoothstep'; - const initialNodes = [ + + + let initialNodes = [ /* { id: '1', type: 'input', @@ -80,7 +83,7 @@ export const PrerequisitePresenter = observer((props) => { { id: '7', type: 'output', data: { label: 'output' }, position }, */ ]; - const initialEdges = [ + let initialEdges = [ /* { id: 'e12', source: '1', target: '2', type: edgeType, animated: true }, { id: 'e13', source: '1', target: '3', type: edgeType, animated: true }, { id: 'e22a', source: '2', target: '2a', type: edgeType, animated: true }, @@ -98,6 +101,9 @@ export const PrerequisitePresenter = observer((props) => { const nodeHeight = 36; + loadTree(""); + + @@ -139,9 +145,7 @@ export const PrerequisitePresenter = observer((props) => { const Flow = () => { - - loadTree(); - console.log("arived") + console.log("arived in Flow"); const [nodes, setNodes, onNodesChange] = useNodesState(layoutedNodes); @@ -195,7 +199,7 @@ export const PrerequisitePresenter = observer((props) => { return bool; } function createNode(id, name) { - if (id == "and" || id == "or") { + if (id == "and" || id == "or" || id == "Err") { return { id: id + uniqueCounter++, type: 'input', @@ -220,36 +224,36 @@ export const PrerequisitePresenter = observer((props) => { console.log(obj); if (typeof obj === 'object') { for (let i = 0; i < obj.arr.length; i++) { + let functionNode = createNode("Err", "Error"); if (obj.arr[i].type == "and") { console.log("Create and node") - let newN = createNode("and", "All of"); - initialNodes.push(newN); - initialEdges.push(createEdge(parent.id, newN.id)); + functionNode = createNode("and", "All of"); + initialNodes.push(functionNode); + initialEdges.push(createEdge(parent.id, functionNode.id)); for (let i = 0; i < obj.arr[i].length; i++) { let nCource = loadCource(obj.arr[i]); initialNodes.push(nCource); - initialEdges.push(createEdge(newN.id, nCource.id)); + initialEdges.push(createEdge(functionNode.id, nCource.id)); } - return newN; } else if (obj.arr[i].type == "or") { - console.log("Create or node") - + console.log("Create or node"); - let newN = createNode("or", "One of"); - initialNodes.push(newN); - initialEdges.push(createEdge(parent.id, newN.id)); + functionNode = createNode("or", "One of"); + initialNodes.push(functionNode); + initialEdges.push(createEdge(parent.id, functionNode.id)); for (let i = 0; i < obj.arr[i].length; i++) { let nCource = loadCource(obj.arr[i]); initialNodes.push(nCource); - initialEdges.push(createEdge(newN.id, nCource.id)); + initialEdges.push(createEdge(functionNode.id, nCource.id)); } - return newN; } + + return functionNode; } } else { console.log("Create text node") From abaa804653576e91967477d39225656124c00244 Mon Sep 17 00:00:00 2001 From: daDevBoat Date: Fri, 11 Apr 2025 11:26:35 +0200 Subject: [PATCH 17/29] Working v1 --- .../src/presenters/PrerequisitePresenter.jsx | 310 ++++++++---------- 1 file changed, 129 insertions(+), 181 deletions(-) diff --git a/my-app/src/presenters/PrerequisitePresenter.jsx b/my-app/src/presenters/PrerequisitePresenter.jsx index c96a1f6a..75271f2d 100644 --- a/my-app/src/presenters/PrerequisitePresenter.jsx +++ b/my-app/src/presenters/PrerequisitePresenter.jsx @@ -17,8 +17,6 @@ import '@xyflow/react/dist/style.css'; export const PrerequisitePresenter = observer((props) => { - - let uniqueCounter = 0; //let toAdd = []; @@ -27,85 +25,19 @@ export const PrerequisitePresenter = observer((props) => { - let initialNodes = [ - /* { - id: '1', - type: 'input', - data: { label: 'input' }, - position, - }, - { - id: '2', - data: { label: 'node 2' }, - position, - }, - { - id: '2a', - data: { label: 'node 2a' }, - position, - }, - { - id: '2b', - data: { label: 'node 2b' }, - position, - }, - { - id: '2c', - data: { label: 'node 2c' }, - position, - }, - { - id: '2d', - data: { label: 'node 2d' }, - position, - }, - { - id: '3', - data: { label: 'node 3' }, - position, - }, - { - id: '4', - data: { label: 'node 4' }, - position, - }, - { - id: '5', - data: { label: 'node 5' }, - position, - }, - { - id: '6', - type: 'output', - data: { label: 'output' }, - position, - }, - { id: '7', type: 'output', data: { label: 'output' }, position }, */ - ]; - - let initialEdges = [ - /* { id: 'e12', source: '1', target: '2', type: edgeType, animated: true }, - { id: 'e13', source: '1', target: '3', type: edgeType, animated: true }, - { id: 'e22a', source: '2', target: '2a', type: edgeType, animated: true }, - { id: 'e22b', source: '2', target: '2b', type: edgeType, animated: true }, - { id: 'e22c', source: '2', target: '2c', type: edgeType, animated: true }, - { id: 'e2c2d', source: '2c', target: '2d', type: edgeType, animated: true }, - { id: 'e45', source: '4', target: '5', type: edgeType, animated: true }, - { id: 'e56', source: '5', target: '6', type: edgeType, animated: true }, - { id: 'e57', source: '5', target: '7', type: edgeType, animated: true }, */ - ]; + let initialNodes = []; + let initialEdges = []; const dagreGraph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})); const nodeWidth = 172; const nodeHeight = 36; - - loadTree(""); - - - - + //initialNodes.push(createNode("IK1203", "IK1203", "default")); + //initialNodes.push(createNode("IK1204", "IK1204", "default")); + //initialEdges.push(createEdge(props.selectedCourse.code, "IK1203")); + //initialEdges.push(createEdge(props.selectedCourse.code, "IK1204")); + loadTree(props.selectedCourse.code); const getLayoutedElements = (nodes, edges, direction = 'LR') => { const isHorizontal = direction === 'LR'; @@ -173,6 +105,11 @@ export const PrerequisitePresenter = observer((props) => { connectionLineType={ConnectionLineType.SmoothStep} fitView style={{ backgroundColor: '#F7F9FB' }} + nodesDraggable={false} + nodesConnectable={false} + elementsSelectable={false} + elementsFocusable={false} + edgesFocusable={false} > @@ -186,30 +123,18 @@ export const PrerequisitePresenter = observer((props) => { - - - - function containsArr(arr) { - let bool = false - arr.forEach(element => { - if (Array.isArray(element)) { - bool = true; - } - }); - return bool; - } - function createNode(id, name) { - if (id == "and" || id == "or" || id == "Err") { + function createNode(id, name, node_type) { + if (id == "and" || id == "or") { return { - id: id + uniqueCounter++, - type: 'input', + id: id, + type: node_type, data: { label: name }, position, }; } else { return { id: id, - type: 'input', + type: node_type, data: { label: name }, position, }; @@ -217,112 +142,87 @@ export const PrerequisitePresenter = observer((props) => { } } function createEdge(s, t) { - return { id: s + " " + t, source: s, target: s, type: edgeType, animated: true }; + return { id: s + " " + t, source: s, target: t, type: edgeType, animated: true }; } - function loadCource(parent, obj) { - console.log(obj); - if (typeof obj === 'object') { - for (let i = 0; i < obj.arr.length; i++) { - let functionNode = createNode("Err", "Error"); - if (obj.arr[i].type == "and") { - - console.log("Create and node") - - functionNode = createNode("and", "All of"); - initialNodes.push(functionNode); - initialEdges.push(createEdge(parent.id, functionNode.id)); - for (let i = 0; i < obj.arr[i].length; i++) { - let nCource = loadCource(obj.arr[i]); - initialNodes.push(nCource); - initialEdges.push(createEdge(functionNode.id, nCource.id)); - } - - } else if (obj.arr[i].type == "or") { - - console.log("Create or node"); - functionNode = createNode("or", "One of"); - initialNodes.push(functionNode); - initialEdges.push(createEdge(parent.id, functionNode.id)); - for (let i = 0; i < obj.arr[i].length; i++) { - let nCource = loadCource(obj.arr[i]); - initialNodes.push(nCource); - initialEdges.push(createEdge(functionNode.id, nCource.id)); - } + function prereq_convert(current_object, previous_key, previous_node_id) { + if (current_object == undefined) {return} + + if (!Array.isArray(current_object)) { // Is object + let key = Object.keys(current_object)[0]; + //console.log("key: " + key); + if (key == "or") { + initialNodes.push(createNode(key + uniqueCounter, "One of these", "default")); + initialEdges.push(createEdge(previous_node_id, key + uniqueCounter)); + prereq_convert(current_object[key], key, key + uniqueCounter++); + } else if (key == "and") { + prereq_convert(current_object[key], key, previous_node_id); + } + } else { // Is an array + for (let i = 0; i < current_object.length; i++) { + if (typeof current_object[i] == "string") { + let input_text = current_object[i]; + if (current_object[i].startsWith("#")) { + input_text = input_text.slice(1, 115); + } + initialNodes.push(createNode(input_text, input_text, "output")); + initialEdges.push(createEdge(previous_node_id, input_text, "output")); + } else { + prereq_convert(current_object[i], previous_key, previous_node_id); } - - return functionNode; } - } else { - console.log("Create text node") - console.log(obj); - - - + } + + /* + + if (typeof current_object == "object" && !Array.isArray(current_object)) { + let key = Object.keys(current_object)[0]; + let object_array = current_object[key]; + console.log(key); + console.log(object_array); + let num_of_matches = 0; + for (let i = 0; i < object_array.length; i++) { + if (Array.isArray(object_array[i])) { + let num_of_inner_matches = 0; + for (let j = 0; j < object_array[i].length; j++) { + if (object_array[i][j]) { + num_of_inner_matches ++; + } + } + if (key == "or" && num_of_inner_matches > 0) {object_array[i] = true; num_of_matches++; continue;} + if (key == "and" && num_of_inner_matches == object_array[i].length) {object_array[i] = true; num_of_matches++; continue;} + object_array[i] = false; + } else if (typeof object_array[i] == "object") { + let inner_key = Object.keys(object_array[i])[0]; + if (object_array[i][inner_key]) {num_of_matches++;} + } else if(object_array[i]) {num_of_matches++} + } + if (key == "or" && num_of_matches > 0) {current_object[key] = true} + else if (key == "and" && num_of_matches == object_array.length) {current_object[key] = true} + else {current_object[key] = false} - let newN = createNode(obj, obj); - initialNodes.push(newN); - return newN; } + */ } - - function loadTree(course) { - let start = createNode(props.selectedCourse.code, props.selectedCourse.code) - initialNodes.push(start); - let preR = loadPrer(props.selectedCourse); - loadCource(start, preR); + function generateTree(prereqs) { + console.log(JSON.stringify(prereqs, null, 4)); + prereq_convert(prereqs, null, props.selectedCourse.code); } - //loads the prerequistes - function load(cur) { - if (cur.and != null) { - let temp = { type: "and", arr: [] }; - for (let i = 0; i < cur.and.length; i++) { - temp.arr.push(load(cur.and[i])); - } - return temp; - } else if (cur.or != null) { - let temp = { type: "or", arr: [] }; + function loadTree(course) { + let root = createNode(props.selectedCourse.code, props.selectedCourse.code, "input") + initialNodes.push(root); - for (let i = 0; i < cur.or.length; i++) { - temp.arr.push(load(cur.or[i])); - } - return temp; - } - return cur; - } + generateTree(props.selectedCourse.prerequisites); - function dataFixer(obj) { - let fixed; - if (typeof obj === 'object') { - if (Array.isArray(obj.arr)) { - if (Array.isArray(obj.arr[0]) && obj.arr.length == 1) { - obj.arr = obj.arr[0]; - } - for (let i = 0; i < obj.arr.length; i++) { - fixed = dataFixer(obj.arr[i]); - } - } - } else { - return obj; - } - return fixed; + console.log(initialNodes); + console.log(initialEdges); } - //does the loading and cleaning of prerequisites - function loadPrer(course) { - let prereq = course.prerequisites; - let arrP = load(prereq) - dataFixer(arrP); - return arrP; - } - - loadTree(props.selectedCourse.code); - /* return */ return }); @@ -330,3 +230,51 @@ export const PrerequisitePresenter = observer((props) => { export default PrerequisitePresenter; +/* + + +if (current_object == undefined) {return} + + if (!Array.isArray(current_object)) { // Is object + let key = Object.keys(current_object)[0]; + prereq_convert(current_object[key], key); + } else { // Is an array + for (let i = 0; i < current_object.length; i++) { + if (typeof current_object[i] == "string") { + + } else { + prereq_convert(current_object[i], previous_key); + } + } + } + + + if (typeof current_object == "object" && !Array.isArray(current_object)) { + let key = Object.keys(current_object)[0]; + let object_array = current_object[key]; + console.log(key); + console.log(object_array); + let num_of_matches = 0; + for (let i = 0; i < object_array.length; i++) { + if (Array.isArray(object_array[i])) { + let num_of_inner_matches = 0; + for (let j = 0; j < object_array[i].length; j++) { + if (object_array[i][j]) { + num_of_inner_matches ++; + } + } + if (key == "or" && num_of_inner_matches > 0) {object_array[i] = true; num_of_matches++; continue;} + if (key == "and" && num_of_inner_matches == object_array[i].length) {object_array[i] = true; num_of_matches++; continue;} + object_array[i] = false; + } else if (typeof object_array[i] == "object") { + let inner_key = Object.keys(object_array[i])[0]; + if (object_array[i][inner_key]) {num_of_matches++;} + } else if(object_array[i]) {num_of_matches++} + } + if (key == "or" && num_of_matches > 0) {current_object[key] = true} + else if (key == "and" && num_of_matches == object_array.length) {current_object[key] = true} + else {current_object[key] = false} + + } + +*/ \ No newline at end of file From 407425e27f9658e30ee66a4ccb5e4d732f3d3d84 Mon Sep 17 00:00:00 2001 From: daDevBoat Date: Fri, 11 Apr 2025 13:08:52 +0200 Subject: [PATCH 18/29] handling #prereqs --- .../src/presenters/PrerequisitePresenter.jsx | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/my-app/src/presenters/PrerequisitePresenter.jsx b/my-app/src/presenters/PrerequisitePresenter.jsx index 75271f2d..7a342d83 100644 --- a/my-app/src/presenters/PrerequisitePresenter.jsx +++ b/my-app/src/presenters/PrerequisitePresenter.jsx @@ -18,6 +18,8 @@ import '@xyflow/react/dist/style.css'; export const PrerequisitePresenter = observer((props) => { let uniqueCounter = 0; + let textCounter = 0; + let codeCounter = 0; //let toAdd = []; const position = { x: 0, y: 0 }; @@ -41,7 +43,7 @@ export const PrerequisitePresenter = observer((props) => { const getLayoutedElements = (nodes, edges, direction = 'LR') => { const isHorizontal = direction === 'LR'; - dagreGraph.setGraph({ rankdir: direction }); + dagreGraph.setGraph({ rankdir: direction, nodesep: 30}); nodes.forEach((node) => { dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight }); @@ -77,7 +79,7 @@ export const PrerequisitePresenter = observer((props) => { const Flow = () => { - console.log("arived in Flow"); + //console.log("arived in Flow"); const [nodes, setNodes, onNodesChange] = useNodesState(layoutedNodes); @@ -107,7 +109,7 @@ export const PrerequisitePresenter = observer((props) => { style={{ backgroundColor: '#F7F9FB' }} nodesDraggable={false} nodesConnectable={false} - elementsSelectable={false} + elementsSelectable={true} elementsFocusable={false} edgesFocusable={false} > @@ -163,12 +165,16 @@ export const PrerequisitePresenter = observer((props) => { } else { // Is an array for (let i = 0; i < current_object.length; i++) { if (typeof current_object[i] == "string") { + let input_id = ""; let input_text = current_object[i]; if (current_object[i].startsWith("#")) { - input_text = input_text.slice(1, 115); - } - initialNodes.push(createNode(input_text, input_text, "output")); - initialEdges.push(createEdge(previous_node_id, input_text, "output")); + input_text = "More Info..."; //input_text.slice(1, 115); + input_id = "text" + ++textCounter; + } else { + input_id = current_object[i] + " " + ++codeCounter; + } + initialNodes.push(createNode(input_id, input_text, "output")); + initialEdges.push(createEdge(previous_node_id, input_id, "output")); } else { prereq_convert(current_object[i], previous_key, previous_node_id); } From cda406540eb0809d118904bb9b94391a252c56d2 Mon Sep 17 00:00:00 2001 From: daDevBoat Date: Fri, 11 Apr 2025 13:26:07 +0200 Subject: [PATCH 19/29] Test --- my-app/src/presenters/ListViewPresenter.jsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/my-app/src/presenters/ListViewPresenter.jsx b/my-app/src/presenters/ListViewPresenter.jsx index 001cd6a4..defb8a66 100644 --- a/my-app/src/presenters/ListViewPresenter.jsx +++ b/my-app/src/presenters/ListViewPresenter.jsx @@ -4,15 +4,9 @@ import { useState } 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 {ReviewPresenter} from "../presenters/ReviewPresenter.jsx" const ListViewPresenter = observer(({ model }) => { - - const [isPopupOpen, setIsPopupOpen] = useState(false); - const [selectedCourse, setSelectedCourse] = useState(null); - const preP = - const popup = setIsPopupOpen(false)} course={selectedCourse} prerequisiteTree={preP}/> - const addFavourite = (course) => { model.addFavourite(course); } From 0287df4ffa5774b3807c3090bf721d830df1817a Mon Sep 17 00:00:00 2001 From: Elias Tosteberg Date: Fri, 11 Apr 2025 13:49:52 +0200 Subject: [PATCH 20/29] removed commented out code --- .../src/presenters/PrerequisitePresenter.jsx | 43 +------------------ 1 file changed, 2 insertions(+), 41 deletions(-) diff --git a/my-app/src/presenters/PrerequisitePresenter.jsx b/my-app/src/presenters/PrerequisitePresenter.jsx index 455f0cb1..981b1d98 100644 --- a/my-app/src/presenters/PrerequisitePresenter.jsx +++ b/my-app/src/presenters/PrerequisitePresenter.jsx @@ -20,7 +20,6 @@ export const PrerequisitePresenter = observer((props) => { let uniqueCounter = 0; let textCounter = 0; let codeCounter = 0; - //let toAdd = []; const position = { x: 0, y: 0 }; const edgeType = 'smoothstep'; @@ -35,10 +34,6 @@ export const PrerequisitePresenter = observer((props) => { const nodeWidth = 172; const nodeHeight = 36; - //initialNodes.push(createNode("IK1203", "IK1203", "default")); - //initialNodes.push(createNode("IK1204", "IK1204", "default")); - //initialEdges.push(createEdge(props.selectedCourse.code, "IK1203")); - //initialEdges.push(createEdge(props.selectedCourse.code, "IK1204")); loadTree(props.selectedCourse.code); const getLayoutedElements = (nodes, edges, direction = 'LR') => { @@ -79,9 +74,6 @@ export const PrerequisitePresenter = observer((props) => { const Flow = () => { - //console.log("arived in Flow"); - - const [nodes, setNodes, onNodesChange] = useNodesState(layoutedNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(layoutedEdges); @@ -153,7 +145,6 @@ export const PrerequisitePresenter = observer((props) => { if (!Array.isArray(current_object)) { // Is object let key = Object.keys(current_object)[0]; - //console.log("key: " + key); if (key == "or") { initialNodes.push(createNode(key + uniqueCounter, "One of these", "default")); initialEdges.push(createEdge(previous_node_id, key + uniqueCounter)); @@ -168,7 +159,7 @@ export const PrerequisitePresenter = observer((props) => { let input_id = ""; let input_text = current_object[i]; if (current_object[i].startsWith("#")) { - input_text = "More Info..."; //input_text.slice(1, 115); + input_text = "More Info..."; input_id = "text" + ++textCounter; } else { input_id = current_object[i] + " " + ++codeCounter; @@ -180,37 +171,7 @@ export const PrerequisitePresenter = observer((props) => { } } } - - /* - - if (typeof current_object == "object" && !Array.isArray(current_object)) { - let key = Object.keys(current_object)[0]; - let object_array = current_object[key]; - console.log(key); - console.log(object_array); - let num_of_matches = 0; - for (let i = 0; i < object_array.length; i++) { - if (Array.isArray(object_array[i])) { - let num_of_inner_matches = 0; - for (let j = 0; j < object_array[i].length; j++) { - if (object_array[i][j]) { - num_of_inner_matches ++; - } - } - if (key == "or" && num_of_inner_matches > 0) {object_array[i] = true; num_of_matches++; continue;} - if (key == "and" && num_of_inner_matches == object_array[i].length) {object_array[i] = true; num_of_matches++; continue;} - object_array[i] = false; - } else if (typeof object_array[i] == "object") { - let inner_key = Object.keys(object_array[i])[0]; - if (object_array[i][inner_key]) {num_of_matches++;} - } else if(object_array[i]) {num_of_matches++} - } - if (key == "or" && num_of_matches > 0) {current_object[key] = true} - else if (key == "and" && num_of_matches == object_array.length) {current_object[key] = true} - else {current_object[key] = false} - - } - */ + } function generateTree(prereqs) { From 3216309e1a260042caa9911c1b89d98542b1be78 Mon Sep 17 00:00:00 2001 From: daDevBoat Date: Fri, 11 Apr 2025 15:50:28 +0200 Subject: [PATCH 21/29] More info expands now --- .../src/presenters/PrerequisitePresenter.jsx | 61 +++++++++++++++++-- 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/my-app/src/presenters/PrerequisitePresenter.jsx b/my-app/src/presenters/PrerequisitePresenter.jsx index 981b1d98..f3a3740b 100644 --- a/my-app/src/presenters/PrerequisitePresenter.jsx +++ b/my-app/src/presenters/PrerequisitePresenter.jsx @@ -21,6 +21,8 @@ export const PrerequisitePresenter = observer((props) => { let textCounter = 0; let codeCounter = 0; + let input_text_obj = {}; + const position = { x: 0, y: 0 }; const edgeType = 'smoothstep'; @@ -101,9 +103,9 @@ export const PrerequisitePresenter = observer((props) => { style={{ backgroundColor: '#F7F9FB' }} nodesDraggable={false} nodesConnectable={false} - elementsSelectable={true} - elementsFocusable={false} edgesFocusable={false} + onNodeClick={clicked} + elementsSelectable={false} > @@ -111,6 +113,28 @@ export const PrerequisitePresenter = observer((props) => {
); + + function setLabel(id, label) { + setNodes((nodes) => + nodes.map((n) => + n.id === id ? { ...n, data: { ...n.data, label } } : n + ) + ); + } + + function clicked(event, node) { + if (node["id"].split(" ")[0] === "text") { + if (node["data"]["label"] === "More Info...") { + node["style"]["zIndex"] = 1; + setLabel(node["id"], {input_text_obj[node["id"]]}
CLOSE
); + } else { + node["style"]["zIndex"] = 0; + setLabel(node["id"], "More Info..."); + } + } else { + // ADD FUNCTIONALITY FOR CLICKING COURSE CODE NODE + } + } }; @@ -123,6 +147,7 @@ export const PrerequisitePresenter = observer((props) => { id: id, type: node_type, data: { label: name }, + style: { zIndex: 0 }, position, }; } else { @@ -130,6 +155,7 @@ export const PrerequisitePresenter = observer((props) => { id: id, type: node_type, data: { label: name }, + style: { zIndex: 0 }, position, }; @@ -160,7 +186,8 @@ export const PrerequisitePresenter = observer((props) => { let input_text = current_object[i]; if (current_object[i].startsWith("#")) { input_text = "More Info..."; - input_id = "text" + ++textCounter; + input_id = "text " + ++textCounter; + input_text_obj[input_id] = current_object[i].slice(1); } else { input_id = current_object[i] + " " + ++codeCounter; } @@ -175,7 +202,7 @@ export const PrerequisitePresenter = observer((props) => { } function generateTree(prereqs) { - console.log(JSON.stringify(prereqs, null, 4)); + //console.log(JSON.stringify(prereqs, null, 4)); prereq_convert(prereqs, null, props.selectedCourse.code); } @@ -186,8 +213,10 @@ export const PrerequisitePresenter = observer((props) => { generateTree(props.selectedCourse.prerequisites); - console.log(initialNodes); - console.log(initialEdges); + //console.log(initialNodes); + //console.log(initialEdges); + //console.log(JSON.stringify(input_text_obj, null, 4)); + } /* return */ @@ -196,3 +225,23 @@ export const PrerequisitePresenter = observer((props) => { export default PrerequisitePresenter; + + +/* + + + + + let HTML_nodes = document.getElementsByClassName("react-flow__node"); + + for (let i = 0; i < HTML_nodes.length; i++) { + //console.log(HTML_nodes[i].children[0].getAttribute("data-nodeid").split(" ")[0]) + if (HTML_nodes[i].children[0].getAttribute("data-nodeid").split(" ")[0] === "text") { + HTML_nodes[i].addEventListener('click', function () { + alert('Button was clicked!'); + }); + } // Can add else + } + + + */ \ No newline at end of file From a5b690954352793607335a1e26b4e76390a56f27 Mon Sep 17 00:00:00 2001 From: daDevBoat Date: Fri, 11 Apr 2025 15:51:55 +0200 Subject: [PATCH 22/29] More info expands now --- my-app/src/presenters/PrerequisitePresenter.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/my-app/src/presenters/PrerequisitePresenter.jsx b/my-app/src/presenters/PrerequisitePresenter.jsx index f3a3740b..3446fd91 100644 --- a/my-app/src/presenters/PrerequisitePresenter.jsx +++ b/my-app/src/presenters/PrerequisitePresenter.jsx @@ -132,7 +132,7 @@ export const PrerequisitePresenter = observer((props) => { setLabel(node["id"], "More Info..."); } } else { - // ADD FUNCTIONALITY FOR CLICKING COURSE CODE NODE + // ADD FUNCTIONALITY FOR CLICKING COURSE CODE NODE (Tu eres muy retrasado y gordo)! :) } } }; From 30f7e76c7bd1f35bc0dad03ffc80605666b82fcf Mon Sep 17 00:00:00 2001 From: daDevBoat Date: Fri, 11 Apr 2025 15:53:51 +0200 Subject: [PATCH 23/29] More info expands now --- my-app/src/presenters/PrerequisitePresenter.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/my-app/src/presenters/PrerequisitePresenter.jsx b/my-app/src/presenters/PrerequisitePresenter.jsx index 3446fd91..3835312f 100644 --- a/my-app/src/presenters/PrerequisitePresenter.jsx +++ b/my-app/src/presenters/PrerequisitePresenter.jsx @@ -131,7 +131,7 @@ export const PrerequisitePresenter = observer((props) => { node["style"]["zIndex"] = 0; setLabel(node["id"], "More Info..."); } - } else { + } else if (node["data"]["label"] !== "One of these") { // ADD FUNCTIONALITY FOR CLICKING COURSE CODE NODE (Tu eres muy retrasado y gordo)! :) } } From e748152575ace260de00c690cf146085422afd63 Mon Sep 17 00:00:00 2001 From: daDevBoat Date: Fri, 11 Apr 2025 16:31:12 +0200 Subject: [PATCH 24/29] Start coding on line 135 in PrereqTreePresenter Mr. PO --- .../src/presenters/PrerequisitePresenter.jsx | 20 +++++++++++-------- my-app/src/styles.css | 4 ++++ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/my-app/src/presenters/PrerequisitePresenter.jsx b/my-app/src/presenters/PrerequisitePresenter.jsx index 3835312f..6f92740e 100644 --- a/my-app/src/presenters/PrerequisitePresenter.jsx +++ b/my-app/src/presenters/PrerequisitePresenter.jsx @@ -131,7 +131,7 @@ export const PrerequisitePresenter = observer((props) => { node["style"]["zIndex"] = 0; setLabel(node["id"], "More Info..."); } - } else if (node["data"]["label"] !== "One of these") { + } else if (node["data"]["label"] !== "One of these" && node["data"]["label"] !== "No Prerequisites" && node["id"] !== props.selectedCourse.code) { // ADD FUNCTIONALITY FOR CLICKING COURSE CODE NODE (Tu eres muy retrasado y gordo)! :) } } @@ -208,14 +208,18 @@ export const PrerequisitePresenter = observer((props) => { function loadTree(course) { - let root = createNode(props.selectedCourse.code, props.selectedCourse.code, "input") - initialNodes.push(root); - - generateTree(props.selectedCourse.prerequisites); + console.log(JSON.stringify(props.selectedCourse.prerequisites, null, 4)); + if (props.selectedCourse.prerequisites === "null" || props.selectedCourse.prerequisites.length == 0) { + let display_node = createNode("No Prerequisites", "No Prerequisites", "defeault"); + display_node.style["pointerEvents"] = "none"; + display_node["className"] = 'no-handles'; + initialNodes.push(display_node); + } else { + let root = createNode(props.selectedCourse.code, props.selectedCourse.code, "input"); + initialNodes.push(root); + generateTree(props.selectedCourse.prerequisites); + } - //console.log(initialNodes); - //console.log(initialEdges); - //console.log(JSON.stringify(input_text_obj, null, 4)); } diff --git a/my-app/src/styles.css b/my-app/src/styles.css index f1d8c73c..82572f5d 100644 --- a/my-app/src/styles.css +++ b/my-app/src/styles.css @@ -1 +1,5 @@ @import "tailwindcss"; + +.react-flow__node.no-handles .react-flow__handle { + display: none; +} \ No newline at end of file From 88e94f65db5dad6b9254771789e9a509b7add11f Mon Sep 17 00:00:00 2001 From: daDevBoat Date: Wed, 16 Apr 2025 14:42:05 +0200 Subject: [PATCH 25/29] Prereqs colored and expands more info --- my-app/src/assets/example.json | 2 +- my-app/src/model.js | 18 ++- my-app/src/presenters/ListViewPresenter.jsx | 19 ++- .../src/presenters/PrerequisitePresenter.jsx | 153 ++++++++++++------ 4 files changed, 124 insertions(+), 68 deletions(-) diff --git a/my-app/src/assets/example.json b/my-app/src/assets/example.json index 0da7da94..fc8e79d4 100644 --- a/my-app/src/assets/example.json +++ b/my-app/src/assets/example.json @@ -145248,4 +145248,4 @@ "prerequisites_text": "null", "learning_outcomes": "null" } -} +} \ No newline at end of file diff --git a/my-app/src/model.js b/my-app/src/model.js index 3da7c114..d1007484 100644 --- a/my-app/src/model.js +++ b/my-app/src/model.js @@ -53,15 +53,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); }); diff --git a/my-app/src/presenters/ListViewPresenter.jsx b/my-app/src/presenters/ListViewPresenter.jsx index defb8a66..a6acb3d7 100644 --- a/my-app/src/presenters/ListViewPresenter.jsx +++ b/my-app/src/presenters/ListViewPresenter.jsx @@ -4,13 +4,13 @@ import { useState } 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 { ReviewPresenter } from "../presenters/ReviewPresenter.jsx" const ListViewPresenter = observer(({ model }) => { const addFavourite = (course) => { model.addFavourite(course); } - const removeFavourite = (course) => { + const removeFavourite = (course) => { model.removeFavourite(course); } const handleFavouriteClick = (course) => { @@ -23,8 +23,13 @@ const ListViewPresenter = observer(({ model }) => { const [isPopupOpen, setIsPopupOpen] = useState(false); const [selectedCourse, setSelectedCourse] = useState(null); - const preP = ; - const reviewPresenter = ; + const preP = ; + const reviewPresenter = ; const popup = { handleFavouriteClick={handleFavouriteClick} isOpen={isPopupOpen} onClose={() => setIsPopupOpen(false)} course={selectedCourse} - prerequisiteTree={preP} - reviewPresenter={reviewPresenter}/> - + prerequisiteTree={preP} + reviewPresenter={reviewPresenter} /> + return { const nodeHeight = 36; loadTree(props.selectedCourse.code); + console.log(initialNodes); const getLayoutedElements = (nodes, edges, direction = 'LR') => { const isHorizontal = direction === 'LR'; @@ -92,24 +93,25 @@ export const PrerequisitePresenter = observer((props) => { return (
- - - - + + + + +
); @@ -133,6 +135,7 @@ export const PrerequisitePresenter = observer((props) => { } } else if (node["data"]["label"] !== "One of these" && node["data"]["label"] !== "No Prerequisites" && node["id"] !== props.selectedCourse.code) { // ADD FUNCTIONALITY FOR CLICKING COURSE CODE NODE (Tu eres muy retrasado y gordo)! :) + // ONCLICK HERE } } }; @@ -140,7 +143,6 @@ export const PrerequisitePresenter = observer((props) => { - function createNode(id, name, node_type) { if (id == "and" || id == "or") { return { @@ -166,17 +168,19 @@ export const PrerequisitePresenter = observer((props) => { } - function prereq_convert(current_object, previous_key, previous_node_id) { + function prereq_convert(courses_taken, current_object, previous_key, previous_node_id) { + let current_node = null; if (current_object == undefined) {return} if (!Array.isArray(current_object)) { // Is object let key = Object.keys(current_object)[0]; if (key == "or") { - initialNodes.push(createNode(key + uniqueCounter, "One of these", "default")); + current_node = createNode(key + uniqueCounter, "One of these", "default") + initialNodes.push(current_node); initialEdges.push(createEdge(previous_node_id, key + uniqueCounter)); - prereq_convert(current_object[key], key, key + uniqueCounter++); + prereq_convert(courses_taken, current_object[key], key, key + uniqueCounter++); } else if (key == "and") { - prereq_convert(current_object[key], key, previous_node_id); + prereq_convert(courses_taken, current_object[key], key, previous_node_id); } } else { // Is an array @@ -191,33 +195,98 @@ export const PrerequisitePresenter = observer((props) => { } else { input_id = current_object[i] + " " + ++codeCounter; } - initialNodes.push(createNode(input_id, input_text, "output")); + let new_node = createNode(input_id, input_text, "output"); + if (courses_taken.includes(current_object[i])) { + new_node["style"]["backgroundColor"] = "lightgreen"; + current_object[i] = true; + } else { + current_object[i] = false; + } + current_node = new_node; + initialNodes.push(new_node); initialEdges.push(createEdge(previous_node_id, input_id, "output")); } else { - prereq_convert(current_object[i], previous_key, previous_node_id); + prereq_convert(courses_taken, current_object[i], previous_key, previous_node_id); + } + } + } + + /* STEP 2: Check if an object is true or false based on content of the inner object */ + if (typeof current_object == "object" && !Array.isArray(current_object)) { + let key = Object.keys(current_object)[0]; + let object_array = current_object[key]; + let num_of_matches = 0; + for (let i = 0; i < object_array.length; i++) { + if (Array.isArray(object_array[i])) { + let num_of_inner_matches = 0; + for (let j = 0; j < object_array[i].length; j++) { + if (object_array[i][j]) { + num_of_inner_matches ++; + if (current_node != null) { + current_node["style"]["backgroundColor"] = "lightgreen"; + } + } + } + if (key == "or" && num_of_inner_matches > 0) { + object_array[i] = true; num_of_matches++; + if (current_node != null) { + current_node["style"]["backgroundColor"] = "lightgreen"; + } + continue; + } + if (key == "and" && num_of_inner_matches == object_array[i].length) { + object_array[i] = true; num_of_matches++; + if (current_node != null) { + current_node["style"]["backgroundColor"] = "lightgreen"; + } + continue; + } + object_array[i] = false; + } else if (typeof object_array[i] == "object") { + let inner_key = Object.keys(object_array[i])[0]; + if (object_array[i][inner_key]) {num_of_matches++;} + } else if(object_array[i]) {num_of_matches++} + } + if (key == "or" && num_of_matches > 0) { + current_object[key] = true; + if (current_node != null) { + current_node["style"]["backgroundColor"] = "lightgreen"; + } + } + else if (key == "and" && num_of_matches == object_array.length) { + current_object[key] = true; + if (current_node != null) { + current_node["style"]["backgroundColor"] = "lightgreen"; } } + else {current_object[key] = false} } - } - function generateTree(prereqs) { - //console.log(JSON.stringify(prereqs, null, 4)); - prereq_convert(prereqs, null, props.selectedCourse.code); + function generateTree(courses_taken, prereqs) { + prereq_convert(courses_taken, prereqs, null, props.selectedCourse.code); + let key = Object.keys(prereqs); + return prereqs[key]; + } - function loadTree(course) { + function loadTree(courses_taken) { + console.log(JSON.stringify(props.selectedCourse.prerequisites, null, 4)); if (props.selectedCourse.prerequisites === "null" || props.selectedCourse.prerequisites.length == 0) { - let display_node = createNode("No Prerequisites", "No Prerequisites", "defeault"); + let display_node = createNode("No Prerequisites", "No Prerequisites", "default"); display_node.style["pointerEvents"] = "none"; display_node["className"] = 'no-handles'; initialNodes.push(display_node); } else { let root = createNode(props.selectedCourse.code, props.selectedCourse.code, "input"); + let copy = {...props.selectedCourse.prerequisites}; + let eligible = generateTree(JSON.parse(localStorage.getItem("completedCourses")), copy); + if (eligible) { + root["style"]["backgroundColor"] = "lightgreen"; + } initialNodes.push(root); - generateTree(props.selectedCourse.prerequisites); } @@ -229,23 +298,3 @@ export const PrerequisitePresenter = observer((props) => { export default PrerequisitePresenter; - - -/* - - - - - let HTML_nodes = document.getElementsByClassName("react-flow__node"); - - for (let i = 0; i < HTML_nodes.length; i++) { - //console.log(HTML_nodes[i].children[0].getAttribute("data-nodeid").split(" ")[0]) - if (HTML_nodes[i].children[0].getAttribute("data-nodeid").split(" ")[0] === "text") { - HTML_nodes[i].addEventListener('click', function () { - alert('Button was clicked!'); - }); - } // Can add else - } - - - */ \ No newline at end of file From 810b63fbd8d6d3cdedff015c335bde6805db26f4 Mon Sep 17 00:00:00 2001 From: daDevBoat Date: Wed, 16 Apr 2025 14:55:46 +0200 Subject: [PATCH 26/29] added css styling --- my-app/package-lock.json | 6 ------ my-app/src/styles.css | 4 ++++ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/my-app/package-lock.json b/my-app/package-lock.json index f5ed50a8..e22c837a 100644 --- a/my-app/package-lock.json +++ b/my-app/package-lock.json @@ -3038,12 +3038,6 @@ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", "license": "MIT" }, - "node_modules/@types/geojson": { - "version": "7946.0.16", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", - "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", - "license": "MIT" - }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", diff --git a/my-app/src/styles.css b/my-app/src/styles.css index c6499c5d..0b707bad 100644 --- a/my-app/src/styles.css +++ b/my-app/src/styles.css @@ -18,3 +18,7 @@ .font-kanit { font-family: var(--font-kanit); } + +.react-flow__node.no-handles .react-flow__handle { + display: none; + } \ No newline at end of file From 771b4c9de2dd5f9ec29be498eb10bd7385d177c9 Mon Sep 17 00:00:00 2001 From: daDevBoat Date: Wed, 16 Apr 2025 15:05:56 +0200 Subject: [PATCH 27/29] Fixed copy bug --- my-app/src/presenters/PrerequisitePresenter.jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/my-app/src/presenters/PrerequisitePresenter.jsx b/my-app/src/presenters/PrerequisitePresenter.jsx index d8537f37..c45698f3 100644 --- a/my-app/src/presenters/PrerequisitePresenter.jsx +++ b/my-app/src/presenters/PrerequisitePresenter.jsx @@ -99,6 +99,7 @@ export const PrerequisitePresenter = observer((props) => { onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} onConnect={onConnect} + onNodeClick={clicked} connectionLineType={ConnectionLineType.SmoothStep} fitView style={{ backgroundColor: 'white', borderRadius: '10px'}} @@ -211,6 +212,7 @@ export const PrerequisitePresenter = observer((props) => { } /* STEP 2: Check if an object is true or false based on content of the inner object */ + if (typeof current_object == "object" && !Array.isArray(current_object)) { let key = Object.keys(current_object)[0]; let object_array = current_object[key]; @@ -260,6 +262,7 @@ export const PrerequisitePresenter = observer((props) => { } else {current_object[key] = false} } + } function generateTree(courses_taken, prereqs) { @@ -280,7 +283,7 @@ export const PrerequisitePresenter = observer((props) => { initialNodes.push(display_node); } else { let root = createNode(props.selectedCourse.code, props.selectedCourse.code, "input"); - let copy = {...props.selectedCourse.prerequisites}; + let copy = JSON.parse(JSON.stringify(props.selectedCourse.prerequisites)); let eligible = generateTree(JSON.parse(localStorage.getItem("completedCourses")), copy); if (eligible) { root["style"]["backgroundColor"] = "lightgreen"; From a99b4f9c9ca2d83a53761ca3ebc1822e885a02bc Mon Sep 17 00:00:00 2001 From: Elias Tosteberg Date: Thu, 17 Apr 2025 13:20:57 +0200 Subject: [PATCH 28/29] Crash fix in PrerequisitePresenter.jsx --- my-app/src/presenters/PrerequisitePresenter.jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/my-app/src/presenters/PrerequisitePresenter.jsx b/my-app/src/presenters/PrerequisitePresenter.jsx index c45698f3..0bdabcce 100644 --- a/my-app/src/presenters/PrerequisitePresenter.jsx +++ b/my-app/src/presenters/PrerequisitePresenter.jsx @@ -284,7 +284,11 @@ export const PrerequisitePresenter = observer((props) => { } else { let root = createNode(props.selectedCourse.code, props.selectedCourse.code, "input"); let copy = JSON.parse(JSON.stringify(props.selectedCourse.prerequisites)); - let eligible = generateTree(JSON.parse(localStorage.getItem("completedCourses")), copy); + let courses_taken_local = JSON.parse(localStorage.getItem("completedCourses")); + if (courses_taken_local == null) { + courses_taken_local = []; + } + let eligible = generateTree(courses_taken_local, copy); if (eligible) { root["style"]["backgroundColor"] = "lightgreen"; } From 7569178a309cc0ecfd5043551aab717223d40730 Mon Sep 17 00:00:00 2001 From: Elias Tosteberg Date: Thu, 17 Apr 2025 14:34:20 +0200 Subject: [PATCH 29/29] merge fix --- my-app/src/presenters/PrerequisitePresenter.jsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/my-app/src/presenters/PrerequisitePresenter.jsx b/my-app/src/presenters/PrerequisitePresenter.jsx index 7e070c3e..c8bba0d3 100644 --- a/my-app/src/presenters/PrerequisitePresenter.jsx +++ b/my-app/src/presenters/PrerequisitePresenter.jsx @@ -36,11 +36,7 @@ export const PrerequisitePresenter = observer((props) => { const nodeWidth = 172; const nodeHeight = 36; -<<<<<<< HEAD loadTree(props.selectedCourse.code); -======= - loadTree(props.selectedCourse?.code); ->>>>>>> origin/main console.log(initialNodes); const getLayoutedElements = (nodes, edges, direction = 'LR') => { @@ -137,11 +133,7 @@ export const PrerequisitePresenter = observer((props) => { node["style"]["zIndex"] = 0; setLabel(node["id"], "More Info..."); } -<<<<<<< HEAD } else if (node["data"]["label"] !== "One of these" && node["data"]["label"] !== "No Prerequisites" && node["id"] !== props.selectedCourse.code) { -======= - } else if (node["data"]["label"] !== "One of these" && node["data"]["label"] !== "No Prerequisites" && node["id"] !== props.selectedCourse?.code) { ->>>>>>> origin/main // ADD FUNCTIONALITY FOR CLICKING COURSE CODE NODE (Tu eres muy retrasado y gordo)! :) // ONCLICK HERE }