diff --git a/src/components/Course/CatalogCourse.tsx b/src/components/Course/CatalogCourse.tsx index f2fadc5..296ec06 100644 --- a/src/components/Course/CatalogCourse.tsx +++ b/src/components/Course/CatalogCourse.tsx @@ -1,9 +1,19 @@ import React, { useState } from "react"; -import Tag from "./CatalogCourse/Tag"; import AddButton from "./CatalogCourse/AddButton"; import { motion } from "framer-motion"; import { APICourse } from "../../types/interfaces/Course.interface"; import { useCourseWorkspace } from "../../hooks/useCourseWorkspace"; +import { useFilterData } from "../../hooks/useFilters"; +import { FilterData } from "../../types/interfaces/Filters.interface"; +import Tag from "../Tag"; + +const findFiltersForCourse = ( + api_list: string[], + filterDataType: FilterData[], +) => { + console.log("API List:", api_list); + return filterDataType.filter((attr) => api_list.includes(attr.code)); +}; interface CourseProps { course: APICourse; @@ -11,6 +21,10 @@ interface CourseProps { const Course: React.FC = ({ course }) => { const { toolboxCourses, setToolboxCourses } = useCourseWorkspace(); + const { attributes, semesters } = useFilterData(); + + const attrFilters = findFiltersForCourse(course.attr_list || [], attributes); + const semFilters = findFiltersForCourse(course.sem_list || [], semesters); const [isOpen, setIsOpen] = useState(false); const toggleOpen = (e: React.MouseEvent) => { @@ -58,7 +72,7 @@ const Course: React.FC = ({ course }) => {

{courseCount}

@@ -71,26 +85,12 @@ const Course: React.FC = ({ course }) => {

{toTitleCase(course.title)}

-
- {course.attr_list?.map((attr, index) => { - return ( - - ); +
+ {attrFilters.map((attr, index) => { + return ; })} - {course.sem_list?.map((semester, index) => { - return ( - - ); + {semFilters?.map((semester, index) => { + return ; })}
diff --git a/src/components/Course/CatalogCourse/Tag.tsx b/src/components/Course/CatalogCourse/Tag.tsx deleted file mode 100644 index cf5d581..0000000 --- a/src/components/Course/CatalogCourse/Tag.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; - -interface TagProp { - name: string; - bgcolor: string; - color: string; -} -const Tag: React.FC = ({ name, bgcolor, color }) => { - return ( -
- {name} -
- ); -}; - -export default Tag; diff --git a/src/components/Course/ToolboxCourse.tsx b/src/components/Course/ToolboxCourse.tsx index 7f137d3..481db12 100644 --- a/src/components/Course/ToolboxCourse.tsx +++ b/src/components/Course/ToolboxCourse.tsx @@ -53,7 +53,7 @@ const ToolboxCourse: React.FC = ({ className={`relative bg-carpipink text-nowrap rounded-md w-fit px-3 py-1`} >
diff --git a/src/components/Department-Filters.tsx b/src/components/Department-Filters.tsx index ea1ca61..e098c7a 100644 --- a/src/components/Department-Filters.tsx +++ b/src/components/Department-Filters.tsx @@ -5,19 +5,19 @@ const DepartmentFilters: React.FC = () => { const { toggleFilter, subjects } = useFilterData(); return ( -
+
{subjects.map((subject) => ( ))} -
+
); }; diff --git a/src/components/SearchBar/ChosenTag.tsx b/src/components/SearchBar/ChosenTag.tsx deleted file mode 100644 index eff9e0c..0000000 --- a/src/components/SearchBar/ChosenTag.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from "react"; -import { IoClose } from "react-icons/io5"; -import { useFilterData } from "../../hooks/useFilters"; -import { FilterData } from "../../types/interfaces/Filters.interface"; - -interface ChosenTagProp { - filter: FilterData; -} - -const ChosenTag: React.FC = ({ filter }) => { - const { toggleFilter } = useFilterData(); - return ( -
- {filter.value} - -
- ); -}; - -export default ChosenTag; diff --git a/src/components/SearchBar/FilterPanel/FilterPanel.tsx b/src/components/SearchBar/FilterPanel/FilterPanel.tsx index a27a7e3..6c47390 100644 --- a/src/components/SearchBar/FilterPanel/FilterPanel.tsx +++ b/src/components/SearchBar/FilterPanel/FilterPanel.tsx @@ -1,40 +1,40 @@ import { useFilterData } from "../../../hooks/useFilters"; import FilterSection from "./FilterSection"; +// import FilterSectionChild from "./FilterSectionChild"; const FilterPanel: React.FC = () => { - const { selectedFilters, toggleFilter, subjects, attributes, semesters } = - useFilterData(); + const { selectedFilters, subjects, attributes, semesters } = useFilterData(); const selectedSubjects = selectedFilters .filter((f) => f.type === "Subject") .map((f) => f.code); const selectedAttributes = selectedFilters - .filter((f) => f.type === "Attributes") + .filter((f) => f.type === "Attribute") .map((f) => f.code); const selectedSemesters = selectedFilters - .filter((f) => f.type === "Semesters") + .filter((f) => f.type === "Semester") .map((f) => f.code); return ( -
+
+

Filter By

+
); }; diff --git a/src/components/SearchBar/FilterPanel/FilterPanelPopup.tsx b/src/components/SearchBar/FilterPanel/FilterPanelPopup.tsx index 226325a..c57dbdd 100644 --- a/src/components/SearchBar/FilterPanel/FilterPanelPopup.tsx +++ b/src/components/SearchBar/FilterPanel/FilterPanelPopup.tsx @@ -3,7 +3,7 @@ import FilterPanel from "./FilterPanel"; const FilterPanelPopup: React.FC = () => { return ( -
+
); diff --git a/src/components/SearchBar/FilterPanel/FilterSection.tsx b/src/components/SearchBar/FilterPanel/FilterSection.tsx index f6f6092..94623fe 100644 --- a/src/components/SearchBar/FilterPanel/FilterSection.tsx +++ b/src/components/SearchBar/FilterPanel/FilterSection.tsx @@ -1,34 +1,82 @@ -import React from "react"; -import { FilterData } from "../../../types/interfaces/Filters.interface"; +import React, { useState } from "react"; +import { motion } from "framer-motion"; +import { + FilterCategory, + FilterData, +} from "../../../types/interfaces/Filters.interface"; +import useIsDesktop from "../../../hooks/useIsDesktop"; +import Tag from "../../Tag"; interface FilterSectionProps { - sectionName: "Subject" | "Attributes" | "Semesters"; + sectionName: FilterCategory; tags: FilterData[]; selected: string[]; - toggleFilter: (filter: FilterData) => void; + showCode?: boolean; } const FilterSection: React.FC = ({ sectionName, tags, selected, - toggleFilter, + showCode = false, }) => { + const isDesktop = useIsDesktop(); + const [isExpanded, setIsExpanded] = useState(false); + + const COLLAPSED_HEIGHT = "6rem"; + const TAG_THRESHOLD = 6; + const shouldTruncate = tags.length > TAG_THRESHOLD; + return (
-

{sectionName}

-
- {tags.map((tag) => ( - - ))} +

{sectionName}

+ +
+ + {tags.map((tag) => ( + + ))} + + + {isDesktop && !isExpanded && shouldTruncate && ( +
+ + {/* Toggle Button */} + {shouldTruncate && ( + + )}
); }; diff --git a/src/components/SearchBar/FilterPanel/FilterSectionChild.tsx b/src/components/SearchBar/FilterPanel/FilterSectionChild.tsx new file mode 100644 index 0000000..7fd161e --- /dev/null +++ b/src/components/SearchBar/FilterPanel/FilterSectionChild.tsx @@ -0,0 +1,54 @@ +import React, { useState } from "react"; +import { + FilterCategory, + FilterData, +} from "../../../types/interfaces/Filters.interface"; + +interface FilterSectionProps { + sectionName: FilterCategory; + tags: FilterData[]; + selected: string[]; + toggleFilter: (filter: FilterData) => void; +} + +const FilterSectionChild: React.FC = ({ + sectionName, + tags, + selected, + toggleFilter, +}) => { + const [isExpanded, setIsExpanded] = useState(false); + + return ( +
+

setIsExpanded(true)} + onMouseLeave={() => setIsExpanded(false)} + > + {sectionName} +

+ + {isExpanded && ( +
+ {tags.map((tag) => ( + + ))} +
+ )} +
+ ); +}; + +export default FilterSectionChild; diff --git a/src/components/SearchBar/SeachBar.tsx b/src/components/SearchBar/SearchBar.tsx similarity index 73% rename from src/components/SearchBar/SeachBar.tsx rename to src/components/SearchBar/SearchBar.tsx index b673867..0b62558 100644 --- a/src/components/SearchBar/SeachBar.tsx +++ b/src/components/SearchBar/SearchBar.tsx @@ -1,23 +1,28 @@ -import React, { useState, useEffect, useRef } from "react"; +import React, { useEffect, useRef } from "react"; import { IoClose, IoSearchOutline, IoFilter } from "react-icons/io5"; import FilterPanel from "./FilterPanel/FilterPanel.tsx"; import useIsDesktop from "../../hooks/useIsDesktop.ts"; import FilterPanelPopup from "./FilterPanel/FilterPanelPopup.tsx"; +import { useFilterData } from "../../hooks/useFilters.ts"; +import Tag from "../Tag.tsx"; interface SearchBarProps { searchPrompt: string; setSearchPrompt: React.Dispatch>; + showFilter: boolean; + setShowFilter: React.Dispatch>; } const SearchBar: React.FC = ({ searchPrompt, setSearchPrompt, + showFilter, + setShowFilter, }) => { + const { selectedFilters } = useFilterData(); const isDesktop = useIsDesktop(); const componentRef = useRef(null); - const [showFilter, setShowFilter] = useState(false); - const handleInputChange = (e: React.ChangeEvent) => { setSearchPrompt(e.target.value); setShowFilter(false); @@ -37,14 +42,14 @@ const SearchBar: React.FC = ({ return () => { document.removeEventListener("mousedown", handleClickOutside); }; - }, []); + }, [setShowFilter]); return ( -
-
+
+
= ({ {showFilter && isDesktop && }
+ {!isDesktop && selectedFilters.length > 0 && ( +
+ {selectedFilters.map((filter) => ( + + ))} +
+ )} {showFilter && !isDesktop && }
); diff --git a/src/components/Tag.tsx b/src/components/Tag.tsx new file mode 100644 index 0000000..7a9f690 --- /dev/null +++ b/src/components/Tag.tsx @@ -0,0 +1,106 @@ +import React from "react"; +import { IoClose } from "react-icons/io5"; +import { FilterData } from "../types/interfaces/Filters.interface"; +import { useFilterData } from "../hooks/useFilters"; +import useIsDesktop from "../hooks/useIsDesktop"; + +interface ColorClass { + selected_border: string; + selected_bg: string; + hover_bg: string; +} + +const getColorClass = (type: string): ColorClass => { + switch (type) { + case "Subject": + return { + selected_border: "border-burgundy", + selected_bg: "bg-burgundy", + hover_bg: + "hover:bg-[color-mix(in_oklab,var(--color-burgundy)_90%,white_10%)]", + }; + case "Attribute": + return { + selected_border: "border-slategray", + selected_bg: "bg-slategray", + hover_bg: + "hover:bg-[color-mix(in_oklab,var(--color-slategray)_90%,white_10%)]", + }; + case "Semester": + return { + selected_border: "border-copperwood", + selected_bg: "bg-copperwood", + hover_bg: + "hover:bg-[color-mix(in_oklab,var(--color-copperwood)_90%,white_10%)]", + }; + default: + return { + selected_border: "border-darkblue", + selected_bg: "bg-darkblue", + hover_bg: + "hover:bg-[color-mix(in_oklab,var(--color-darkblue)_90%,white_10%)]", + }; + } +}; + +interface TagProps { + filter: FilterData; + + // optional props for behavior and style + isSelectable?: boolean; + isSelected?: boolean; + showCode?: boolean; + isRemovable?: boolean; +} + +const Tag: React.FC = ({ + filter, + showCode = false, + isSelectable = false, + isSelected = false, + isRemovable = false, +}) => { + const { toggleFilter } = useFilterData(); + const isDesktop = useIsDesktop(); + + // Determine content: value or code + const content = showCode ? filter.code : filter.value; + const selectedValue = isSelectable ? isSelected : true; + + // Determine styles + const baseClass = + "rounded-full px-3 py-1 text-sm inline-flex items-center transition-colors text-carpipink"; + const accentColor = getColorClass(filter.type); + const colorClasses = selectedValue + ? `${accentColor.selected_border} ${accentColor.selected_bg} ${accentColor.hover_bg}` + : isDesktop + ? `border-carpipink ${accentColor.hover_bg}` + : `border-darkblue text-darkblue ${accentColor.hover_bg}`; + + const handleClick = () => { + if (isSelectable || isRemovable) { + toggleFilter(filter); + } + }; + + return ( + + ); +}; + +export default Tag; diff --git a/src/components/Toolbox/NavButton.tsx b/src/components/Toolbox/NavButton.tsx index 2beda65..ce8ec6a 100644 --- a/src/components/Toolbox/NavButton.tsx +++ b/src/components/Toolbox/NavButton.tsx @@ -5,14 +5,12 @@ const NavButton = () => { const path = location.pathname; return ( - <> - - {path == "/" ? "Go to Planner" : "Go to Courses"} - - + + {path == "/" ? "Go to Planner" : "Go to Courses"} + ); }; diff --git a/src/components/Toolbox/Toolbox.tsx b/src/components/Toolbox/Toolbox.tsx index cfedc70..c4e46fa 100644 --- a/src/components/Toolbox/Toolbox.tsx +++ b/src/components/Toolbox/Toolbox.tsx @@ -45,9 +45,9 @@ const Toolbox: React.FC = () => { )}
{ > -
-
-

TOOLBOX

- -
- {count > 0 && ( -
- {count} -
- )} -
-
- - {(provided) => ( -
- {toolboxCourses.map((course, index) => ( - - ))} - {provided.placeholder} +
+
+

TOOLBOX

+ +
+ {count > 0 && ( +
+ {count}
)} - +
+ +
+ + {(provided) => ( +
+ {toolboxCourses.map((course, index) => ( + + ))} + {provided.placeholder} +
+ )} +
+
diff --git a/src/components/Toolbox/ToolboxButton.tsx b/src/components/Toolbox/ToolboxButton.tsx index 00427ca..2e0df53 100644 --- a/src/components/Toolbox/ToolboxButton.tsx +++ b/src/components/Toolbox/ToolboxButton.tsx @@ -20,7 +20,7 @@ const ToolboxButton: React.FC = ({

{count}

diff --git a/src/context/FilterProvider.tsx b/src/context/FilterProvider.tsx index b29e9ab..2a23fe1 100644 --- a/src/context/FilterProvider.tsx +++ b/src/context/FilterProvider.tsx @@ -34,8 +34,8 @@ export const FilterProvider: React.FC<{ children: ReactNode }> = ({ ]); setSubjects(formatApiData("Subject", subjectsResponse.data)); - setAttributes(formatApiData("Attributes", attributesResponse.data)); - setSemesters(formatApiData("Semesters", semestersResponse.data)); + setAttributes(formatApiData("Attribute", attributesResponse.data)); + setSemesters(formatApiData("Semester", semestersResponse.data)); } catch (error) { console.error("Failed to fetch filters:", error); } diff --git a/src/index.css b/src/index.css index 9f5c249..cdf56e0 100644 --- a/src/index.css +++ b/src/index.css @@ -2,10 +2,14 @@ @plugin 'tailwind-scrollbar'; @theme { - --color-carpipink: oklch(88.46% 0.0441 18.02); - --color-darkblue: oklch(31.09% 0.0378 267.38); - --color-darkblue-40: oklch(31.09% 0.0378 267.38/40%); - --color-lightblue: oklch(68.85% 0.0588 236.57); + --color-carpipink: oklch(88.46% 0.0441 18.02); /* #F5CECE */ + --color-steelblue: oklch(68.85% 0.0588 236.57); /* #78a1bb */ + --color-dustygrape: oklch(49.23% 0.0665 275.21); /* #565E87 */ + --color-darkblue: oklch(31.09% 0.0378 267.38); /* #283044 */ + --color-mutedteal: oklch(77.97% 0.0441 181.55); /* #99C1B9 */ + --color-burgundy: oklch(36.07% 0.1294 18.64); /* #721121 */ + --color-slategray: oklch(54.39% 0.0444 212.21); /* #51777F */ + --color-copperwood: oklch(53.68% 0.1081 63.18); /* #AA6822 */ --color-rosewood: oklch(54.14% 0.206 10.45); /* #c91752 */ } diff --git a/src/pages/Catalog.tsx b/src/pages/Catalog.tsx index 75e57cd..722bb60 100644 --- a/src/pages/Catalog.tsx +++ b/src/pages/Catalog.tsx @@ -1,19 +1,22 @@ import React, { useState, useRef, useEffect } from "react"; import api from "../axios.ts"; import Course from "../components/Course/CatalogCourse.tsx"; -import SearchBar from "../components/SearchBar/SeachBar"; +import SearchBar from "../components/SearchBar/SearchBar"; import { APICourse } from "../types/interfaces/Course.interface.ts"; import DepartmentFilters from "../components/Department-Filters.tsx"; -import ChosenTag from "../components/SearchBar/ChosenTag"; import { useFilterData } from "../hooks/useFilters.ts"; +import useIsDesktop from "../hooks/useIsDesktop.ts"; +import Tag from "../components/Tag.tsx"; const Catalog: React.FC = () => { const { selectedFilters } = useFilterData(); + const isDesktop = useIsDesktop(); const [searchResults, setSearchResults] = React.useState([]); const [searchPrompt, setSearchPrompt] = useState(""); const [isLoading, setIsLoading] = useState(false); const [hasSearched, setHasSearched] = useState(false); + const [showFilter, setShowFilter] = useState(false); const debounceTimeout = useRef | null>(null); const lastNoResultQuery = useRef(null); @@ -39,10 +42,10 @@ const Catalog: React.FC = () => { .filter((filter) => filter.type === "Subject") .map((filter) => filter.code); const attrFilters = selectedFilters - .filter((filter) => filter.type === "Attributes") + .filter((filter) => filter.type === "Attribute") .map((filter) => filter.code); const semFilters = selectedFilters - .filter((filter) => filter.type === "Semesters") + .filter((filter) => filter.type === "Semester") .map((filter) => filter.code); if (searchPrompt.length < 3) { @@ -89,68 +92,74 @@ const Catalog: React.FC = () => { }, [selectedFilters, searchPrompt, setSearchResults]); return ( -
-
+
+

Courses

-
- {selectedFilters.map((filter) => ( - - ))} -
+ {isDesktop && ( +
+ {selectedFilters.map((filter) => ( + + ))} +
+ )}
-
- {searchResults.length > 0 ? ( -
- {searchResults?.map((course: APICourse, index: number) => ( - - ))} + {(isDesktop || !showFilter) && ( +
+ {searchResults.length > 0 ? ( +
+ {searchResults?.map((course: APICourse, index: number) => ( + + ))} -
-
- ) : isLoading ? ( - Array.from({ length: 6 }).map((_, i) => ( -
-
-
-
- -
- {Array.from({ length: 4 }).map((_, j) => ( -
- ))} +
+
+ ) : isLoading ? ( + Array.from({ length: 6 }).map((_, i) => ( +
+
+
+
+ +
+ {Array.from({ length: 4 }).map((_, j) => ( +
+ ))} +
-
-
+
+
+ )) + ) : hasSearched ? ( +
+

D:

+

+ No courses found for "{searchPrompt}" +

+

+ Try searching for another course.
+ Maybe "CSCI 1100" or "Computer Science I" +

- )) - ) : hasSearched ? ( -
-

D:

-

- No courses found for "{searchPrompt}" -

-

- Try searching for another course.
- Maybe "CSCI 1100" or "Computer Science I" -

-
- ) : ( - selectedFilters.length === 0 && - )} -
+ ) : ( + selectedFilters.length === 0 && + )} +
+ )}
); }; diff --git a/src/types/interfaces/Filters.interface.ts b/src/types/interfaces/Filters.interface.ts index bddc73a..2074a8a 100644 --- a/src/types/interfaces/Filters.interface.ts +++ b/src/types/interfaces/Filters.interface.ts @@ -1,9 +1,9 @@ -export type FilterCategory = "Subject" | "Attributes" | "Semesters"; +export type FilterCategory = "Subject" | "Attribute" | "Semester"; export interface Filters { Subject: string[]; - Attributes: string[]; - Semesters: string[]; + Attribute: string[]; + Semester: string[]; } export interface FilterData {