Skip to content

Commit ed781b4

Browse files
Ascent817GoogolGeniusCopilot
authored
Code cleanup (#158)
* Add Computer Science Tag * Remove repetition in TagBar.tsx * Clean up codebase to better follow React best practices * Run prettier * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Revert "Apply suggestions from code review" This reverts commit fb85a5b. * Extract tags to single structure * Remove repetition in TagBar.tsx * Clean up codebase to better follow React best practices * Run prettier * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Revert "Apply suggestions from code review" This reverts commit fb85a5b. * Extract tags to single structure * remove handleClearFilters dependency * Revert "remove handleClearFilters dependency" This reverts commit f94cfa3. --------- Co-authored-by: Erich <81032623+GoogolGenius@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 88899dd commit ed781b4

12 files changed

Lines changed: 328 additions & 590 deletions

File tree

src/App.tsx

Lines changed: 218 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,192 @@
11
import './App.css';
22
import {
3+
ClearFilter,
34
ContactForm,
5+
Course,
46
FloatingActionable,
7+
Loader,
58
MobileNavigation,
69
Navigation,
710
TagBar,
811
TopBar,
912
} from './components';
10-
// import { ParsePDF } from './components/ParsePDF';
13+
import { auth, db, provider } from './config';
14+
import { TAGS } from './data';
15+
import localCourseData from './data/coursedata.json';
16+
import { CourseDataType, CourseType } from './types';
17+
import { getCookieValue, getWidth } from './utils';
1118
import firebase from 'firebase/compat/app';
12-
import { FC, useCallback, useEffect } from 'react';
19+
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
1320

14-
interface AppProps {
15-
user: firebase.User | null;
16-
classItems: JSX.Element;
17-
authLevel: number;
18-
}
21+
const COURSE_ID_REGEX = /[A-Z]{3}[0-9]{3}/g;
22+
const COURSE_ID_REGEX_SINGLE = /[A-Z]{3}[0-9]{3}/;
1923

20-
const App: FC<AppProps> = ({ user, classItems, authLevel }): JSX.Element => {
21-
// Hacky workaround because something with React probably interferes with the default browser behavior
22-
// Also, reactivates the highlight on the course element
24+
const getNumColumns = (width: number): number => {
25+
if (width > 1400) return 4;
26+
if (width > 1100) return 3;
27+
if (width > 800) return 2;
28+
return 1;
29+
};
30+
31+
const App: FC = (): JSX.Element => {
32+
const [courseData, setCourseData] = useState<CourseDataType | null>(null);
33+
const [user, setUser] = useState<firebase.User | null>(() => {
34+
const cookie = getCookieValue('user');
35+
if (!cookie) return null;
36+
try {
37+
return JSON.parse(cookie);
38+
} catch {
39+
return null;
40+
}
41+
});
42+
const [authLevel, setAuthLevel] = useState(0);
43+
const [searchQuery, setSearchQuery] = useState('');
44+
const [activeTags, setActiveTags] = useState<string[]>([]);
45+
const [windowWidth, setWindowWidth] = useState(getWidth());
46+
47+
// Fetch course data from Firebase, fall back to local data
48+
useEffect(() => {
49+
db.ref()
50+
.get()
51+
.then((snapshot) => {
52+
setCourseData(
53+
snapshot.exists() ? snapshot.val().courses : localCourseData
54+
);
55+
})
56+
.catch(() => setCourseData(localCourseData));
57+
}, []);
58+
59+
// Fetch auth level for the signed-in user
60+
useEffect(() => {
61+
if (!user) return;
62+
db.ref()
63+
.get()
64+
.then((snapshot) => {
65+
if (!snapshot.exists()) return;
66+
const users: { [key: string]: { email: string; level: number } } =
67+
snapshot.val().users;
68+
Object.values(users).forEach(({ email, level }) => {
69+
if (user.email === email) setAuthLevel(level);
70+
});
71+
});
72+
}, [user]);
73+
74+
// Window resize listener for responsive column layout
75+
useEffect(() => {
76+
const handleResize = () => setWindowWidth(getWidth());
77+
window.addEventListener('resize', handleResize);
78+
return () => window.removeEventListener('resize', handleResize);
79+
}, []);
80+
81+
// Scroll listener: show/hide desktop back-to-top button and mobile nav
82+
useEffect(() => {
83+
let prevScrollPos = window.scrollY;
84+
const handleScroll = () => {
85+
const topButton = document.getElementById('to-top');
86+
if (topButton) {
87+
const visible = window.scrollY >= 80 && windowWidth >= 525;
88+
topButton.style.visibility = visible ? 'visible' : 'hidden';
89+
topButton.style.opacity = visible ? '1' : '0';
90+
}
91+
if (windowWidth < 525) {
92+
const nav = document.getElementById('mobile-nav');
93+
if (nav) {
94+
nav.style.bottom =
95+
window.scrollY < prevScrollPos ? '0' : `-${nav.offsetHeight}px`;
96+
}
97+
}
98+
prevScrollPos = window.scrollY;
99+
};
100+
window.addEventListener('scroll', handleScroll);
101+
return () => window.removeEventListener('scroll', handleScroll);
102+
}, [windowWidth]);
103+
104+
// Re-trigger hash jump after initial course data loads
105+
// (React rendering can interfere with native hash navigation)
23106
useEffect(() => {
24107
const jumpId = window.location.hash;
25108
window.location.hash = '';
26109
window.location.hash = jumpId;
27110
}, []);
28111

112+
const courseIDtoNameMap = useMemo(() => {
113+
const map = new Map<string, string>();
114+
for (const courseName in courseData) {
115+
courseData![courseName].courseid.match(COURSE_ID_REGEX)?.forEach((id) => {
116+
map.set(id, courseName);
117+
});
118+
}
119+
return map;
120+
}, [courseData]);
121+
122+
const courseIDtoCourse = useCallback(
123+
(courseID: string): CourseType => {
124+
const id = courseID.match(COURSE_ID_REGEX_SINGLE)?.[0] ?? '';
125+
return (
126+
courseData?.[courseIDtoNameMap.get(id) ?? ''] ??
127+
('' as unknown as CourseType)
128+
);
129+
},
130+
[courseData, courseIDtoNameMap]
131+
);
132+
133+
const filteredCourseItems = useMemo(() => {
134+
if (!courseData) return [];
135+
const key = encodeURIComponent(
136+
searchQuery.toLowerCase().replaceAll(' ', '-')
137+
).replace(/\./g, '%2E');
138+
return Object.keys(courseData)
139+
.filter((name) => {
140+
const matchesSearch = name.search(key) !== -1;
141+
const matchesTags =
142+
activeTags.length === 0 ||
143+
activeTags.some((tag) =>
144+
courseData[name].tags?.includes(
145+
TAGS[TAGS.findIndex((t) => t.id === tag)].label
146+
)
147+
);
148+
return matchesSearch && matchesTags;
149+
})
150+
.map((name) => (
151+
<Course
152+
key={name}
153+
authLevel={authLevel}
154+
course={courseData[name]}
155+
jumpId={courseData[name].courseid}
156+
courseIDtoCourse={courseIDtoCourse}
157+
/>
158+
));
159+
}, [courseData, searchQuery, activeTags, authLevel, courseIDtoCourse]);
160+
161+
const handleTagToggle = useCallback((id: string) => {
162+
setActiveTags((prev) =>
163+
prev.includes(id) ? prev.filter((t) => t !== id) : [...prev, id]
164+
);
165+
}, []);
166+
167+
const handleClearFilters = useCallback(() => {
168+
setSearchQuery('');
169+
setActiveTags([]);
170+
}, []);
171+
172+
const handleSignIn = useCallback(() => {
173+
if (user) {
174+
auth.signOut().then(() => {
175+
setUser(null);
176+
setAuthLevel(0);
177+
document.cookie =
178+
'user=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
179+
});
180+
} else {
181+
auth.signInWithPopup(provider).then((result) => {
182+
const signedInUser = result.user;
183+
if (!signedInUser) return;
184+
setUser(signedInUser);
185+
document.cookie = `user=${JSON.stringify(signedInUser)}`;
186+
});
187+
}
188+
}, [user]);
189+
29190
const handleContactModalOpen = useCallback(() => {
30191
const contactModal = document.getElementById(
31192
'contact-modal'
@@ -34,26 +195,62 @@ const App: FC<AppProps> = ({ user, classItems, authLevel }): JSX.Element => {
34195
contactModal?.showModal();
35196
}, []);
36197

37-
const userElement: JSX.Element =
38-
user && user.photoURL ? (
39-
<div className="user">
40-
<img id="user-img" src={user.photoURL} alt="User profile" />
198+
const numColumns = getNumColumns(windowWidth);
199+
200+
const classItems = useMemo(() => {
201+
const items =
202+
filteredCourseItems.length > 0
203+
? filteredCourseItems
204+
: [<ClearFilter key="clear" onClearFilters={handleClearFilters} />];
205+
const flexParents = Array.from({ length: numColumns }, (_, i) => (
206+
<div key={i} className="flex-parent">
207+
{items.filter((_, j) => j % numColumns === i)}
41208
</div>
42-
) : (
43-
<div className="hide"></div>
44-
);
209+
));
210+
return <div className="parent">{flexParents}</div>;
211+
}, [filteredCourseItems, numColumns, handleClearFilters]);
212+
213+
const userElement: JSX.Element = user?.photoURL ? (
214+
<div className="user">
215+
<img id="user-img" src={user.photoURL} alt="User profile" />
216+
</div>
217+
) : (
218+
<div className="hide" />
219+
);
220+
221+
if (!courseData) {
222+
return <Loader />;
223+
}
45224

46225
return (
47226
<div className="App">
48227
<ContactForm />
49-
{/* <ParsePDF /> */}
50228
<div className="container">
51229
<Navigation authLevel={authLevel} />
52230
</div>
53231
<div className="main">
54-
<TopBar user={user} userElement={userElement} />
55-
<TagBar />
56-
<div id="course-container">{classItems}</div>
232+
<TopBar
233+
user={user}
234+
userElement={userElement}
235+
searchQuery={searchQuery}
236+
onSearch={setSearchQuery}
237+
onSignIn={handleSignIn}
238+
/>
239+
<TagBar
240+
activeTags={activeTags}
241+
onTagToggle={handleTagToggle}
242+
onClearTags={handleClearFilters}
243+
/>
244+
<div
245+
id="course-container"
246+
style={
247+
filteredCourseItems.length === 0
248+
? { display: 'flex', justifyContent: 'center' }
249+
: undefined
250+
}
251+
>
252+
{classItems}
253+
</div>
57254
</div>
58255
<FloatingActionable handleContactModalOpen={handleContactModalOpen} />
59256
<MobileNavigation handleContactModalOpen={handleContactModalOpen} />

src/components/ClearFilter.tsx

Lines changed: 25 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,38 @@
11
import '../App.css';
2-
import { filterCourses } from '../index';
32
import { getWidth } from '../utils';
4-
import { FC } from 'react';
3+
import { FC, useEffect } from 'react';
54

6-
export const ClearFilter: FC = (): JSX.Element => {
7-
const courseContainer = document.getElementById(
8-
'course-container'
9-
) as HTMLDivElement;
10-
courseContainer.style.display = 'flex';
11-
courseContainer.style.justifyContent = 'center';
5+
interface ClearFilterProps {
6+
onClearFilters: () => void;
7+
}
128

13-
const adjustCourseContainerHeight = () => {
14-
if (getWidth() >= 525) {
15-
courseContainer.style.height = 'calc(100vh - 180px)';
16-
} else {
17-
courseContainer.style.height =
18-
'calc(100vh - (45.5px + 65.5px + (clamp(15px, 5vw, 20px) * 2)))';
19-
}
20-
};
21-
22-
adjustCourseContainerHeight();
23-
window.addEventListener('resize', adjustCourseContainerHeight);
9+
export const ClearFilter: FC<ClearFilterProps> = ({
10+
onClearFilters,
11+
}): JSX.Element => {
12+
useEffect(() => {
13+
const courseContainer = document.getElementById(
14+
'course-container'
15+
) as HTMLDivElement;
2416

25-
const clearResults = () => {
26-
const search = document.getElementById('searchbar') as HTMLInputElement;
27-
search.value = '';
28-
29-
const tags = document.getElementsByClassName('tag');
30-
for (let i = 0; i < tags.length; i++) {
31-
if (tags[i].classList.contains('tag-true')) {
32-
tags[i].classList.remove('tag-true');
33-
}
34-
}
17+
const adjustHeight = () => {
18+
courseContainer.style.height =
19+
getWidth() >= 525
20+
? 'calc(100vh - 180px)'
21+
: 'calc(100vh - (45.5px + 65.5px + (clamp(15px, 5vw, 20px) * 2)))';
22+
};
3523

36-
filterCourses();
37-
};
24+
adjustHeight();
25+
window.addEventListener('resize', adjustHeight);
26+
return () => {
27+
window.removeEventListener('resize', adjustHeight);
28+
courseContainer.style.height = '';
29+
};
30+
}, []);
3831

3932
return (
4033
<div className="ClearFilter" id="clear-filter-class">
4134
<h1>No Results</h1>
42-
<button className="clear-results" onClick={clearResults}>
35+
<button className="clear-results" onClick={onClearFilters}>
4336
Clear Filters
4437
</button>
4538
</div>

src/components/Course.tsx

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
11
import '../App.css';
2-
import { firebaseConfig } from '../config';
2+
import { db } from '../config';
33
import { CourseType } from '../types';
4-
import firebase from 'firebase/compat/app';
5-
import 'firebase/compat/database';
64
import { FC, useCallback, useRef, useState, useMemo } from 'react';
75

8-
firebase.initializeApp(firebaseConfig);
9-
firebase.database().ref();
10-
116
interface CourseProps {
127
course: CourseType;
138
authLevel: number;
@@ -114,12 +109,9 @@ export const Course: FC<CourseProps> = ({
114109
})
115110
);
116111

117-
firebase
118-
.database()
119-
.ref('courses')
120-
.update({
121-
[loopName]: { ...course, ...overwriteCourse },
122-
});
112+
db.ref('courses').update({
113+
[loopName]: { ...course, ...overwriteCourse },
114+
});
123115

124116
// Exit the edit menu
125117
setIsEditing(false);

0 commit comments

Comments
 (0)