Skip to content

Commit de9eb64

Browse files
authored
Merge pull request #524 from cornell-dti/matt/course-evals
course evals
2 parents 046f4af + e06d082 commit de9eb64

File tree

11 files changed

+812
-129
lines changed

11 files changed

+812
-129
lines changed

client/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,12 @@
2424
"dependencies": {
2525
"@vitejs/plugin-react": "^4.3.1",
2626
"axios": "^1.6.7",
27+
"chart.js": "^4.4.8",
28+
"chartjs-plugin-datalabels": "^2.2.0",
2729
"common": "0.0.1",
2830
"i": "^0.3.7",
2931
"react": "^18.3.1",
32+
"react-chartjs-2": "^5.3.0",
3033
"react-circular-progressbar": "^2.0.3",
3134
"react-dom": "^18.3.1",
3235
"react-google-login": "^5.2.2",

client/src/modules/Admin/Components/DevTools.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,18 @@ export default function AdminTools({ token }: AdminToolsProps) {
4747
}
4848
};
4949

50+
const handleCourseEvalApiCall = async (endpoint: string, successState: keyof typeof messages, resetEvals: boolean) => {
51+
setUpdating(true)
52+
try {
53+
await axios.post(endpoint, { token, resetEvals });
54+
setUpdated(successState);
55+
} catch {
56+
setUpdated('failure');
57+
} finally {
58+
setUpdating(false);
59+
}
60+
};
61+
5062
const raffleHandler = async () => {
5163
if (!raffleStartDate) return;
5264
setUpdating(true);
@@ -68,6 +80,31 @@ export default function AdminTools({ token }: AdminToolsProps) {
6880
<div className={styles.adminWrapper}>
6981
<h1>Developer Tools</h1>
7082
<div className={styles.buttonGroup}>
83+
<button
84+
onClick={() => handleApiCall('/api/admin/semester/add', 'semester')}
85+
disabled={updating}
86+
className={styles.adminButtons}
87+
>
88+
Add New Semester
89+
</button>
90+
<button
91+
onClick={() =>
92+
handleCourseEvalApiCall('/api/admin/courses/add-course-evals', 'courseEval', false)
93+
}
94+
disabled={updating}
95+
className={styles.adminButtons}
96+
>
97+
Add Course Evaluations
98+
</button>
99+
<button
100+
onClick={() =>
101+
handleCourseEvalApiCall('/api/admin/courses/add-course-evals', 'courseEval', true)
102+
}
103+
disabled={updating}
104+
className={styles.adminButtons}
105+
>
106+
Delete and regenerate all Course Evaluations
107+
</button>
71108
For semesterly updates:
72109
<div className={styles.semester}>
73110
<button

client/src/modules/Course/Components/Course.tsx

Lines changed: 86 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { Session } from '../../../session-store';
2727
import { useAuthOptionalLogin } from '../../../auth/auth_utils';
2828

2929
import ReviewModal from './ReviewModal';
30+
import CourseEval from './CourseEval';
3031

3132
enum PageStatus {
3233
Loading,
@@ -43,6 +44,7 @@ export const Course = () => {
4344
const [similarCourses, setSimilarCourses] = useState<Recommendation[]>();
4445
const [pageStatus, setPageStatus] = useState<PageStatus>(PageStatus.Loading);
4546
const [scrolled, setScrolled] = useState(false);
47+
const [reviewTabSelected, setReviewTabSelected] = useState(true);
4648
const [screenWidth, setScreenWidth] = useState(window.innerWidth);
4749
const [visibleCourseReviews, setVisibleCourseReviews] = useState<Review[]>(
4850
[]
@@ -321,73 +323,98 @@ export const Course = () => {
321323
/>
322324
</div>
323325
<div className={styles.rightPanel}>
324-
<div className={styles.reviewscontainer}>
325-
<div className={styles.bar}>
326-
<h2 className={styles.title}>
327-
Past Reviews ({visibleCourseReviews.length}){' '}
328-
</h2>
329-
<div>
330-
<div className={styles['select-container']}>
331-
<div className={styles['filter-container']}>
332-
<label
333-
htmlFor="sort-reviews"
334-
style={{ whiteSpace: 'nowrap' }}
335-
>
336-
Sort by:{' '}
337-
</label>
338-
<select
339-
name="sort-reviews"
340-
id="sort-reviews"
341-
onChange={sortReviewsBy}
342-
className={styles.filtertext}
343-
>
344-
<option value="helpful">Most Helpful</option>
345-
<option value="recent">Recent</option>
346-
{selectedProf.current === 'none' && (
347-
<option value="professor">Professor Name</option>
348-
)}
349-
</select>
350-
</div>
351-
<div className={styles.filterContainer}>
352-
<label
353-
htmlFor="filter-by-prof"
354-
style={{ whiteSpace: 'nowrap' }}
355-
>
356-
Filter by professor:{' '}
357-
</label>
358-
<select
359-
name="filter-by-prof"
360-
id="filter-by-prof"
361-
onChange={filterByProf}
362-
className={styles.filtertext}
363-
>
364-
<option value="none">None</option>
365-
{[...pastProfs.current]
366-
.sort()
367-
?.filter((o) => o !== 'Not Listed')
368-
.map((o) => <option value={o}>{o}</option>)}
369-
</select>
326+
{/* Custom Tab Component Structure */}
327+
<div className={styles.tabs}>
328+
<button
329+
className={reviewTabSelected ? styles.tabactivetitle : styles.tabtitle}
330+
onClick={() => setReviewTabSelected(true)}
331+
>
332+
Past Reviews ({visibleCourseReviews.length})
333+
</button>
334+
{courseEval !== null && (
335+
<button
336+
className={reviewTabSelected ? styles.tabtitle : styles.tabactivetitle}
337+
onClick={() => setReviewTabSelected(false)}
338+
>
339+
Course Evaluation Data
340+
</button>
341+
)}
342+
343+
{/* Add a gray background line spanning the full width */}
344+
<div className={styles.tabIndicator}></div>
345+
346+
{/* Add a blue active indicator that moves */}
347+
<div className={`${courseEval != null ? styles.activeIndicator : styles.noEvalsIndicator} ${reviewTabSelected ? styles.firstTab : styles.secondTab}`}></div>
348+
</div>
349+
{reviewTabSelected && (
350+
<div className={styles.reviewscontainer}>
351+
<div className={styles.bar}>
352+
<div>
353+
<div className={styles['select-container']}>
354+
<div className={styles['filter-container']}>
355+
<label
356+
htmlFor="sort-reviews"
357+
style={{ whiteSpace: 'nowrap' }}
358+
>
359+
Sort by:{' '}
360+
</label>
361+
<select
362+
name="sort-reviews"
363+
id="sort-reviews"
364+
onChange={sortReviewsBy}
365+
className={styles.filtertext}
366+
>
367+
<option value="helpful">Most Helpful</option>
368+
<option value="recent">Recent</option>
369+
{selectedProf.current === 'none' && (
370+
<option value="professor">Professor Name</option>
371+
)}
372+
</select>
373+
</div>
374+
<div className={styles.filterContainer}>
375+
<label
376+
htmlFor="filter-by-prof"
377+
style={{ whiteSpace: 'nowrap' }}
378+
>
379+
Filter by professor:{' '}
380+
</label>
381+
<select
382+
name="filter-by-prof"
383+
id="filter-by-prof"
384+
onChange={filterByProf}
385+
className={styles.filtertext}
386+
>
387+
<option value="none">None</option>
388+
{[...pastProfs.current]
389+
.sort()
390+
?.filter((o) => o !== 'Not Listed')
391+
.map((o) => <option value={o}>{o}</option>)}
392+
</select>
393+
</div>
370394
</div>
371395
</div>
372396
</div>
373-
</div>
374-
<div className={styles.reviews}>
375-
<CourseReviews
376-
reviews={visibleCourseReviews}
377-
isPreview={false}
378-
isProfile={false}
379-
token={token}
380-
/>
381-
</div>
382-
</div>
397+
<div className={styles.reviews}>
398+
<CourseReviews
399+
reviews={visibleCourseReviews}
400+
isPreview={false}
401+
isProfile={false}
402+
token={token}
403+
/>
404+
</div>
405+
</div>)}
406+
{!reviewTabSelected && courseEval != null && (
407+
<CourseEval courseEval={courseEval} />
408+
)}
383409
<SimilarCoursesSection
384410
similarCourses={similarCourses}
385411
isVisible={screenWidth <= 768}
386412
/>
387413
</div>
388414
</div>
389415

390-
{/* Fixed Bottom-Right Review Button */}
416+
{/* Fixed Bottom-Right Review Button */
417+
}
391418
<button
392419
className={`${!scrolled && styles.hide} ${styles.fixedreviewbutton} `}
393420
onClick={() => setOpen(true)}
@@ -404,7 +431,8 @@ export const Course = () => {
404431
}
405432
/>
406433
</div>
407-
);
434+
)
435+
;
408436
}
409437

410438
return <Loading />;

0 commit comments

Comments
 (0)