Skip to content

Commit f68a605

Browse files
committed
added course cart support, filter indicators, optimized api calls, fixed bugs
1 parent a0a7f2b commit f68a605

11 files changed

Lines changed: 310 additions & 192 deletions

File tree

frontend/review/src/components/CourseResults.js

Lines changed: 48 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -131,34 +131,40 @@ const SpecialPromptContainer = styled.div`
131131
padding: 40px 0;
132132
`;
133133

134-
const SemesterBanner = styled.div`
134+
const InfoBanner = styled.div`
135135
display: flex;
136136
align-items: center;
137137
gap: 8px;
138138
width: 100%;
139139
padding: 10px 14px;
140140
border-radius: 8px;
141-
background: #EBF5FF;
142-
border: 1px solid #B3D7F5;
143-
color: #1A6FAF;
141+
background: ${props => props.$isError ? '#ffebeb' : '#EBF5FF'};
142+
border: 1px solid ${props => props.$isError ? '#f5b3b3' : '#B3D7F5'};
143+
color: ${props => props.$isError ? '#ff6b6e' : '#1A6FAF'};
144144
font-size: 14px;
145145
font-weight: 400;
146146
`;
147147

148148
const numFiltersChanged = (filters) => {
149149
let count = 0;
150+
let onlySemesterChanged = true;
150151
for(const [key, value] of Object.entries(filters)) {
151152
if (Array.isArray(value)) {
152153
const isDifferentArray = value.length !== DEFAULT_FILTERS[key].length || !value.every(v => DEFAULT_FILTERS[key].includes(v));
153154
if (isDifferentArray) {
154155
count++;
156+
onlySemesterChanged = false;
155157
}
156158
} else {
157159
if (value !== DEFAULT_FILTERS[key]) {
158160
count++;
161+
if (key !== 'semester') onlySemesterChanged = false;
159162
}
160163
}
161164
}
165+
if (onlySemesterChanged) {
166+
return 0;
167+
}
162168
return count;
163169
}
164170

@@ -204,8 +210,8 @@ const formatFiltersForAPI = (filters) => {
204210
return formatted;
205211
}
206212

207-
const CourseResults = ({ filters, setFilters }) => {
208-
const [subjectSlice, setSubjectSlice] = useState({ start: 0, end: 100 });
213+
const CourseResults = ({ filters, setFilters, autocompleteData }) => {
214+
const [subjectSlice, setSubjectSlice] = useState({ start: 0, end: 101 });
209215

210216
const [departments, setDepartments] = useState([]);
211217
const [filteredResults, setFilteredResults] = useState(numFiltersChanged(filters) > 0 ? {} : null);
@@ -223,14 +229,12 @@ const CourseResults = ({ filters, setFilters }) => {
223229
const isAuth = useContext(AuthContext);
224230

225231
useEffect(() => {
226-
apiAutocomplete()
227-
.then(data => {
228-
setDepartments(data.departments);
229-
})
230-
.catch(error => {
231-
console.error("Error fetching autocomplete data:", error);
232-
});
233-
}, []);
232+
if (autocompleteData) {
233+
console.log("Setting departments from autocomplete data:", autocompleteData.departments);
234+
setDepartments(autocompleteData.departments);
235+
return;
236+
}
237+
}, [autocompleteData]);
234238

235239
const fetchCourses = useCallback((filters, page, append = false) => {
236240
if (isLoadingRef.current) return;
@@ -243,6 +247,7 @@ const CourseResults = ({ filters, setFilters }) => {
243247
const ff = formatFiltersForAPI(filters);
244248
apiCourseSearch(ff.semester, ff.attributes, ff.difficulty, ff.course_quality, ff.instructor_quality, ff.days, ff.time, ff.departments, page)
245249
.then(data => {
250+
console.log(data.results)
246251
const newResults = (data.results || []).reduce((acc, course) => {
247252
acc[course.id] = course;
248253
return acc;
@@ -334,12 +339,12 @@ const CourseResults = ({ filters, setFilters }) => {
334339
/>
335340
</SearchResultsHeader>
336341
{getActiveSemesterFilters(filters).length > 0 && (
337-
<SemesterBanner>
342+
<InfoBanner $isError={false}>
338343
<i className="fa fa-info-circle" />
339344
<span>
340345
Filtering by {getActiveSemesterFilters(filters).map(k => SEMESTER_FILTER_LABELS[k]).join(', ')} — results limited to current semester offerings.
341346
</span>
342-
</SemesterBanner>
347+
</InfoBanner>
343348
)}
344349
<div style={{width: '100%', height: '100%'}}>
345350
<CourseResultsTable
@@ -351,7 +356,17 @@ const CourseResults = ({ filters, setFilters }) => {
351356
</div>
352357
</>
353358
) : (
354-
<p>No results found.</p>
359+
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', padding: '20px', justifyContent: 'center', width: '100%' }}>
360+
<InfoBanner $isError={true} style={{ justifyContent: 'center' }}>
361+
<i className="fa fa-info-circle" />
362+
<span>
363+
No results found! Try adjusting your filters.
364+
</span>
365+
{filters.semester == "Next Available" && (
366+
<i>Matching results may exist for previous semesters (Try "All")</i>
367+
)}
368+
</InfoBanner>
369+
</div>
355370
)
356371
)}
357372
</>
@@ -372,23 +387,27 @@ const CourseResults = ({ filters, setFilters }) => {
372387
<BrowsingTitle>Browsing {departments.length} Subjects</BrowsingTitle>
373388
<SubjectDisplayWrapper>
374389
{departments.slice(subjectSlice.start, subjectSlice.end).map((dept, index) => (
375-
<SubjectCard key={index}>
376-
<div style={{ width: '60px', flexShrink: 0 }}>
377-
<LinkText onClick={() => {
378-
setFilters({ ...filters, departments: [...filters.departments, dept.title] });
379-
}}>{dept.title}</LinkText>
380-
</div>
381-
<DescText>{dept.desc}</DescText>
382-
</SubjectCard>
390+
<React.Fragment key={index}>
391+
{dept.desc != null && dept.desc != "" ? (
392+
<SubjectCard key={index}>
393+
<div style={{ width: '60px', flexShrink: 0 }}>
394+
<LinkText onClick={() => {
395+
setFilters({ ...filters, departments: [...filters.departments, dept.title] });
396+
}}>{dept.title}</LinkText>
397+
</div>
398+
<DescText>{dept.desc}</DescText>
399+
</SubjectCard>
400+
) : null}
401+
</React.Fragment>
383402
))}
384403
</SubjectDisplayWrapper>
385404
<PaginationContainer>
386405
<ResponsivePagination
387-
current={Math.floor(subjectSlice.start / 100) + 1}
388-
total={Math.ceil(departments.length / 100)}
406+
current={Math.floor(subjectSlice.start / 101) + 1}
407+
total={Math.ceil(departments.length / 101)}
389408
onPageChange={(page) => {
390-
const start = (page - 1) * 100;
391-
const end = start + 100;
409+
const start = (page - 1) * 101;
410+
const end = start + 101;
392411
setSubjectSlice({ start, end });
393412
}}
394413
/>

frontend/review/src/components/FilterBox.js

Lines changed: 52 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -83,16 +83,23 @@ const ResetButton = styled.button`
8383
}
8484
`;
8585

86-
const FilterDropdown = ({ title, renderContent }) => {
86+
const FilterDropdown = ({ title, renderContent, active }) => {
8787
const [isOpen, setIsOpen] = useState(false);
8888

8989
return (
9090
<DropdownWrapper $isOpen={isOpen} id={`dropdown-${title}`}>
9191
<FilterDropdownContainer onMouseDown={() => setIsOpen(!isOpen)}>
9292
<p>{title}</p>
93-
<motion.div animate={{ rotate: isOpen ? 90 : 0, display: 'flex', alignItems: 'center' }}>
94-
<SlArrowRight size={15} color="#6D6F71" />
95-
</motion.div>
93+
94+
<div style={{ display: 'flex', alignItems: 'center', gap: '30px' }}>
95+
{active && (
96+
<div style={{ width: '6px', height: '6px', borderRadius: '3px', backgroundColor: '#6D6F71', display: 'inline-block', marginLeft: '6px' }} />
97+
)}
98+
<motion.div animate={{ rotate: isOpen ? 90 : 0, display: 'flex', alignItems: 'center' }}>
99+
<SlArrowRight size={15} color="#6D6F71" />
100+
</motion.div>
101+
</div>
102+
96103
</FilterDropdownContainer>
97104

98105
<AnimatePresence initial={false}>
@@ -115,64 +122,75 @@ const FilterDropdown = ({ title, renderContent }) => {
115122
);
116123
}
117124

118-
const FilterBox = ({ filters, setFilters }) => {
125+
const FilterBox = ({ filters, setFilters, autocompleteData }) => {
119126
const [departments, setDepartments] = useState([]);
120127
const [attributes, setAttributes] = useState([]);
121128

122129
useEffect(() => {
123-
const fetchFilterData = async () => {
124-
try {
125-
const [attributesData, autocompleteData] = await Promise.all([
126-
apiAttributes(),
127-
apiAutocomplete()
128-
]);
129-
130-
setAttributes(attributesData);
131-
setDepartments(autocompleteData.departments.map(dept => dept.title));
132-
133-
} catch (error) {
134-
console.error("Error fetching filter data:", error);
135-
}
136-
};
137-
138-
fetchFilterData();
130+
apiAttributes()
131+
.then(data => setAttributes(data))
132+
.catch(error => console.error("Error fetching attributes data:", error));
139133
}, []);
140134

135+
useEffect(() => {
136+
if (autocompleteData) {
137+
setDepartments(autocompleteData.departments.map(dept => dept.title));
138+
}
139+
}, [autocompleteData]);
140+
141+
const filterHasChanged = (filterName) => {
142+
const currFilter = filters[filterName];
143+
if (Array.isArray(currFilter)) {
144+
return currFilter.length !== DEFAULT_FILTERS[filterName].length || !currFilter.every(v => DEFAULT_FILTERS[filterName].includes(v));
145+
} else {
146+
return currFilter !== DEFAULT_FILTERS[filterName]
147+
}
148+
}
149+
141150
return (
142151
<>
143152
<Container>
144153
<FilterContainer>
145-
<FilterDropdown title="Department" renderContent={() => (
154+
<FilterDropdown title="Semester Offered" active={filterHasChanged("semester")} renderContent={() => (
155+
<SemesterSelect semesterList={filters.semester} setSemesterList={(semester) => setFilters({ ...filters, semester })} />
156+
)} />
157+
<FilterDropdown title="Department" active={filterHasChanged("departments")} renderContent={() => (
146158
<SelectBox
147159
options={filters.departments}
148160
setOptions={(departments) => setFilters({ ...filters, departments })}
149161
availableItems={departments}
150162
/>
151163
)} />
152-
<FilterDropdown title="Attributes" renderContent={() => (
164+
<FilterDropdown title="Attributes" active={filterHasChanged("attributes")} renderContent={() => (
153165
<SelectBox
154166
options={filters.attributes}
155167
setOptions={(attributes) => setFilters({ ...filters, attributes })}
156168
availableItems={attributes}
157169
/>
158170
)} />
159-
<FilterDropdown title="Time Offered" renderContent={() => (
171+
<FilterDropdown title="Time Offered" active={filterHasChanged("time")} renderContent={() => (
160172
<TimeSelect timeString={filters.time} setTimeString={(time) => setFilters({ ...filters, time })} diameter={200} />
161173
)} />
162-
<FilterDropdown title="Days Offered" renderContent={() => (
174+
<FilterDropdown title="Days Offered" active={filterHasChanged("days")} renderContent={() => (
163175
<DaySelect daysOfferedList={filters.days} setDaysOfferedList={(days) => setFilters({ ...filters, days })} />
164176
)} />
165-
<FilterDropdown title="Semester Offered" renderContent={() => (
166-
<SemesterSelect semesterList={filters.semester} setSemesterList={(semester) => setFilters({ ...filters, semester })} />
167-
)} />
168-
<FilterDropdown title="Course Quality" renderContent={() => (
169-
<SliderSelect ratingValues={filters.course_quality} setRatingValues={(course_quality) => setFilters({ ...filters, course_quality })} />
177+
<FilterDropdown title="Course Quality" active={filterHasChanged("course_quality")} renderContent={() => (
178+
<SliderSelect
179+
ratingValues={filters.course_quality}
180+
setRatingValues={(course_quality) => setFilters({ ...filters, course_quality })}
181+
rangeDescription={{ min: "Poor", max: "Excellent"}}/>
170182
)} />
171-
<FilterDropdown title="Course Difficulty" renderContent={() => (
172-
<SliderSelect ratingValues={filters.difficulty} setRatingValues={(difficulty) => setFilters({ ...filters, difficulty })} />
183+
<FilterDropdown title="Course Difficulty" active={filterHasChanged("difficulty")} renderContent={() => (
184+
<SliderSelect
185+
ratingValues={filters.difficulty}
186+
setRatingValues={(difficulty) => setFilters({ ...filters, difficulty })}
187+
rangeDescription={{ min: "Easy", max: "Hard"}}/>
173188
)} />
174-
<FilterDropdown title="Instructor Quality" renderContent={() => (
175-
<SliderSelect ratingValues={filters.instructor_quality} setRatingValues={(instructor_quality) => setFilters({ ...filters, instructor_quality })} />
189+
<FilterDropdown title="Instructor Quality" active={filterHasChanged("instructor_quality")} renderContent={() => (
190+
<SliderSelect
191+
ratingValues={filters.instructor_quality}
192+
setRatingValues={(instructor_quality) => setFilters({ ...filters, instructor_quality })}
193+
rangeDescription={{ min: "Poor", max: "Excellent"}}/>
176194
)} />
177195
</FilterContainer>
178196
<ResetButton onClick={() => setFilters(DEFAULT_FILTERS)}>Reset Filters</ResetButton>

frontend/review/src/components/Header.js

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import React, { useState } from 'react';
1+
import React, { useState, useEffect } from 'react';
22
import styled from 'styled-components';
33
import { HiBars3, HiXMark } from "react-icons/hi2";
4-
import { motion } from 'motion/react';
4+
import { motion, transformValue } from 'motion/react';
55
import NewSearchBar from './NewSearchBar';
6-
import { useHistory } from 'react-router-dom';
6+
import { Link, useHistory } from 'react-router-dom';
7+
import { apiAutocomplete } from '../utils/api';
78

89
const HeaderContainer = styled.div`
910
display: flex;
@@ -50,7 +51,7 @@ const SearchBarContainer = styled.div`
5051
const LinksContainer = styled(motion.div)`
5152
display: flex;
5253
margin-left: auto;
53-
margin-right: 60px;
54+
margin-right: 30px;
5455
align-items: center;
5556
gap: 40px;
5657
font-family: 'SFPro', sans-serif;
@@ -126,9 +127,23 @@ const menuVariants = {
126127
}
127128
};
128129

129-
const Header = () => {
130+
const Header = ({ autocompleteData, loadDataIndependently = true }) => {
130131
const [isOpen, setIsOpen] = useState(false);
131132

133+
const getCourseCount = () => Object.keys(localStorage).filter(a => !a.startsWith("meta-")).length;
134+
const [courseCount, setCourseCount] = useState(getCourseCount());
135+
136+
useEffect(() => {
137+
const onStorageChange = () => setCourseCount(getCourseCount());
138+
window.addEventListener("storage", onStorageChange);
139+
window.onCartUpdated = onStorageChange;
140+
return () => {
141+
window.removeEventListener("storage", onStorageChange);
142+
window.onCartUpdated = null;
143+
};
144+
}, []);
145+
146+
132147
const history = useHistory();
133148

134149
return (
@@ -137,17 +152,24 @@ const Header = () => {
137152
<div style={{ display: 'flex', alignItems: 'center', gap: '15px', margin: '0 28px' }}>
138153
<img
139154
src="/static/image/logo.png" alt="Penn Course Review" style={{ height: '35px', cursor: 'pointer' }}
140-
onClick={() => history.push('/')}
155+
onClick={() => {
156+
history.push('/');
157+
window.location.reload();
158+
}}
141159
/>
142160
<Title>Penn Course Review</Title>
143161
</div>
144162
<SearchBarContainer>
145-
<NewSearchBar isTitle={true} />
163+
<NewSearchBar isTitle={true} autocompleteData={autocompleteData} loadDataIndependently={loadDataIndependently}/>
146164
</SearchBarContainer>
147165
<LinksContainer>
148166
<StyledLink href='/about'>About</StyledLink>
149167
<StyledLink href='/faq'>FAQs</StyledLink>
150168
<StyledLink href='https://airtable.com/appFRa4NQvNMEbWsA/shrCCsGC2BjUif5Wx' target="_blank">Feedback</StyledLink>
169+
<Link to="/cart" id="cart-icon" title="Course Cart">
170+
<i id="cart" className="fa fa-shopping-cart" />
171+
{courseCount > 0 && <span id="cart-count">{courseCount}</span>}
172+
</Link>
151173
</LinksContainer>
152174
<Hamburger onClick={() => setIsOpen(!isOpen)}>
153175
{isOpen ? <HiXMark /> : <HiBars3 />}
@@ -162,6 +184,7 @@ const Header = () => {
162184
<StyledLink href='/about'>About</StyledLink>
163185
<StyledLink href='/faq'>FAQs</StyledLink>
164186
<StyledLink href='https://airtable.com/appFRa4NQvNMEbWsA/shrCCsGC2BjUif5Wx' target="_blank">Feedback</StyledLink>
187+
<StyledLink href="/cart">Course Cart</StyledLink>
165188
</MobileLinksInner>
166189
</MobileMenuWrapper>
167190
</>

frontend/review/src/components/InfoBox/index.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,6 @@ const InfoBox = ({
148148

149149
{data.registration_metrics && (
150150
<StatsToggleContainer>
151-
<NewLabel>NEW</NewLabel>
152151
<div className="btn-group">
153152
<button
154153
onClick={() => setIsCourseEval(false)}

0 commit comments

Comments
 (0)