Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified .DS_Store
Binary file not shown.
Binary file added my-app/src/assets/project_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
38 changes: 23 additions & 15 deletions my-app/src/model.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,27 @@
// 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
setUser(user) {
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
Expand All @@ -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 !== course);
},

getCourse(courseID) {
return this.courses.find(course => course.code === courseID);
},
Expand All @@ -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);
}
};
10 changes: 8 additions & 2 deletions my-app/src/presenters/ListViewPresenter.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <ListView courses={props.model.courses} />;
const ListViewPresenter = observer(({ model }) => {
return <ListView
courses={model.courses}
searchResults={model.currentSearch}
favouriteCourses={model.favourites}
addFavourite={model.addFavourite}
removeFavourite={model.removeFavourite}
/>;
});

export { ListViewPresenter };
11 changes: 10 additions & 1 deletion my-app/src/presenters/SearchbarPresenter.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<SearchbarView courses = {model.courses} searchResults={model.setCurrentSearch} />
<SearchbarView
model={model}
searchCourses={searchCourses}
favouriteCourses = {model.favourites}
removeFavourite={model.removeFavourite}
/>
);
});

Expand Down
32 changes: 32 additions & 0 deletions my-app/src/views/Components/FavouriteDropdown.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';

function FavouritesDropdown(props) {
return (
<div className=" absolute mt-2 w-48 bg-white border border-gray-300 rounded-lg">
{props.favouriteCourses.length > 0 ? (
props.favouriteCourses.map(course => (

<div
key={course.code}
className="p-2 flex justify-between items-center">

<p>{course.name}</p>

<button
className="text-red-500 cursor-pointer"
onClick={() => removeFavourite(course.code)}>
X
</button>

</div>
))
) :
(
<div className="p-2 text-gray-500">No favourites</div>
)
}
</div>
);
}

export default FavouritesDropdown;
53 changes: 45 additions & 8 deletions my-app/src/views/ListView.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="bg-white text-black p-2 flex flex-col gap-5 h-full overflow-auto">
<div className="relative bg-white text-black p-2 flex flex-col gap-5 h-full overflow-auto">
{
props?.courses && props.courses.length > 0 ?
props.courses.map((course) => (
coursesToDisplay.length > 0 ?
coursesToDisplay.map((course) => (
<div
key={course.code}
className="p-5 hover:bg-[#000061] flex items-center cursor-pointer border border-b-black border-solid w-full rounded-lg">
className="p-5 hover:bg-blue-100 flex items-center border border-b-black border-solid w-full rounded-lg cursor-pointer">
<div>
<p className={"font-bold text-[#000061]"}>{course.code}</p>
<p className="font-bold">{course.name}</p>
<p className="text-gray-600" dangerouslySetInnerHTML={{__html:course.description}}/>
<p className="text-gray-600" dangerouslySetInnerHTML={
{ __html: readMoreState[course.code] ? course.description : course.description.slice(0, 150) }
}
/>
{course.description.length > 150 && (
<span className="text-blue-500" onClick={() => toggleReadMore(course.code)}>
{readMoreState[course.code] ? ' show less' : 'read more'}
</span>
)}

<div>
<button className="text-yellow-500 cursor-pointer" onClick={() => handleFavouriteClick(course.code)}>
{props.favouriteCourses.includes(course.code)
?
'Remove from Favourites'
:
'Add to Favourites'
}
</button>
</div>
</div>
</div>
)) :
Expand Down
69 changes: 45 additions & 24 deletions my-app/src/views/SearchbarView.jsx
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand All @@ -37,7 +40,7 @@ function SearchbarView(props) {
<div className="w-full px-6 py-6 bg-[#000061] flex items-center justify-between">
<a href="https://www.kth.se" className="flex items-center h-[90px] w-auto">
<img
src={kth_logo}
src={project_logo}
className="h-[90px] w-auto"
alt="KTH Logo"
/>
Expand All @@ -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"
/>

<div className="flex gap-4">
<div className="flex gap-6 items-center">
<button
className="w-[100px] h-[44px] bg-white text-black rounded-full text-center border border-solid cursor-pointer hover:bg-[#000061] hover:text-white"
className="w-[120px] h-[44px] bg-[#003399] text-white rounded-full border border-[#000061] cursor-pointer hover:bg-[#001a4d] transition-all duration-200"
onClick={() => window.location.href = "https://inferencekth.github.io/Find-My-Next-Course/"}>
About us
</button>

<div className="w-[100px] h-[44px] bg-white text-black rounded-full text-center border border-solid cursor-pointer hover:bg-[#000061] hover:text-white flex items-center justify-center">
<button onClick={() => setShowFavourites(!showFavourites)}
className="w-[120px] h-[44px] bg-[#003399] text-white rounded-full border border-[#000061] cursor-pointer hover:bg-[#001a4d] transition-all duration-200">
Favourites
</button>

<div className="relative">
{showFavourites && (
<FavouritesDropdown
favouriteCourses={props.favouriteCourses}
removeFavourite={props.removeFavourite}
/>
)}
</div>

<div className="flex items-center cursor-pointer">
{user ? (
<div className="flex items-center cursor-pointer">
<img src={user.photoURL} alt="Profile" className="h-6 w-6 rounded-full mr-2" />
<button
onClick={handleSignOut}
className="flex items-center justify-center w-full h-full curspor-pointer">
Sign out
</button>
</div>
<button
onClick={handleSignOut}
className="w-[120px] h-[44px] bg-[#003399] text-white rounded-full border border-[#000061] cursor-pointer hover:bg-[#001a4d] transition-all duration-200">
Sign out
</button>
) : (
<button
onClick={handleSignIn}
className="flex items-center justify-center w-full h-full cursor-pointer">
className="w-auto min-w-[120px] h-[44px] bg-[#003399] text-white text-sm rounded-full border border-[#001a4d] cursor-pointer hover:bg-[#001a4d] transition-all duration-200 flex items-center justify-center px-4">
Sign in with Google
</button>
)}
</div>

{user && (
<img
src={user.photoURL}
alt="Profile"
className="w-[44px] h-[44px] rounded-full border border-[#000061]"
/>
)}
</div>
</div>
);
Expand Down
Binary file added src/.DS_Store
Binary file not shown.