diff --git a/my-app/src/assets/upload.gif b/my-app/src/assets/upload.gif new file mode 100644 index 0000000..1443ebe Binary files /dev/null and b/my-app/src/assets/upload.gif differ diff --git a/my-app/src/model.js b/my-app/src/model.js index 372d0b1..a6e6851 100644 --- a/my-app/src/model.js +++ b/my-app/src/model.js @@ -17,6 +17,7 @@ export const model = { departments : [], locations: [], avgRatings: [], + /* courses the user selected as their favourite */ favourites: [], searchHistory:[], isReady: false, @@ -31,18 +32,19 @@ export const model = { filterOptions: { //apply-X-Filter boolean triggering flag wether corresponding filtering functions should run or not //different arrays require different data, some uses string arrays, some boolean values, and so on - applyTranscriptFilter: true, + applyTranscriptFilter: false, eligibility: "weak", //the possible values for the string are: "weak"/"moderate"/"strong" applyLevelFilter: true, level: ["PREPARATORY", "BASIC", "ADVANCED", "RESEARCH"], //the possible values for the array are: "PREPARATORY", "BASIC", "ADVANCED", "RESEARCH" - applyLanguageFilter: true, + applyLanguageFilter: false, language: "none", //the possible values for the string are: "none"/"english"/"swedish"/"both" - applyLocationFilter:true, + applyLocationFilter:false, location: [], //the possible values for the array are: 'KTH Campus', 'KTH Kista', 'AlbaNova', 'KTH Flemingsberg', 'KTH Solna', 'KTH Södertälje', 'Handelshögskolan', 'KI Solna', 'Stockholms universitet', 'KONSTFACK' applyCreditsFilter:true, creditMin: 0, creditMax: 45, - applyDepartmentFilter: true, + applyDepartmentFilter: false, + department: [], applyRemoveNullCourses: false, period: [true, true, true, true], applyPeriodFilter: true @@ -209,6 +211,7 @@ export const model = { updateLevelFilter(level) { this.filterOptions.level = level; + console.log(level); }, updateDepartmentFilter(department) { @@ -258,6 +261,29 @@ export const model = { setApplyPeriodFilter(periodfilterState) { this.filterOptions.applyPeriodFilter = periodfilterState; }, + //for better display we would like the departments in a structured format based on school + formatDepartments() { + const grouped = this.departments?.reduce((acc, item) => { + const [school, department] = item.split("/"); + if (!acc[school]) { + acc[school] = []; + } + acc[school].push(department?.trim()); + return acc; + }, {}); + const sortedGrouped = Object.keys(grouped) + .sort() + .reduce((acc, key) => { + acc[key] = grouped[key].sort(); + return acc; + }, {}); + const fields = Object.entries(sortedGrouped).map(([school, departments], index) => ({ + id: index + 1, + label: school, + subItems: departments, + })); + return fields; + }, async getAverageRating(courseCode) { const reviews = await getReviewsForCourse(courseCode); if (!reviews || reviews.length === 0) return null; diff --git a/my-app/src/pages/App.jsx b/my-app/src/pages/App.jsx index 2a3d4e6..3cb7a83 100644 --- a/my-app/src/pages/App.jsx +++ b/my-app/src/pages/App.jsx @@ -16,14 +16,14 @@ function App({ model }) { return ( /* The sidebar styling(under the menu)*/ -
+
{ /* If sidebar is open, set length to 400px, else it should not be visible */} -
+
setSidebarIsOpen(state.isOpen)} - className="bg-gradient-to-t from-[#6246a8] to-[#6747c0] z-0 h-screen" + className="bg-gradient-to-t from-[#4f3646] to-[#6747c0] z-0 " noOverlay styles={{ bmMenuWrap: { @@ -32,7 +32,7 @@ function App({ model }) { bmBurgerButton: { position: 'absolute', top: '20px', - left: '20px', + left: '8px', width: '36px', height: '30px', zIndex: '20' diff --git a/my-app/src/presenters/FilterPresenter.jsx b/my-app/src/presenters/FilterPresenter.jsx index fb37f58..233c9dd 100644 --- a/my-app/src/presenters/FilterPresenter.jsx +++ b/my-app/src/presenters/FilterPresenter.jsx @@ -147,9 +147,9 @@ const FilterPresenter = observer(({ model }) => { bestCourses = localFilteredCourses.filter(function (course) { try { - return (locations.includes(course?.location)); + return (locations.includes(course?.location.toUpperCase())); } catch (error) { - console.log("for some reason course?.location is: ", course?.location, error); + console.log("for some reason course?.location is: ", course, error); return false; } @@ -394,10 +394,7 @@ const FilterPresenter = observer(({ model }) => { updatePeriods(); } if (model.filterOptions.applyLocationFilter) { - //after deo finishes locations, until then dont - - //console.log("going to apply location on:",localFilteredCourses.length); - //updateLocations(); + updateLocations(); } if (model.filterOptions.applyLevelFilter) { updateLevels(); @@ -412,8 +409,7 @@ const FilterPresenter = observer(({ model }) => { applyTranscriptEligibility(); } if (model.filterOptions.applyDepartments) { - //console.log("going to apply location on:",localFilteredCourses.length); - //updateDepartments(); + updateDepartments(); } model.filteredCourses = [...localFilteredCourses]; diff --git a/my-app/src/presenters/PrerequisitePresenter.jsx b/my-app/src/presenters/PrerequisitePresenter.jsx index cc5f61f..88b8b5b 100644 --- a/my-app/src/presenters/PrerequisitePresenter.jsx +++ b/my-app/src/presenters/PrerequisitePresenter.jsx @@ -462,7 +462,8 @@ export const PrerequisitePresenter = observer((props) => { let key = Object.keys(prereqs); if (prereqs[key] === true) { return true; - } else { + } + else { return false; } diff --git a/my-app/src/presenters/SidebarPresenter.jsx b/my-app/src/presenters/SidebarPresenter.jsx index abd3204..a75a61e 100644 --- a/my-app/src/presenters/SidebarPresenter.jsx +++ b/my-app/src/presenters/SidebarPresenter.jsx @@ -7,17 +7,13 @@ const SidebarPresenter = observer(({ model }) => { useEffect(() => { model.setFiltersChange(); - }) + }); let currentLanguageSet = model.filterOptions.language; let currentLevelSet = model.filterOptions.level; let currentPeriodSet = model.filterOptions.period; - let currentDepartmentSet = [ - "EECS/Computational Science and Technology", "EECS/Theoretical Computer Science", "EECS/Electric Power and Energy Systems", "EECS/Network and Systems Engineering", - "ITM/Learning in Engineering Sciences", "ITM/Industrial Economics and Management", "ITM/Energy Systems", "ITM/Integrated Product Development and Design", "ITM/SKD GRU", - "SCI/Mathematics", "SCI/Applied Physics", "SCI/Mechanics", "SCI/Aeronautical and Vehicle Engineering", - "ABE/Sustainability and Environmental Engineering", "ABE/Concrete Structures", "ABE/Structural Design & Bridges", "ABE/History of Science, Technology and Environment", - ] + let currentDepartmentSet = model.filterOptions.department; + let currentLocationSet = model.filterOptions.location function handleLanguageFilterChange(param) { if (param === "English") { @@ -58,26 +54,11 @@ const SidebarPresenter = observer(({ model }) => { model.updateLanguageFilter(currentLanguageSet); } function handleLevelFilterChange(param) { - let properParam; - switch (param) { - case "Preparatory": - properParam = "PREPARATORY"; - break; - case "Basic": - properParam = "BASIC"; - break; - case "Advanced": - properParam = "ADVANCED"; - break; - case "Research": - properParam = "RESEARCH"; - break; - } - if (!currentLevelSet.includes(properParam)) { - currentLevelSet.push(properParam); + if (!currentLevelSet.includes(param)) { + currentLevelSet.push(param); } else { - const index = currentLevelSet.indexOf(properParam); + const index = currentLevelSet.indexOf(param); if (index > -1) { currentLevelSet.splice(index, 1); } @@ -104,6 +85,19 @@ const SidebarPresenter = observer(({ model }) => { model.setFiltersChange(); } + function handleLocationFilterChange(param) { + if (currentLocationSet.includes(param)) { + const index = currentLocationSet.indexOf(param); + if (index > -1) { + currentLocationSet.splice(index, 1); + } + } else { + currentLocationSet.push(param); + } + model.updateLocationFilter(currentLocationSet); + model.setFiltersChange(); + } + /*HandleFilterChange param is structured as such [ type of the field: (toggle, slider, dropdown, buttongroup) @@ -120,7 +114,7 @@ const SidebarPresenter = observer(({ model }) => { handleLevelFilterChange(param[2]); break; case "location": - console.log("location filter set to: " + param[2]); + handleLocationFilterChange(param[2]); break; case "credits": model.updateCreditsFilter(param[2]); @@ -130,6 +124,7 @@ const SidebarPresenter = observer(({ model }) => { break; case "department": handleDepartmentFilterChange(param[2]); + console.log(param[2]); break; case "period": handlePeriodFilterChange(param[2]); @@ -194,20 +189,30 @@ const SidebarPresenter = observer(({ model }) => { HandleFilterEnable={HandleFilterEnable} reApplyFilter={reApplyFilter} toggleRemoveNull={setApplyRemoveNullCourses} + initialApplyTranscriptFilter={model.filterOptions.applyTranscriptFilter} - initialTranscriptElegiblityValue = {model.filterOptions.eligibility} + initialTranscriptElegiblityValue={model.filterOptions.eligibility} + initialLanguageFilterOptions={currentLanguageSet} initialLanguageFilterEnable={model.filterOptions.applyLanguageFilter} + initialLevelFilterOptions={currentLevelSet} initialLevelFilterEnable={model.filterOptions.applyLevelFilter} + initialPeriodFilterOptions={currentPeriodSet} initialPeriodFilterEnable={model.filterOptions.applyPeriodFilter} + initialDepartmentFilterOptions={currentDepartmentSet} initialDepartmentFilterEnable={model.filterOptions.applyDepartmentFilter} - initialLocationFilterOptions={[]} + DepartmentFilterField = {model.formatDepartments()} + + initialLocationFilterOptions={currentLocationSet} initialLocationFilterEnable={model.filterOptions.applyLocationFilter} + LocationFilterField = {model.locations} + initialCreditsFilterOptions={[model.filterOptions.creditMin, model.filterOptions.creditMax]} initialCreditsFilterEnable={model.filterOptions.applyCreditsFilter} + initialApplyNullFilterEnable={model.filterOptions.applyRemoveNullCourses } /> ); diff --git a/my-app/src/views/Components/SideBarComponents/CollapsibleCheckboxes.jsx b/my-app/src/views/Components/SideBarComponents/CollapsibleCheckboxes.jsx index e48275d..8ca4aad 100644 --- a/my-app/src/views/Components/SideBarComponents/CollapsibleCheckboxes.jsx +++ b/my-app/src/views/Components/SideBarComponents/CollapsibleCheckboxes.jsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useRef } from "react"; import FilterEnableCheckbox from "./FilterEnableCheckbox"; import Tooltip from "./ToolTip"; @@ -12,6 +12,8 @@ const CollapsibleCheckboxes = (props) => { let paramFieldType = "checkboxhierarchy"; + const checkboxRef = useRef(null); + const rows = props.fields; const toggleExpand = (id, subItems) => { @@ -19,8 +21,18 @@ const CollapsibleCheckboxes = (props) => { ...prev, [id]: !prev[id], })); + let label = rows.find(item => item.id === id).label; subItems.map((_, index) => { setSubCheckbox(id, index) + if (rows.find(item => item.id === id)?.subItems && rows.find(item => item.id === id)?.subItems.length>0 && rows.find(item => item.id === id)?.subItems[0]) { + props.HandleFilterChange([paramFieldType, props.filterName, + label + "/" + rows.find(item => item.id === id).subItems[index] + ]); + } else { + props.HandleFilterChange([paramFieldType, props.filterName, + label + ]); + } }); }; @@ -38,7 +50,9 @@ const CollapsibleCheckboxes = (props) => { ...prev, [key]: !prev[key], })); - props.HandleFilterChange([paramFieldType, props.filterName, rows.find(item => item.id === mainId).label+"/"+rows.find(item => item.id === mainId).subItems[index]]); + props.HandleFilterChange([paramFieldType, props.filterName, + rows.find(item => item.id === mainId).label + "/" + rows.find(item => item.id === mainId).subItems[index] + ]); }; @@ -55,27 +69,27 @@ const CollapsibleCheckboxes = (props) => { />
{ setFilterEnabled(!filterEnabled); props.HandleFilterEnable([props.filterName, !filterEnabled]);}} + onToggle={() => { setFilterEnabled(!filterEnabled); props.HandleFilterEnable([props.filterName, !filterEnabled]); }} />
-
+
{ + if (!filterEnabled && checkboxRef.current) { + checkboxRef.current.click(); + } + }}>
{rows.map((row) => (
toggleExpand(row.id, row.subItems)} + id={`checkbox-${row?.id}`} + checked={expanded[row?.id] || false} + onChange={() => toggleExpand(row?.id, row?.subItems)} className="accent-violet-500 z-10" - /> + /> @@ -83,45 +97,45 @@ const CollapsibleCheckboxes = (props) => { 1) ? row.subItems.length : 0)) * 24 + 34 : 30}`} + viewBox={`0 0 40 ${expanded[row.id] ? (((!row?.subItems || row?.subItems?.length > 1) ? row.subItems.length : 0)) * 24 + 34 : 30}`} preserveAspectRatio="none" className="absolute left-[-25px] top-[-5px]" - > - {/*big horizontal line */ } + > + {/*big horizontal line */} 1) ? row.subItems.length : 0) * 24 + 33}`} stroke="white" strokeWidth={strokeWidth} fill="none" className="" /> - {/*top vertical line */ } + {/*top vertical line */} {row.id === 1 && ( + d={`M20 2 H33`} + stroke="white" + strokeWidth={strokeWidth} + fill="none" + className="" + /> )} - {/*bottom vertical line */ } + {/*bottom vertical line */} {row.id === rows.length && ( + d={`M20 ${(expanded[row.id] ? row.subItems.length : 1) * 34 - 8} H33`} + stroke="white" + strokeWidth={strokeWidth} + fill="none" + className="" + /> )} - + - {expanded[row.id] && ( + {expanded[row.id] && row.subItems.length > 1 && (
- {/*vertical line */ } + {/*vertical line */} { onChange={() => toggleSubCheckbox(row.id, index)} />
); diff --git a/my-app/src/views/Components/SideBarComponents/CourseTranscriptList.jsx b/my-app/src/views/Components/SideBarComponents/CourseTranscriptList.jsx index 46cb418..642c6f7 100644 --- a/my-app/src/views/Components/SideBarComponents/CourseTranscriptList.jsx +++ b/my-app/src/views/Components/SideBarComponents/CourseTranscriptList.jsx @@ -48,38 +48,42 @@ export default function CourseTranscriptList(props) {
)} +
- {/* Container for multiple items per row */} -
- {items.map((item, index) => ( -
- {item} -
+
+ {items.map((item, index) => ( +
- - - - + {item} + +
+ ))}
- ))}
); diff --git a/my-app/src/views/Components/SideBarComponents/DropDownField.jsx b/my-app/src/views/Components/SideBarComponents/DropDownField.jsx index 503806d..b416a4e 100644 --- a/my-app/src/views/Components/SideBarComponents/DropDownField.jsx +++ b/my-app/src/views/Components/SideBarComponents/DropDownField.jsx @@ -9,19 +9,18 @@ export default function DropDownField(props) { let paramFieldType = "dropdown"; const [filterEnabled, setFilterEnabled] = useState(props.filterEnable); const [isOpen, setIsOpen] = useState(false); - const [selectedItems, setSelectedItems] = useState(props.initialValues.map( - (item) => item.charAt(0).toUpperCase() + item.slice(1).toLowerCase() - )); + const [selectedItems, setSelectedItems] = useState(props.initialValues.map(t => t?.toUpperCase())); - const items = props.options; + const items = props.options.map(t => t?.toUpperCase()); + + const checkboxRef = useRef(null); const toggleDropdown = () => setIsOpen(!isOpen); const handleCheckboxChange = (item) => { setSelectedItems((prev) => - prev.includes(item) ? prev.filter((i) => i !== item) : [...prev, item] + prev.includes(item?.toUpperCase()) ? prev.filter((i) => i !== item) : [...prev, item] ); - console.log(item); props.HandleFilterChange([paramFieldType, props.filterName, item]); }; @@ -53,12 +52,16 @@ export default function DropDownField(props) { />
{ setFilterEnabled(!filterEnabled); props.HandleFilterEnable([props.filterName, !filterEnabled]); }} />
-
+
{ + if (!filterEnabled && checkboxRef.current) { + checkboxRef.current.click(); + } + }}>
{/* Dropdown Button */} @@ -66,7 +69,7 @@ export default function DropDownField(props) { onClick={toggleDropdown} className="bg-violet-500 text-white px-4 py-2 rounded-md shadow-md focus:outline-none hover:bg-[#aba8e0] w-full" > - {selectedItems.length? (selectedItems.join(", ").substring(0, 30) + ((selectedItems.join(", ").substring(0, 30).length>=30)? "...": "") ):"Select Options"} + {selectedItems.length? (selectedItems.map(i => String(i).charAt(0).toUpperCase() + String(i).slice(1).toLowerCase()).join(", ").substring(0, 30) + ((selectedItems.join(", ").substring(0, 30).length>=30)? "...": "") ):"Select Options"} {/* Dropdown Menu */} @@ -82,7 +85,7 @@ export default function DropDownField(props) { handleCheckboxChange(item)} className="mr-2 sr-only peer" /> @@ -92,7 +95,7 @@ export default function DropDownField(props) { peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-violet-500 ">
- {item} + {String(item).charAt(0).toUpperCase() + String(item).slice(1).toLowerCase()} ))} diff --git a/my-app/src/views/Components/SideBarComponents/FilterEnableCheckbox.jsx b/my-app/src/views/Components/SideBarComponents/FilterEnableCheckbox.jsx index fefa79a..d211298 100644 --- a/my-app/src/views/Components/SideBarComponents/FilterEnableCheckbox.jsx +++ b/my-app/src/views/Components/SideBarComponents/FilterEnableCheckbox.jsx @@ -1,16 +1,17 @@ -import React from 'react'; +import React, { forwardRef } from "react"; -const FilterEnableCheckbox = (props) => { +const FilterEnableCheckbox = forwardRef(({ initialValue, onToggle }, ref) => { return (
); -}; +}); export default FilterEnableCheckbox; \ No newline at end of file diff --git a/my-app/src/views/Components/SideBarComponents/ButtonGroupFullComponent.jsx b/my-app/src/views/Components/SideBarComponents/MultipleChoiceButtons.jsx similarity index 74% rename from my-app/src/views/Components/SideBarComponents/ButtonGroupFullComponent.jsx rename to my-app/src/views/Components/SideBarComponents/MultipleChoiceButtons.jsx index fb4f862..725abd7 100644 --- a/my-app/src/views/Components/SideBarComponents/ButtonGroupFullComponent.jsx +++ b/my-app/src/views/Components/SideBarComponents/MultipleChoiceButtons.jsx @@ -3,18 +3,28 @@ import { useRef, useEffect } from "react"; import FilterEnableCheckbox from "./FilterEnableCheckbox"; import Tooltip from "./ToolTip"; -export default function ButtonGroupFullComponent(props) { - const [filterEnabled, setFilterEnabled] = useState(props.filterEnable); +export default function MultipleChoiceButtons(props) { + const [filterEnabled, setFilterEnabled] = useState(false); const [selectedItems, setSelectedItems] = useState(props.initialValues || []); + const checkboxRef = useRef(null); + + useEffect(() => { + setFilterEnabled(props.filterEnable); + }) + const handleClick = (index) => { - const selectedItem = props.items[index]; - setSelectedItems((prevSelectedItems) => { - return prevSelectedItems.map((item, idx) => - idx === index ? !item : item - ); + setSelectedItems((prev) => { + if (!Array.isArray(prev) || index < 0 || index >= prev.length) { + console.warn("Invalid selectedItems or index:", prev, index); + return prev; + } + + const updated = [...prev]; + updated[index] = !updated[index]; + props.HandleFilterChange(["buttongroup", "period", index, updated[index]]); + return updated; }); - props.HandleFilterChange(["buttongroup", "period", index]); }; const getButtonClasses = (index) => { @@ -52,12 +62,16 @@ export default function ButtonGroupFullComponent(props) { />
{ setFilterEnabled(!filterEnabled); props.HandleFilterEnable([props.filterName, !filterEnabled]); }} />
-
+
{ + if (!filterEnabled && checkboxRef.current) { + checkboxRef.current.click(); + } + }}>
+
{ + if (!filterEnabled && checkboxRef.current) { + checkboxRef.current.click(); + } + }}>
Credits: {values[minIndex]} – {values[maxIndex]} diff --git a/my-app/src/views/Components/SideBarComponents/ToggleField.jsx b/my-app/src/views/Components/SideBarComponents/ToggleField.jsx index 1fda94b..1c4067b 100644 --- a/my-app/src/views/Components/SideBarComponents/ToggleField.jsx +++ b/my-app/src/views/Components/SideBarComponents/ToggleField.jsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import React, { useState, useRef } from "react"; import FilterEnableCheckbox from "./FilterEnableCheckbox"; import Tooltip from "./ToolTip"; @@ -10,6 +10,8 @@ export default function ToggleField(props) { const [prop1Set, setprop1Set] = useState((props.initialValues=="both") || (props.initialValues==String(props.fields[0]).charAt(0).toLowerCase() + String(props.fields[0]).slice(1))); const [prop2Set, setprop2Set] = useState((props.initialValues=="both") || (props.initialValues==String(props.fields[1]).charAt(0).toLowerCase() + String(props.fields[1]).slice(1))); + const checkboxRef = useRef(null); + return (
@@ -23,12 +25,17 @@ export default function ToggleField(props) { />
{ setFilterEnabled(!filterEnabled); props.HandleFilterEnable([props.filterName, !filterEnabled]); }} />
-
+
{ + if (!filterEnabled && checkboxRef.current) { + checkboxRef.current.click(); + } + }}>
-
-
{ + if (!filterEnabled && checkboxRef.current) { + checkboxRef.current.click(); + } + }}> +
- -
- + +
+
@@ -81,7 +86,7 @@ export default function UploadField(props) {
diff --git a/my-app/src/views/SidebarView.jsx b/my-app/src/views/SidebarView.jsx index 1fbcfe6..4130c9b 100644 --- a/my-app/src/views/SidebarView.jsx +++ b/my-app/src/views/SidebarView.jsx @@ -5,7 +5,7 @@ import DropDownField from "./Components/SideBarComponents/DropDownField.jsx"; import { UploadTranscriptPresenter } from '../presenters/UploadTranscriptPresenter.jsx'; import CollapsibleCheckboxes from './Components/SideBarComponents/CollapsibleCheckboxes.jsx'; import Tooltip from './Components/SideBarComponents/ToolTip.jsx'; -import ButtonGroupFullComponent from './Components/SideBarComponents/ButtonGroupFullComponent.jsx'; +import MultipleChoiceButtons from './Components/SideBarComponents/MultipleChoiceButtons.jsx'; @@ -51,7 +51,7 @@ function SidebarView(props) { /> -