Conversation
|
Visit the preview URL for this PR (updated for commit e320c92): https://magnify--pr158-code-cleanup-tz5nhp0a.web.app (expires Mon, 02 Mar 2026 00:53:11 GMT) 🔥 via Firebase Hosting GitHub Action 🌎 Sign: 9d7af0ca9f95ffb1f7aaef8a25560cf9e42173b6 |
There was a problem hiding this comment.
Pull request overview
This PR refactors the codebase to eliminate imperative DOM manipulation and embrace React best practices. The changes centralize application logic in App.tsx with proper state management, extract Firebase configuration to a dedicated module, and use the modern createRoot API for rendering.
Changes:
- Migrated from imperative DOM manipulation to React state management for search, tag filtering, and user authentication
- Consolidated Firebase initialization into a single config module and removed redundant initialization code
- Refactored TagBar component to use map() instead of 16 hardcoded tag buttons
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| src/index.tsx | Simplified to use React 18's createRoot API and removed all business logic |
| src/App.tsx | Now manages all application state (courseData, user, authLevel, search, tags) with React hooks instead of global variables and DOM manipulation |
| src/components/TopBar.tsx | Converted to controlled component with searchQuery state and event handlers passed as props |
| src/components/TagBar.tsx | Refactored to generate tag buttons via map() and receive state/handlers as props |
| src/components/ClearFilter.tsx | Updated to use props for callbacks and useEffect for side effects |
| src/components/Course.tsx | Simplified to use centralized Firebase db export instead of initializing Firebase locally |
| src/config/firebase.ts | Centralized Firebase initialization and exports auth, db, and provider instances |
| src/config/index.ts | Added exports for auth, db, and provider |
| src/utils/getCookieValue.ts | New utility extracted for cookie parsing |
| src/utils/index.ts | Added export for getCookieValue utility |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const courseIDtoNameMap = useMemo(() => { | ||
| const map = new Map<string, string>(); | ||
| for (const courseName in courseData) { | ||
| courseData![courseName].courseid.match(COURSE_ID_REGEX)?.forEach((id) => { | ||
| map.set(id, courseName); | ||
| }); | ||
| } | ||
| return map; | ||
| }, [courseData]); |
There was a problem hiding this comment.
The courseIDtoNameMap computation will crash when courseData is null. The useMemo checks if courseData exists in the loop condition but uses the non-null assertion operator, which can cause a runtime error if courseData is null during the initial render before the useEffect sets it.
Add a guard check at the beginning of the useMemo: if (!courseData) return new Map();
There was a problem hiding this comment.
@Ascent817 is coursedata gonna ever be null, don't we load in the pre-downloaded coursedata json regardless
There was a problem hiding this comment.
Yeah, we should always have local to fall back on. We should make sure to update the local version when we update the db.
| snapshot.val().users; | ||
| Object.values(users).forEach(({ email, level }) => { | ||
| if (user.email === email) setAuthLevel(level); | ||
| }); |
There was a problem hiding this comment.
The auth level fetch effect lacks error handling. If the database read fails, the error will be unhandled. Add a .catch() handler to the promise chain to handle potential database errors gracefully.
| }); | |
| }); | |
| }) | |
| .catch((error) => { | |
| console.error('Failed to fetch auth level:', error); |
| useEffect(() => { | ||
| const courseContainer = document.getElementById( | ||
| 'course-container' | ||
| ) as HTMLDivElement; | ||
|
|
||
| const clearResults = () => { | ||
| const search = document.getElementById('searchbar') as HTMLInputElement; | ||
| search.value = ''; | ||
|
|
||
| const tags = document.getElementsByClassName('tag'); | ||
| for (let i = 0; i < tags.length; i++) { | ||
| if (tags[i].classList.contains('tag-true')) { | ||
| tags[i].classList.remove('tag-true'); | ||
| } | ||
| } | ||
| const adjustHeight = () => { | ||
| courseContainer.style.height = | ||
| getWidth() >= 525 | ||
| ? 'calc(100vh - 180px)' | ||
| : 'calc(100vh - (45.5px + 65.5px + (clamp(15px, 5vw, 20px) * 2)))'; | ||
| }; | ||
|
|
||
| filterCourses(); | ||
| }; | ||
| adjustHeight(); | ||
| window.addEventListener('resize', adjustHeight); | ||
| return () => { | ||
| window.removeEventListener('resize', adjustHeight); | ||
| courseContainer.style.height = ''; | ||
| }; |
There was a problem hiding this comment.
The ClearFilter component directly manipulates the DOM to set the height of the course-container element. This goes against the PR's stated goal of eliminating imperative DOM manipulation in favor of React best practices. Consider managing the container height through React state and inline styles on the container div in App.tsx, similar to how display and justifyContent are already handled on lines 262-266.
There was a problem hiding this comment.
well this could be fixed if we used CSS subgrid
| useEffect(() => { | ||
| let prevScrollPos = window.scrollY; | ||
| const handleScroll = () => { | ||
| const topButton = document.getElementById('to-top'); | ||
| if (topButton) { | ||
| const visible = window.scrollY >= 80 && windowWidth >= 525; | ||
| topButton.style.visibility = visible ? 'visible' : 'hidden'; | ||
| topButton.style.opacity = visible ? '1' : '0'; | ||
| } | ||
| if (windowWidth < 525) { | ||
| const nav = document.getElementById('mobile-nav'); | ||
| if (nav) { | ||
| nav.style.bottom = | ||
| window.scrollY < prevScrollPos ? '0' : `-${nav.offsetHeight}px`; | ||
| } | ||
| } | ||
| prevScrollPos = window.scrollY; | ||
| }; | ||
| window.addEventListener('scroll', handleScroll); | ||
| return () => window.removeEventListener('scroll', handleScroll); | ||
| }, [windowWidth]); |
There was a problem hiding this comment.
The scroll handler in App.tsx continues to use imperative DOM manipulation to control the visibility of the back-to-top button and mobile navigation. This is inconsistent with the PR's stated goal of using React best practices instead of imperative DOM manipulation. Consider refactoring to use React state to track scroll position and control visibility through conditional rendering or className props.
| ); | ||
| )); | ||
| return <div className="parent">{flexParents}</div>; | ||
| }, [filteredCourseItems, numColumns, handleClearFilters]); |
There was a problem hiding this comment.
The classItems useMemo includes handleClearFilters in its dependency array, but handleClearFilters is stable (has no dependencies). This can cause unnecessary re-computation of the classItems layout when it doesn't need to change. However, since handleClearFilters is wrapped in useCallback with an empty dependency array, this is actually fine. Consider documenting this relationship or removing it from dependencies if handleClearFilters is guaranteed to be stable.
| }, [filteredCourseItems, numColumns, handleClearFilters]); | |
| }, [filteredCourseItems, numColumns]); |
| const signedInUser = result.user; | ||
| if (!signedInUser) return; | ||
| setUser(signedInUser); | ||
| document.cookie = `user=${JSON.stringify(signedInUser)}`; |
There was a problem hiding this comment.
Storing the entire Firebase user object in a cookie is a security concern. The user object may contain sensitive information and tokens that should not be stored in cookies, which are vulnerable to XSS attacks and can be transmitted with every HTTP request. Consider using Firebase's built-in session persistence or storing only minimal, non-sensitive user identifiers in cookies (like UID), and relying on Firebase Auth's persistence mechanisms instead.
| document.cookie = `user=${JSON.stringify(signedInUser)}`; | |
| document.cookie = `user=${encodeURIComponent(signedInUser.uid)}`; |
| const [user, setUser] = useState<firebase.User | null>(() => { | ||
| const cookie = getCookieValue('user'); | ||
| if (!cookie) return null; | ||
| try { | ||
| return JSON.parse(cookie); | ||
| } catch { | ||
| return null; | ||
| } | ||
| }); |
There was a problem hiding this comment.
The user state initialization from cookies lacks validation. If the cookie contains a malformed or malicious JSON payload that successfully parses but doesn't match the expected Firebase User structure, it could cause runtime errors when the app tries to access user properties like user.email or user.photoURL. Consider adding type validation or using a schema validator to ensure the parsed object conforms to the expected Firebase User interface.
| auth.signOut().then(() => { | ||
| setUser(null); | ||
| setAuthLevel(0); | ||
| document.cookie = | ||
| 'user=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; | ||
| }); | ||
| } else { | ||
| auth.signInWithPopup(provider).then((result) => { | ||
| const signedInUser = result.user; | ||
| if (!signedInUser) return; | ||
| setUser(signedInUser); | ||
| document.cookie = `user=${JSON.stringify(signedInUser)}`; | ||
| }); |
There was a problem hiding this comment.
The authentication sign-in and sign-out promises lack error handling. If the Firebase authentication fails (network issues, popup blocked, etc.), the promise rejection will be unhandled, potentially causing user-facing errors or leaving the UI in an inconsistent state. Add .catch() handlers to both auth.signOut() and auth.signInWithPopup(provider) calls to handle errors gracefully.
| auth.signOut().then(() => { | |
| setUser(null); | |
| setAuthLevel(0); | |
| document.cookie = | |
| 'user=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; | |
| }); | |
| } else { | |
| auth.signInWithPopup(provider).then((result) => { | |
| const signedInUser = result.user; | |
| if (!signedInUser) return; | |
| setUser(signedInUser); | |
| document.cookie = `user=${JSON.stringify(signedInUser)}`; | |
| }); | |
| auth | |
| .signOut() | |
| .then(() => { | |
| setUser(null); | |
| setAuthLevel(0); | |
| document.cookie = | |
| 'user=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; | |
| }) | |
| .catch((error) => { | |
| console.error('Error signing out:', error); | |
| }); | |
| } else { | |
| auth | |
| .signInWithPopup(provider) | |
| .then((result) => { | |
| const signedInUser = result.user; | |
| if (!signedInUser) return; | |
| setUser(signedInUser); | |
| document.cookie = `user=${JSON.stringify(signedInUser)}`; | |
| }) | |
| .catch((error) => { | |
| console.error('Error signing in:', error); | |
| }); |
| @@ -34,26 +211,62 @@ | |||
| contactModal?.showModal(); | |||
| }, []); | |||
There was a problem hiding this comment.
The handleContactModalOpen function uses imperative DOM manipulation to open the contact modal. This is inconsistent with the PR's goal of eliminating imperative DOM manipulation. Consider managing modal state through React state and passing it as a prop to the ContactForm component.
There was a problem hiding this comment.
dang we still have a lot of technical debt...
|
@Ascent817 I used the preview URL, and I cannot login. The popup gets blocked even though I allowed popups. Possible bug? Is it working on localhost? |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
| useEffect(() => { | ||
| const courseContainer = document.getElementById( | ||
| 'course-container' | ||
| ) as HTMLDivElement; | ||
|
|
||
| const clearResults = () => { | ||
| const search = document.getElementById('searchbar') as HTMLInputElement; | ||
| search.value = ''; | ||
|
|
||
| const tags = document.getElementsByClassName('tag'); | ||
| for (let i = 0; i < tags.length; i++) { | ||
| if (tags[i].classList.contains('tag-true')) { | ||
| tags[i].classList.remove('tag-true'); | ||
| } | ||
| } | ||
| const adjustHeight = () => { | ||
| courseContainer.style.height = | ||
| getWidth() >= 525 | ||
| ? 'calc(100vh - 180px)' | ||
| : 'calc(100vh - (45.5px + 65.5px + (clamp(15px, 5vw, 20px) * 2)))'; | ||
| }; | ||
|
|
||
| filterCourses(); | ||
| }; | ||
| adjustHeight(); | ||
| window.addEventListener('resize', adjustHeight); | ||
| return () => { | ||
| window.removeEventListener('resize', adjustHeight); | ||
| courseContainer.style.height = ''; | ||
| }; |
| @@ -34,26 +211,62 @@ const App: FC<AppProps> = ({ user, classItems, authLevel }): JSX.Element => { | |||
| contactModal?.showModal(); | |||
| }, []); | |||
There was a problem hiding this comment.
dang we still have a lot of technical debt...
| const courseIDtoNameMap = useMemo(() => { | ||
| const map = new Map<string, string>(); | ||
| for (const courseName in courseData) { | ||
| courseData![courseName].courseid.match(COURSE_ID_REGEX)?.forEach((id) => { | ||
| map.set(id, courseName); | ||
| }); | ||
| } | ||
| return map; | ||
| }, [courseData]); |
There was a problem hiding this comment.
@Ascent817 is coursedata gonna ever be null, don't we load in the pre-downloaded coursedata json regardless
This reverts commit fb85a5b.
It was working on localhost for me, maybe the preview URL isn't an authorized domain to send OAuth requests from? |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This reverts commit fb85a5b.
ed5a4e3 to
1c14ed7
Compare
That's probably what's happening |
…into code-cleanup
This reverts commit f94cfa3.
Description
Cleaned up code that modifies the DOM imperatively rather than using React best practices. Also reduced code duplication and used map to generate tag filters rather than the 12 hardcoded ones we had before.