diff --git a/public/index.html b/public/index.html index 2899d0d..78beb3f 100644 --- a/public/index.html +++ b/public/index.html @@ -6,7 +6,7 @@ BroncoDirectMe - +
diff --git a/src/App.tsx b/src/App.tsx index 4192d3d..e9663ad 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,60 +4,105 @@ import SearchBar from './components/SearchBar'; import { MsalProvider } from '@azure/msal-react'; // eslint-disable-next-line @typescript-eslint/no-unused-vars import { msalInstance, MicrosoftOAuth } from './components/MicrosoftOath'; -import { Box, IconButton } from '@mui/material'; +import { + Box, + IconButton, + BottomNavigation, + BottomNavigationAction, + Tooltip, +} from '@mui/material'; import SettingsIcon from '@mui/icons-material/Settings'; -import { Panel } from './components/panel_component'; +import BugReportIcon from '@mui/icons-material/BugReport'; import DegreeProgressBar from './components/DegreeProgressBar'; import './styles/App.css'; import UpdateAlert from './components/UpdateAlert'; import TermsOfService from './components/TermsOfService'; +import CourseSearchBar from './components/CourseSearchBar'; +import HomeIcon from '@mui/icons-material/Home'; +import SearchIcon from '@mui/icons-material/Search'; /** * @returns Main app component */ export function App(): ReactElement { - const [isPanelOpen, setPanelState] = React.useState(false); - const togglePanel = (): void => setPanelState(!isPanelOpen); - const [isSettingsButtonOpen, setSettingsButtonState] = React.useState(true); + const [value, setValue] = React.useState('home'); + + const handleChange = ( + event: React.SyntheticEvent, + newValue: string + ): void => { + setValue(newValue); + }; + + const handleBugReportClick = (): void => { + // Specify the URL you want to open in a new tab + const url = + 'https://docs.google.com/forms/d/e/1FAIpQLSfEAQ5xbzU98fxRBaQgxKv01pEU07_ALcrJU-lGmOdIhKvkAw/viewform'; + // Open a new tab with the specified URL + window.open(url, '_blank'); + }; return (
- {!isPanelOpen && ( -
- - -
- -

BroncoDirectMe Search

- {isSettingsButtonOpen && ( - { - togglePanel(); - setSettingsButtonState(false); - }} - id="settingsButton" - > - +
+ + +
+ +

BroncoDirectMe

+
+ + + - )} - - - {/* */} - -
- )} - {/* Hides main app components when setting panel opens */} - { - togglePanel(); - setSettingsButtonState(true); - }} + +
+ + {value === 'home' && ( + <> + + + + )} + + {value === 'search' && ( + <> + + {/* Additional components for Search Courses tab */} + + )} + + {/* Settings components */} + {value === 'settings' && ( + <> + + + )} + {/* */} + + - - + } + /> + } + /> + } + /> +
); diff --git a/src/components/CourseSearchBar.tsx b/src/components/CourseSearchBar.tsx new file mode 100644 index 0000000..f93d2a8 --- /dev/null +++ b/src/components/CourseSearchBar.tsx @@ -0,0 +1,159 @@ +import React, { useState, useEffect } from 'react'; +import { TextField, Autocomplete, CircularProgress } from '@mui/material'; + +interface CourseInfo { + id: string; + courseName: string; + courseNumber: string; + preReqs: string; + coReqs: string; + units: string; +} + +const CourseSearchBar: React.FC = () => { + const [open, setOpen] = useState(false); + const [searchText, setSearchText] = useState(''); + const [options, setOptions] = useState([]); + const [selectedCourse, setSelectedCourse] = useState(null); + const [loading, setLoading] = useState(false); + + useEffect(() => { + const delayDebounce = setTimeout(() => { + if (searchText === '') { + // setOptions([]); + return; + } + + setLoading(true); + fetch( + // change to prod api url when the backend endpoints are updated + // `https://api.cppbroncodirect.me/courses?key=${encodeURIComponent(searchText))}` + `http://localhost:3000/courses?key=${encodeURIComponent(searchText)}` + ) + .then(async (response) => await response.json()) + .then((data) => { + setOptions(data); + }) + .catch((error) => { + console.error(`Error fetching courses:`, error); + }) + .finally(() => { + setLoading(false); + }); + }, 2000); // 2-second delay + + return () => clearTimeout(delayDebounce); + }, [searchText]); + + const fetchCourseDetails = async (courseNumber: string): Promise => { + try { + const response = await fetch( + // change to prod api url when the backend endpoints are updated + // `https://api.cppbroncodirect.me/courses/${courseNumber}` + `http://localhost:3000/courses/${courseNumber}` + ); + if (!response.ok) { + throw new Error('Network response was not ok'); + } + const data: CourseInfo = await response.json(); + setSelectedCourse(data); + } catch (error) { + console.error('Error fetching course details:', error); + setSelectedCourse(null); + } + }; + + return ( +
+ { + setOpen(true); + }} + onClose={() => { + setOpen(false); + }} + getOptionLabel={(option) => + `${option.courseNumber}: ${option.courseName}` + } + options={options} + loading={loading} + onChange={(event, newValue: CourseInfo | null) => { + if (newValue) { + fetchCourseDetails(newValue.courseNumber).catch((e) => {}); + } + }} + renderInput={(params) => ( + setSearchText(e.target.value)} + placeholder="Search for a course" + variant="outlined" + InputProps={{ + ...params.InputProps, + endAdornment: ( + <> + {loading && } + {params.InputProps.endAdornment} + + ), + }} + /> + )} + /> + {selectedCourse && ( +
+

{selectedCourse.courseName}

+ + {( + [ + { name: 'Course ID', value: selectedCourse.courseNumber }, + { name: 'Units', value: selectedCourse.units }, + { + name: 'Prerequisites', + value: selectedCourse.preReqs, + hidden: !selectedCourse.preReqs, + }, + { + name: 'Corequisites', + value: selectedCourse.coReqs, + hidden: !selectedCourse.coReqs, + }, + ] as CourseFieldProps[] + ).map((field, index) => ( + + ))} + +
+ )} +
+ ); +}; + +export default CourseSearchBar; + +interface CourseFieldProps { + name: string; + value: string; + hidden?: boolean; +} +const CourseField: React.FC = ({ + name, + value, + hidden = false, +}) => { + return hidden ? null : ( +

+ {name}: {value} +

+ ); +}; diff --git a/src/styles/App.css b/src/styles/App.css index ec73589..4e0f92b 100644 --- a/src/styles/App.css +++ b/src/styles/App.css @@ -1,18 +1,53 @@ .App { - min-height: 250px; + height: 400px; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.App section { + padding: 3vh 5vw; + overflow-y: auto; + flex-grow: 1; } #mainContent { display: flex; justify-content: space-between; - padding-left: 5vw; - padding-right: 5vw; + align-items: center; +} + +#mainButtons { + display: flex; + align-items: center; + gap: 4px; +} + +#bugReportButton { + padding: 0; + border-radius: 6px; + height: fit-content; +} + +#bugReportIcon { + font-size: 2rem; + padding: 2px; + color: #bf0a30; } #settingsButton { padding: 0; + border-radius: 6px; + height: fit-content; } #settingsIcon { font-size: 2rem; + padding: 2px; + color: #2b7dff; +} + +#bottomNavBar { + border-top: solid 1px #989898b0; + flex-shrink: 0; } diff --git a/src/styles/SearchBar.css b/src/styles/SearchBar.css index bd9e362..5141567 100644 --- a/src/styles/SearchBar.css +++ b/src/styles/SearchBar.css @@ -50,15 +50,14 @@ } .search-bar { - width: 85vw; - margin-bottom: 10%; - margin-left: 5vw; - box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); + width: 100%; + margin-bottom: 3vh; } .loading-container { display: flex; - justify-content: space-between; + justify-content: center; + align-items: center; margin-bottom: 20px; }