diff --git a/.DS_Store b/.DS_Store index 0cd3991b..59a8781c 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/my-app/src/assets/project_icon.png b/my-app/src/assets/project_icon.png new file mode 100644 index 00000000..898e331e Binary files /dev/null and b/my-app/src/assets/project_icon.png differ diff --git a/my-app/src/model.js b/my-app/src/model.js index f2a67e9e..4a2e0cf2 100644 --- a/my-app/src/model.js +++ b/my-app/src/model.js @@ -1,13 +1,11 @@ -// This model is representing our program logic. -// A certain model is bound to a specific session. - import { addCourse } from "../firebase"; export const model = { user: undefined, currentCourse: undefined, - currentSearch: {}, + currentSearch: [], courses: [], + favourites: [], isReady: false, // sets the current user @@ -15,15 +13,15 @@ export const model = { if (!this.user) this.user = user; }, - + // sets the currently selected course (detail view?) - could be component state setCurrentCourse(course){ this.currentCourse = course; }, // keeps track of the current search / the associated promises. - setCurrentSearch(search){ - this.currentSearch = search; + setCurrentSearch(searchResults){ + this.currentSearch = searchResults; }, // sets the course array - for example after loading all courses from the DB @@ -32,21 +30,23 @@ export const model = { }, // add a single course - // addCourse(course){ - // this.courses = [...this.courses, course] // update local copy - // addCourse(course); // update firebase - // }, - async addCourse(course) { try { await addCourse(course); this.courses = [...this.courses, course]; - console.log("Course added successfully."); } catch (error) { console.error("Error adding course:", error); } }, + addFavourite(course) { + this.favourites = [...this.favourites, course]; + }, + + removeFavourite(course) { + this.favourites = (this.favourites || []).filter(fav => fav.code !== course.code); + }, + getCourse(courseID) { return this.courses.find(course => course.code === courseID); }, @@ -71,6 +71,14 @@ export const model = { }; this.addCourse(course); }); - } -} + }, + searchCourses(query) { + const searchResults = this.courses.filter(course => + course.code.toLowerCase() === query.toLowerCase() || + course.name.toLowerCase().includes(query.toLowerCase()) || + course.description.toLowerCase().includes(query.toLowerCase()) + ); + this.setCurrentSearch(searchResults); + } +}; \ No newline at end of file diff --git a/my-app/src/presenters/ListViewPresenter.jsx b/my-app/src/presenters/ListViewPresenter.jsx index 500a05c3..b6a50f9e 100644 --- a/my-app/src/presenters/ListViewPresenter.jsx +++ b/my-app/src/presenters/ListViewPresenter.jsx @@ -2,8 +2,14 @@ import React from 'react'; import { observer } from "mobx-react-lite"; import ListView from "../views/ListView.jsx"; -const ListViewPresenter = observer((props) => { - return ; +const ListViewPresenter = observer(({ model }) => { + return ; }); export { ListViewPresenter }; \ No newline at end of file diff --git a/my-app/src/presenters/SearchbarPresenter.jsx b/my-app/src/presenters/SearchbarPresenter.jsx index f2cd9091..dbca139e 100644 --- a/my-app/src/presenters/SearchbarPresenter.jsx +++ b/my-app/src/presenters/SearchbarPresenter.jsx @@ -3,8 +3,17 @@ import { observer } from "mobx-react-lite"; import SearchbarView from "../views/SearchbarView.jsx"; const SearchbarPresenter = observer(({ model }) => { + const searchCourses = (query) => { + model.searchCourses(query); + } + return ( - + ); }); diff --git a/my-app/src/views/Components/FavouriteDropdown.jsx b/my-app/src/views/Components/FavouriteDropdown.jsx new file mode 100644 index 00000000..973c558c --- /dev/null +++ b/my-app/src/views/Components/FavouriteDropdown.jsx @@ -0,0 +1,32 @@ +import React from 'react'; + +function FavouritesDropdown(props) { + return ( +
+ {props.favouriteCourses.length > 0 ? ( + props.favouriteCourses.map(course => ( + +
+ +

{course.name}

+ + + +
+ )) + ) : + ( +
No favourites
+ ) + } +
+ ); +} + +export default FavouritesDropdown; \ No newline at end of file diff --git a/my-app/src/views/ListView.jsx b/my-app/src/views/ListView.jsx index 5a7213f5..9acbeaec 100644 --- a/my-app/src/views/ListView.jsx +++ b/my-app/src/views/ListView.jsx @@ -1,20 +1,57 @@ -import React from 'react'; -import { Quantum } from 'ldrs/react' -import 'ldrs/react/Quantum.css' +import React, { useState } from 'react'; +import { Quantum } from 'ldrs/react'; +import 'ldrs/react/Quantum.css'; +import FavouritesDropdown from './Components/FavouriteDropdown.jsx'; function ListView(props) { + const coursesToDisplay = props.searchResults.length > 0 ? props.searchResults : props.courses; + const [readMoreState, setReadMoreState] = useState({}); + + const toggleReadMore = (courseCode) => { + setReadMoreState(prevState => ( + {...prevState, [courseCode]: !prevState[courseCode]})); + }; + + const handleFavouriteClick = (courseCode) => { + if (props.favouriteCourses.includes(courseCode)) { + props.removeFavourite(courseCode); + } + else { + props.addFavourite(courseCode); + } + }; + return ( -
+
{ - props?.courses && props.courses.length > 0 ? - props.courses.map((course) => ( + coursesToDisplay.length > 0 ? + coursesToDisplay.map((course) => (
+ className="p-5 hover:bg-blue-100 flex items-center border border-b-black border-solid w-full rounded-lg cursor-pointer">

{course.code}

{course.name}

-

+

+ {course.description.length > 150 && ( + toggleReadMore(course.code)}> + {readMoreState[course.code] ? ' show less' : 'read more'} + + )} + +

+ +
)) : diff --git a/my-app/src/views/SearchbarView.jsx b/my-app/src/views/SearchbarView.jsx index 6fc31899..d9fb1bbd 100644 --- a/my-app/src/views/SearchbarView.jsx +++ b/my-app/src/views/SearchbarView.jsx @@ -1,24 +1,27 @@ import React, { useState, useEffect } from 'react'; -import { model } from '../model.js'; -import kth_logo from '../assets/kth_logo.png'; import { getAuth, signInWithPopup, signOut, GoogleAuthProvider } from "firebase/auth"; +import kth_logo from '../assets/kth_logo.png'; +import project_logo from '../assets/project_icon.png'; +import FavouritesDropdown from './Components/FavouriteDropdown.jsx'; function SearchbarView(props) { const [searchQuery, setSearchQuery] = useState(''); const [user, setUser] = useState(null); + const [showFavourites, setShowFavourites] = useState(false); + const auth = getAuth(); const provider = new GoogleAuthProvider(); useEffect(() => { - const unsubscribe = auth.onAuthStateChanged(setUser); + const unsubscribe = auth.onAuthStateChanged((user) => { + setUser(user); + }); return () => unsubscribe(); - },[auth]); + }, [auth]); - const handleSearch = () => { - const results = props.courses.filter(course => - course.name.toLowerCase().includes(searchQuery.toLowerCase()) - ).splice(0, 10); - props.searchResults(results); + const handleSearch = (query) => { + setSearchQuery(query); + props.searchCourses(query); }; const handleSignIn = async () => { @@ -37,7 +40,7 @@ function SearchbarView(props) {
KTH Logo @@ -47,36 +50,54 @@ function SearchbarView(props) { type="text" placeholder="What course are you looking for?" value={searchQuery} - onChange={(e) => setSearchQuery(e.target.value)} - onKeyPress={(e) => e.key === "Enter" && handleSearch()} + onChange={(e) => handleSearch(e.target.value)} className="w-[400px] h-[44px] pl-14 pr-4 bg-white text-black rounded-full" /> -
+
-
+ + +
+ {showFavourites && ( + + )} +
+ +
{user ? ( -
- Profile - -
+ ) : ( )}
+ + {user && ( + Profile + )}
); diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 00000000..59769394 Binary files /dev/null and b/src/.DS_Store differ