From 759c773134becdbee0ca8ae186198c86a9e4c2aa Mon Sep 17 00:00:00 2001 From: Ascent817 Date: Wed, 22 Jan 2025 15:55:44 -0600 Subject: [PATCH 01/15] Add Computer Science Tag --- src/components/TagBar.tsx | 10 ++++++++++ src/index.tsx | 1 + 2 files changed, 11 insertions(+) diff --git a/src/components/TagBar.tsx b/src/components/TagBar.tsx index 3b990b9..ec5016e 100644 --- a/src/components/TagBar.tsx +++ b/src/components/TagBar.tsx @@ -38,6 +38,16 @@ export const TagBar: FC = (): JSX.Element => { AP + - - - - - - - - - - - - - - - - + + {TAGS.map(({ id, label }) => ( + + ))} ); }; From eb30f121c488869ce962d5cb67afccf94460d14d Mon Sep 17 00:00:00 2001 From: Ascent817 Date: Thu, 19 Feb 2026 16:27:43 -0800 Subject: [PATCH 03/15] Clean up codebase to better follow React best practices --- src/App.tsx | 238 ++++++++++++++++++++-- src/components/ClearFilter.tsx | 55 +++-- src/components/Course.tsx | 21 +- src/components/TagBar.tsx | 27 +-- src/components/TopBar.tsx | 15 +- src/config/firebase.ts | 9 + src/config/index.ts | 2 +- src/index.tsx | 353 +-------------------------------- src/utils/getCookieValue.ts | 2 + src/utils/index.ts | 1 + 10 files changed, 296 insertions(+), 427 deletions(-) create mode 100644 src/utils/getCookieValue.ts diff --git a/src/App.tsx b/src/App.tsx index fe089f3..11f6a86 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,31 +1,200 @@ import './App.css'; import { + ClearFilter, ContactForm, + Course, FloatingActionable, + Loader, MobileNavigation, Navigation, TagBar, TopBar, } from './components'; -// import { ParsePDF } from './components/ParsePDF'; +import { auth, db, provider } from './config'; +import localCourseData from './data/coursedata.json'; +import { CourseDataType, CourseType } from './types'; +import { getCookieValue, getWidth } from './utils'; import firebase from 'firebase/compat/app'; -import { FC, useCallback, useEffect } from 'react'; +import { FC, useCallback, useEffect, useMemo, useState } from 'react'; -interface AppProps { - user: firebase.User | null; - classItems: JSX.Element; - authLevel: number; -} +const COURSE_ID_REGEX = /[A-Z]{3}[0-9]{3}/g; +const COURSE_ID_REGEX_SINGLE = /[A-Z]{3}[0-9]{3}/; -const App: FC = ({ user, classItems, authLevel }): JSX.Element => { - // Hacky workaround because something with React probably interferes with the default browser behavior - // Also, reactivates the highlight on the course element +const TAG_CODES: Record = { + ADP: 'Advanced Placement', + CSC: 'Computer Science', + MAT: 'Math', + BUS: 'Business', + SOC: 'Social Studies', + ENG: 'English', + IND: 'Engineering', + FAM: 'Family Consumer Sciences', + AGR: 'Agriculture', + SCI: 'Science', + HPE: 'Health/PE', + ART: 'Art', + FOR: 'Foreign Language', + MUS: 'Music', + TAG: 'Talented and Gifted', + VEN: 'Venture', +}; + +const getNumColumns = (width: number): number => { + if (width > 1400) return 4; + if (width > 1100) return 3; + if (width > 800) return 2; + return 1; +}; + +const App: FC = (): JSX.Element => { + const [courseData, setCourseData] = useState(null); + const [user, setUser] = useState(() => { + const cookie = getCookieValue('user'); + if (!cookie) return null; + try { + return JSON.parse(cookie); + } catch { + return null; + } + }); + const [authLevel, setAuthLevel] = useState(0); + const [searchQuery, setSearchQuery] = useState(''); + const [activeTags, setActiveTags] = useState([]); + const [windowWidth, setWindowWidth] = useState(getWidth()); + + // Fetch course data from Firebase, fall back to local data + useEffect(() => { + db.ref() + .get() + .then((snapshot) => { + setCourseData(snapshot.exists() ? snapshot.val().courses : localCourseData); + }) + .catch(() => setCourseData(localCourseData)); + }, []); + + // Fetch auth level for the signed-in user + useEffect(() => { + if (!user) return; + db.ref() + .get() + .then((snapshot) => { + if (!snapshot.exists()) return; + const users: { [key: string]: { email: string; level: number } } = + snapshot.val().users; + Object.values(users).forEach(({ email, level }) => { + if (user.email === email) setAuthLevel(level); + }); + }); + }, [user]); + + // Window resize listener for responsive column layout + useEffect(() => { + const handleResize = () => setWindowWidth(getWidth()); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + // Scroll listener: show/hide desktop back-to-top button and mobile nav + 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]); + + // Re-trigger hash jump after initial course data loads + // (React rendering can interfere with native hash navigation) useEffect(() => { const jumpId = window.location.hash; window.location.hash = ''; window.location.hash = jumpId; }, []); + const courseIDtoNameMap = useMemo(() => { + const map = new Map(); + for (const courseName in courseData) { + courseData![courseName].courseid.match(COURSE_ID_REGEX)?.forEach((id) => { + map.set(id, courseName); + }); + } + return map; + }, [courseData]); + + const courseIDtoCourse = useCallback( + (courseID: string): CourseType => { + const id = courseID.match(COURSE_ID_REGEX_SINGLE)?.[0] ?? ''; + return courseData?.[courseIDtoNameMap.get(id) ?? ''] ?? ('' as unknown as CourseType); + }, + [courseData, courseIDtoNameMap] + ); + + const filteredCourseItems = useMemo(() => { + if (!courseData) return []; + const key = encodeURIComponent( + searchQuery.toLowerCase().replaceAll(' ', '-') + ).replace(/\./g, '%2E'); + return Object.keys(courseData) + .filter((name) => { + const matchesSearch = name.search(key) !== -1; + const matchesTags = + activeTags.length === 0 || + activeTags.some((tag) => courseData[name].tags?.includes(TAG_CODES[tag])); + return matchesSearch && matchesTags; + }) + .map((name) => ( + + )); + }, [courseData, searchQuery, activeTags, authLevel, courseIDtoCourse]); + + const handleTagToggle = useCallback((id: string) => { + setActiveTags((prev) => + prev.includes(id) ? prev.filter((t) => t !== id) : [...prev, id] + ); + }, []); + + const handleClearFilters = useCallback(() => { + setSearchQuery(''); + setActiveTags([]); + }, []); + + const handleSignIn = useCallback(() => { + if (user) { + 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)}`; + }); + } + }, [user]); + const handleContactModalOpen = useCallback(() => { const contactModal = document.getElementById( 'contact-modal' @@ -34,26 +203,63 @@ const App: FC = ({ user, classItems, authLevel }): JSX.Element => { contactModal?.showModal(); }, []); + const numColumns = getNumColumns(windowWidth); + + const classItems = useMemo(() => { + const items = + filteredCourseItems.length > 0 + ? filteredCourseItems + : []; + const flexParents = Array.from({ length: numColumns }, (_, i) => ( +
+ {items.filter((_, j) => j % numColumns === i)} +
+ )); + return
{flexParents}
; + }, [filteredCourseItems, numColumns, handleClearFilters]); + const userElement: JSX.Element = - user && user.photoURL ? ( + user?.photoURL ? (
User profile
) : ( -
+
); + if (!courseData) { + return ; + } + return (
- {/* */}
- - -
{classItems}
+ + +
+ {classItems} +
diff --git a/src/components/ClearFilter.tsx b/src/components/ClearFilter.tsx index a260108..12c4530 100644 --- a/src/components/ClearFilter.tsx +++ b/src/components/ClearFilter.tsx @@ -1,45 +1,36 @@ import '../App.css'; -import { filterCourses } from '../index'; import { getWidth } from '../utils'; -import { FC } from 'react'; +import { FC, useEffect } from 'react'; -export const ClearFilter: FC = (): JSX.Element => { - const courseContainer = document.getElementById( - 'course-container' - ) as HTMLDivElement; - courseContainer.style.display = 'flex'; - courseContainer.style.justifyContent = 'center'; +interface ClearFilterProps { + onClearFilters: () => void; +} - const adjustCourseContainerHeight = () => { - if (getWidth() >= 525) { - courseContainer.style.height = 'calc(100vh - 180px)'; - } else { - courseContainer.style.height = - 'calc(100vh - (45.5px + 65.5px + (clamp(15px, 5vw, 20px) * 2)))'; - } - }; - - adjustCourseContainerHeight(); - window.addEventListener('resize', adjustCourseContainerHeight); +export const ClearFilter: FC = ({ onClearFilters }): JSX.Element => { + 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 = ''; + }; + }, []); return (

No Results

-
diff --git a/src/components/Course.tsx b/src/components/Course.tsx index ca36c1d..9fc114e 100644 --- a/src/components/Course.tsx +++ b/src/components/Course.tsx @@ -1,13 +1,8 @@ import '../App.css'; -import { firebaseConfig } from '../config'; +import { db } from '../config'; import { CourseType } from '../types'; -import firebase from 'firebase/compat/app'; -import 'firebase/compat/database'; import { FC, useCallback, useRef, useState, useMemo } from 'react'; -firebase.initializeApp(firebaseConfig); -firebase.database().ref(); - interface CourseProps { course: CourseType; authLevel: number; @@ -114,12 +109,9 @@ export const Course: FC = ({ }) ); - firebase - .database() - .ref('courses') - .update({ - [loopName]: { ...course, ...overwriteCourse }, - }); + db.ref('courses').update({ + [loopName]: { ...course, ...overwriteCourse }, + }); // Exit the edit menu setIsEditing(false); @@ -133,9 +125,8 @@ export const Course: FC = ({ const isVenture = course.coursename.includes('Venture'); const Style = { - gridRow: `span ${ - course.courses?.match(/[A-Z][A-Z][A-Z][0-9][0-9][0-9]/gm)?.length ?? 1 - }`, + gridRow: `span ${course.courses?.match(/[A-Z][A-Z][A-Z][0-9][0-9][0-9]/gm)?.length ?? 1 + }`, backgroundColor: isVenture ? 'var(--primary-light)' : '', }; diff --git a/src/components/TagBar.tsx b/src/components/TagBar.tsx index b7e3923..a30a524 100644 --- a/src/components/TagBar.tsx +++ b/src/components/TagBar.tsx @@ -1,5 +1,5 @@ import '../App.css'; -import { FC, useCallback } from 'react'; +import { FC } from 'react'; const TAGS = [ { id: 'ADP', label: 'AP' }, @@ -20,25 +20,20 @@ const TAGS = [ { id: 'VEN', label: 'Venture' }, ] as const; -export const TagBar: FC = (): JSX.Element => { - const handleTagToggle = useCallback((id: string): void => { - document.getElementById(id)?.classList.toggle('tag-true'); - }, []); - - const handleTagTrueRemove = useCallback(() => { - const tags = document.getElementsByClassName('tag'); - for (let i = 0; i < tags.length; i++) { - tags[i].classList.remove('tag-true'); - } - }, []); +interface TagBarProps { + activeTags: string[]; + onTagToggle: (id: string) => void; + onClearTags: () => void; +} +export const TagBar: FC = ({ activeTags, onTagToggle, onClearTags }): JSX.Element => { return (
{userElement} diff --git a/src/config/firebase.ts b/src/config/firebase.ts index 3881a80..b31a098 100644 --- a/src/config/firebase.ts +++ b/src/config/firebase.ts @@ -1,3 +1,7 @@ +import firebase from 'firebase/compat/app'; +import 'firebase/compat/auth'; +import 'firebase/compat/database'; + export const firebaseConfig = { apiKey: 'AIzaSyDL5M-oQos8ZS499eWgEWElT9YctSeWWiU', authDomain: 'magnifyyyyy.firebaseapp.com', @@ -8,3 +12,8 @@ export const firebaseConfig = { appId: '1:798318460321:web:4290be6078dddfe8221fe0', measurementId: 'G-5X5FRMHDHE', }; + +firebase.initializeApp(firebaseConfig); +export const auth = firebase.auth(); +export const db = firebase.database(); +export const provider = new firebase.auth.GoogleAuthProvider(); diff --git a/src/config/index.ts b/src/config/index.ts index 6dd5205..1de1228 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,2 +1,2 @@ -export { firebaseConfig } from './firebase'; +export { auth, db, firebaseConfig, provider } from './firebase'; export { formSubmit } from './formSubmit'; diff --git a/src/index.tsx b/src/index.tsx index 0472abd..4d37f2c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,350 +1,13 @@ import App from './App'; -import { ClearFilter, Course, Loader } from './components'; -import { firebaseConfig } from './config'; -import localCourseData from './data/coursedata.json'; import './index.css'; -import { CourseDataType, CourseType } from './types'; -import { getWidth } from './utils'; -import firebase from 'firebase/compat/app'; -import 'firebase/compat/auth'; -import 'firebase/compat/database'; import React from 'react'; -import ReactDOM from 'react-dom'; +import { createRoot } from 'react-dom/client'; -const renderLoader = (): void => { - ReactDOM.render( - - - , - document.getElementById('root') - ); -}; +const container = document.getElementById('root'); +if (!container) throw new Error('Root element not found'); -renderLoader(); - -let courseData: CourseDataType; -let user: firebase.User | null; -let authData: { [key: string]: { email: string; level: number } }; -let authLevel = 0; - -firebase.initializeApp(firebaseConfig); -firebase.auth(); -const provider = new firebase.auth.GoogleAuthProvider(); -const dbRef = firebase.database().ref(); - -dbRef - .get() - .then((snapshot) => { - courseData = snapshot.exists() ? snapshot.val().courses : localCourseData; - initializeCourseViewer(); - }) - .catch(() => { - courseData = localCourseData; - initializeCourseViewer(); - }); - -const renderDOM = (courseItems: JSX.Element[], userData = user): void => { - let numColumns = 1; - - let width = getWidth(); - - if (width > 1400) { - numColumns = 4; - } else if (width > 1100) { - numColumns = 3; - } else if (width > 800) { - numColumns = 2; - } else if (width > 600) { - numColumns = 1; - } - - let flexParents = Array(numColumns) - .fill(0) - .map((_, i) => ( -
- {courseItems.filter((_, j) => j % numColumns === i)} -
- )); - - ReactDOM.render( - - {flexParents}
} - authLevel={authLevel} - /> - , - document.getElementById('root') - ); -}; - -const initializeCourseViewer = (): void => { - // Read the cookie and check whether the given user is authorized - if (getCookieValue('user')) { - // There shouldn't be an error here, but to prevent unnecessary crashes or security issues a try catch here is useful - try { - user = JSON.parse(getCookieValue('user')); - } catch (error) { - console.error(error); - } - } - - if (user) { - dbRef.get().then((snapshot) => { - if (snapshot.exists()) { - authData = snapshot.val().users; - - Object.keys(authData).forEach((key) => { - if (user?.email === authData[key].email) { - authLevel = authData[key].level; - } - }); - - filterCourses(); - } - }); - } - - const courseIDtoNameMap = new Map(); - for (const courseName in courseData) { - courseData[courseName].courseid - .match(/[A-Z][A-Z][A-Z][0-9][0-9][0-9]/g) - ?.forEach((id) => { - courseIDtoNameMap.set(id, courseName); - }); - } - - console.log(courseIDtoNameMap); - - const courseIDtoCourse = (courseID: string): CourseType => { - let courseid = courseID.match(/[A-Z][A-Z][A-Z][0-9][0-9][0-9]/)?.[0] ?? ''; - return courseData[courseIDtoNameMap.get(courseid) ?? ''] ?? ''; - }; - - const courseArray = Object.keys(courseData); - - const courseItems = courseArray.map((name) => ( - - )); - - renderDOM(courseItems); - - const search = document.getElementById('searchbar'); - const tagButtons = document.getElementsByClassName('tag'); - const signInButton = document.getElementById('signer'); - const topButton = document.getElementById('to-top'); - - search?.addEventListener('input', filterCourses); - signInButton?.addEventListener('click', signInWithPopup); - - let prevScrollpos = window.scrollY; - window.onscroll = () => { - handleDesktopScrollToTopDisplay(); - handleMobileNavigationOnScroll(); - }; - - window.addEventListener('resize', () => { - renderDOM(courseItems); - filterCourses(); - // This is REALLY bad for performance... - // Need to implement subgrid at some point - }); - - const handleDesktopScrollToTopDisplay = (): void => { - if (!topButton) { - throw new Error('Supposed element with id to-top nonexistent'); - } - - if (window.scrollY >= 80 && getWidth() >= 525) { - topButton.style.visibility = 'visible'; - topButton.style.opacity = '1'; - } else { - topButton.style.visibility = 'hidden'; - topButton.style.opacity = '0'; - } - }; - - const handleMobileNavigationOnScroll = (): void => { - if (getWidth() >= 525) return; - - const nav = document.getElementById('mobile-nav'); - if (!nav) { - throw new Error('Supposed element with id mobile-nav nonexistent'); - } - - const currentScrollPos = window.scrollY; - if (prevScrollpos > currentScrollPos) { - nav.style.bottom = '0'; - } else { - nav.style.bottom = `-${nav.offsetHeight}px`; - } - prevScrollpos = currentScrollPos; - }; - - for (let i = 0; i < tagButtons.length; i++) { - const btn = tagButtons[i]; - btn.addEventListener('click', filterCourses); - } - - // This is for the search bar to reload results on enter instead of every time a new input is detected - // Uncomment this for the above behavior, and comment out the other event listener - // search?.addEventListener('keydown', function (e) { - // if (e.code === 'Enter') { - // filterArr(e); - // } - // }); -}; - -export const filterCourses = (): void => { - setTimeout(() => { - const tagCodes = { - ADP: 'Advanced Placement', - CSC: 'Computer Science', - MAT: 'Math', - BUS: 'Business', - SOC: 'Social Studies', - ENG: 'English', - IND: 'Engineering', - FAM: 'Family Consumer Sciences', - AGR: 'Agriculture', - SCI: 'Science', - HPE: 'Health/PE', - ART: 'Art', - FOR: 'Foreign Language', - MUS: 'Music', - TAG: 'Talented and Gifted', - VEN: 'Venture', - }; - - const search = document.getElementById('searchbar') as HTMLInputElement; - let renderedItems = Object.keys(courseData); - const tags = document.getElementsByClassName('tag'); - const tagAll = document.getElementById('ALL'); - const trueTags: string[] = []; - - for (let i = 0; i < tags.length; i++) { - if (tags[i].classList.contains('tag-true')) { - trueTags.push(tags[i].id); - tagAll?.classList.remove('tag-all'); - } - } - - const isValidKey = (key: string): key is keyof typeof tagCodes => { - return key in tagCodes; - }; - - // If this is not true, all the tags are not true and no filtering action needs to be done - if (trueTags.length) { - renderedItems = renderedItems.filter((name) => { - let isPresent = false; - - for (let i = 0; i < trueTags.length; i++) { - const tag = trueTags[i]; - if (!isValidKey(tag)) { - throw new Error(`${tag} is an invalid index for tagCodes`); - } - - if (courseData[name].tags?.includes(tagCodes[tag])) { - isPresent = true; - } - } - - return isPresent; - }); - } else { - tagAll?.classList.add('tag-all'); - } - - const courseIDtoNameMap = new Map(); - for (const courseName in courseData) { - courseIDtoNameMap.set(courseData[courseName].courseid, courseName); - } - - const courseIDtoCourse = (courseID: string): CourseType => - courseData[courseIDtoNameMap.get(courseID) ?? ''] ?? ''; - - // Used to convert normal text to the encoded database keys in the firebase db - const key = encodeURIComponent( - search?.value.toLowerCase().replaceAll(' ', '-') - ).replace(/\./g, '%2E'); - const renderedElements = renderedItems - .filter((name) => name.search(key) !== -1) - .map((name) => { - return ( - - ); - }); - - if (!renderedElements.length) { - renderedElements.push(); - } else { - const courseContainer = document.getElementById( - 'course-container' - ) as HTMLDivElement; - courseContainer.style.display = ''; - courseContainer.style.justifyContent = ''; - courseContainer.style.height = ''; - } - renderDOM(renderedElements); - window.location.hash = ''; - }, 20); -}; - -const signInWithPopup = (): void => { - const button = document.getElementById('signer'); - if (button?.textContent === 'Login') { - firebase - .auth() - .signInWithPopup(provider) - .then((result) => { - // The signed-in user info. - user = result.user; - - if (user) { - document.cookie = `user=${JSON.stringify(user)}`; - } - - dbRef.get().then((snapshot) => { - if (!snapshot.exists()) return; - authData = snapshot.val().users; - - // Authorize the user if the user has been logged-in - if (user !== null) { - Object.keys(authData).forEach((key) => { - if (user?.email === authData[key].email) { - authLevel = authData[key].level; - } - }); - filterCourses(); - } - }); - }); - } else { - firebase - .auth() - .signOut() - .then(() => { - user = null; - authLevel = 0; - document.cookie = - 'user=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; - filterCourses(); // Reload the DOM to update sign-in status - // Sign-out successful - }); - } -}; - -const getCookieValue = (name: string): string => - document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)')?.pop() || ''; +createRoot(container).render( + + + +); diff --git a/src/utils/getCookieValue.ts b/src/utils/getCookieValue.ts new file mode 100644 index 0000000..7156f4a --- /dev/null +++ b/src/utils/getCookieValue.ts @@ -0,0 +1,2 @@ +export const getCookieValue = (name: string): string => + document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)')?.pop() || ''; diff --git a/src/utils/index.ts b/src/utils/index.ts index 4acce19..0e17d0f 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,2 +1,3 @@ +export { getCookieValue } from './getCookieValue'; export { getWidth } from './getWidth'; export { scrollToTop } from './scrollToTop'; From bec3fba4b37ff54bdbb129e8b4e53cc52e1c9df3 Mon Sep 17 00:00:00 2001 From: Ascent817 Date: Thu, 19 Feb 2026 16:37:01 -0800 Subject: [PATCH 04/15] Run prettier --- src/App.tsx | 31 +++++++++++++++++++------------ src/components/ClearFilter.tsx | 4 +++- src/components/Course.tsx | 5 +++-- src/components/TagBar.tsx | 6 +++++- 4 files changed, 30 insertions(+), 16 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 11f6a86..f023e0b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -67,7 +67,9 @@ const App: FC = (): JSX.Element => { db.ref() .get() .then((snapshot) => { - setCourseData(snapshot.exists() ? snapshot.val().courses : localCourseData); + setCourseData( + snapshot.exists() ? snapshot.val().courses : localCourseData + ); }) .catch(() => setCourseData(localCourseData)); }, []); @@ -138,7 +140,10 @@ const App: FC = (): JSX.Element => { const courseIDtoCourse = useCallback( (courseID: string): CourseType => { const id = courseID.match(COURSE_ID_REGEX_SINGLE)?.[0] ?? ''; - return courseData?.[courseIDtoNameMap.get(id) ?? ''] ?? ('' as unknown as CourseType); + return ( + courseData?.[courseIDtoNameMap.get(id) ?? ''] ?? + ('' as unknown as CourseType) + ); }, [courseData, courseIDtoNameMap] ); @@ -153,7 +158,9 @@ const App: FC = (): JSX.Element => { const matchesSearch = name.search(key) !== -1; const matchesTags = activeTags.length === 0 || - activeTags.some((tag) => courseData[name].tags?.includes(TAG_CODES[tag])); + activeTags.some((tag) => + courseData[name].tags?.includes(TAG_CODES[tag]) + ); return matchesSearch && matchesTags; }) .map((name) => ( @@ -183,7 +190,8 @@ const App: FC = (): JSX.Element => { auth.signOut().then(() => { setUser(null); setAuthLevel(0); - document.cookie = 'user=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; + document.cookie = + 'user=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; }); } else { auth.signInWithPopup(provider).then((result) => { @@ -218,14 +226,13 @@ const App: FC = (): JSX.Element => { return
{flexParents}
; }, [filteredCourseItems, numColumns, handleClearFilters]); - const userElement: JSX.Element = - user?.photoURL ? ( -
- User profile -
- ) : ( -
- ); + const userElement: JSX.Element = user?.photoURL ? ( +
+ User profile +
+ ) : ( +
+ ); if (!courseData) { return ; diff --git a/src/components/ClearFilter.tsx b/src/components/ClearFilter.tsx index 12c4530..3098c31 100644 --- a/src/components/ClearFilter.tsx +++ b/src/components/ClearFilter.tsx @@ -6,7 +6,9 @@ interface ClearFilterProps { onClearFilters: () => void; } -export const ClearFilter: FC = ({ onClearFilters }): JSX.Element => { +export const ClearFilter: FC = ({ + onClearFilters, +}): JSX.Element => { useEffect(() => { const courseContainer = document.getElementById( 'course-container' diff --git a/src/components/Course.tsx b/src/components/Course.tsx index 9fc114e..1fd0644 100644 --- a/src/components/Course.tsx +++ b/src/components/Course.tsx @@ -125,8 +125,9 @@ export const Course: FC = ({ const isVenture = course.coursename.includes('Venture'); const Style = { - gridRow: `span ${course.courses?.match(/[A-Z][A-Z][A-Z][0-9][0-9][0-9]/gm)?.length ?? 1 - }`, + gridRow: `span ${ + course.courses?.match(/[A-Z][A-Z][A-Z][0-9][0-9][0-9]/gm)?.length ?? 1 + }`, backgroundColor: isVenture ? 'var(--primary-light)' : '', }; diff --git a/src/components/TagBar.tsx b/src/components/TagBar.tsx index a30a524..cfa9e6b 100644 --- a/src/components/TagBar.tsx +++ b/src/components/TagBar.tsx @@ -26,7 +26,11 @@ interface TagBarProps { onClearTags: () => void; } -export const TagBar: FC = ({ activeTags, onTagToggle, onClearTags }): JSX.Element => { +export const TagBar: FC = ({ + activeTags, + onTagToggle, + onClearTags, +}): JSX.Element => { return (
- - - - - - - - - - - - - - - - + + {TAGS.map(({ id, label }) => ( + + ))}
); }; From 6c9d9c2e66ed31fdd1c6237d5e65684b74250daf Mon Sep 17 00:00:00 2001 From: Ascent817 Date: Thu, 19 Feb 2026 16:27:43 -0800 Subject: [PATCH 09/15] Clean up codebase to better follow React best practices --- src/App.tsx | 238 ++++++++++++++++++++-- src/components/ClearFilter.tsx | 55 +++-- src/components/Course.tsx | 21 +- src/components/TagBar.tsx | 27 +-- src/components/TopBar.tsx | 15 +- src/config/firebase.ts | 9 + src/config/index.ts | 2 +- src/index.tsx | 353 +-------------------------------- src/utils/getCookieValue.ts | 2 + src/utils/index.ts | 1 + 10 files changed, 296 insertions(+), 427 deletions(-) create mode 100644 src/utils/getCookieValue.ts diff --git a/src/App.tsx b/src/App.tsx index fe089f3..11f6a86 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,31 +1,200 @@ import './App.css'; import { + ClearFilter, ContactForm, + Course, FloatingActionable, + Loader, MobileNavigation, Navigation, TagBar, TopBar, } from './components'; -// import { ParsePDF } from './components/ParsePDF'; +import { auth, db, provider } from './config'; +import localCourseData from './data/coursedata.json'; +import { CourseDataType, CourseType } from './types'; +import { getCookieValue, getWidth } from './utils'; import firebase from 'firebase/compat/app'; -import { FC, useCallback, useEffect } from 'react'; +import { FC, useCallback, useEffect, useMemo, useState } from 'react'; -interface AppProps { - user: firebase.User | null; - classItems: JSX.Element; - authLevel: number; -} +const COURSE_ID_REGEX = /[A-Z]{3}[0-9]{3}/g; +const COURSE_ID_REGEX_SINGLE = /[A-Z]{3}[0-9]{3}/; -const App: FC = ({ user, classItems, authLevel }): JSX.Element => { - // Hacky workaround because something with React probably interferes with the default browser behavior - // Also, reactivates the highlight on the course element +const TAG_CODES: Record = { + ADP: 'Advanced Placement', + CSC: 'Computer Science', + MAT: 'Math', + BUS: 'Business', + SOC: 'Social Studies', + ENG: 'English', + IND: 'Engineering', + FAM: 'Family Consumer Sciences', + AGR: 'Agriculture', + SCI: 'Science', + HPE: 'Health/PE', + ART: 'Art', + FOR: 'Foreign Language', + MUS: 'Music', + TAG: 'Talented and Gifted', + VEN: 'Venture', +}; + +const getNumColumns = (width: number): number => { + if (width > 1400) return 4; + if (width > 1100) return 3; + if (width > 800) return 2; + return 1; +}; + +const App: FC = (): JSX.Element => { + const [courseData, setCourseData] = useState(null); + const [user, setUser] = useState(() => { + const cookie = getCookieValue('user'); + if (!cookie) return null; + try { + return JSON.parse(cookie); + } catch { + return null; + } + }); + const [authLevel, setAuthLevel] = useState(0); + const [searchQuery, setSearchQuery] = useState(''); + const [activeTags, setActiveTags] = useState([]); + const [windowWidth, setWindowWidth] = useState(getWidth()); + + // Fetch course data from Firebase, fall back to local data + useEffect(() => { + db.ref() + .get() + .then((snapshot) => { + setCourseData(snapshot.exists() ? snapshot.val().courses : localCourseData); + }) + .catch(() => setCourseData(localCourseData)); + }, []); + + // Fetch auth level for the signed-in user + useEffect(() => { + if (!user) return; + db.ref() + .get() + .then((snapshot) => { + if (!snapshot.exists()) return; + const users: { [key: string]: { email: string; level: number } } = + snapshot.val().users; + Object.values(users).forEach(({ email, level }) => { + if (user.email === email) setAuthLevel(level); + }); + }); + }, [user]); + + // Window resize listener for responsive column layout + useEffect(() => { + const handleResize = () => setWindowWidth(getWidth()); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + // Scroll listener: show/hide desktop back-to-top button and mobile nav + 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]); + + // Re-trigger hash jump after initial course data loads + // (React rendering can interfere with native hash navigation) useEffect(() => { const jumpId = window.location.hash; window.location.hash = ''; window.location.hash = jumpId; }, []); + const courseIDtoNameMap = useMemo(() => { + const map = new Map(); + for (const courseName in courseData) { + courseData![courseName].courseid.match(COURSE_ID_REGEX)?.forEach((id) => { + map.set(id, courseName); + }); + } + return map; + }, [courseData]); + + const courseIDtoCourse = useCallback( + (courseID: string): CourseType => { + const id = courseID.match(COURSE_ID_REGEX_SINGLE)?.[0] ?? ''; + return courseData?.[courseIDtoNameMap.get(id) ?? ''] ?? ('' as unknown as CourseType); + }, + [courseData, courseIDtoNameMap] + ); + + const filteredCourseItems = useMemo(() => { + if (!courseData) return []; + const key = encodeURIComponent( + searchQuery.toLowerCase().replaceAll(' ', '-') + ).replace(/\./g, '%2E'); + return Object.keys(courseData) + .filter((name) => { + const matchesSearch = name.search(key) !== -1; + const matchesTags = + activeTags.length === 0 || + activeTags.some((tag) => courseData[name].tags?.includes(TAG_CODES[tag])); + return matchesSearch && matchesTags; + }) + .map((name) => ( + + )); + }, [courseData, searchQuery, activeTags, authLevel, courseIDtoCourse]); + + const handleTagToggle = useCallback((id: string) => { + setActiveTags((prev) => + prev.includes(id) ? prev.filter((t) => t !== id) : [...prev, id] + ); + }, []); + + const handleClearFilters = useCallback(() => { + setSearchQuery(''); + setActiveTags([]); + }, []); + + const handleSignIn = useCallback(() => { + if (user) { + 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)}`; + }); + } + }, [user]); + const handleContactModalOpen = useCallback(() => { const contactModal = document.getElementById( 'contact-modal' @@ -34,26 +203,63 @@ const App: FC = ({ user, classItems, authLevel }): JSX.Element => { contactModal?.showModal(); }, []); + const numColumns = getNumColumns(windowWidth); + + const classItems = useMemo(() => { + const items = + filteredCourseItems.length > 0 + ? filteredCourseItems + : []; + const flexParents = Array.from({ length: numColumns }, (_, i) => ( +
+ {items.filter((_, j) => j % numColumns === i)} +
+ )); + return
{flexParents}
; + }, [filteredCourseItems, numColumns, handleClearFilters]); + const userElement: JSX.Element = - user && user.photoURL ? ( + user?.photoURL ? (
User profile
) : ( -
+
); + if (!courseData) { + return ; + } + return (
- {/* */}
- - -
{classItems}
+ + +
+ {classItems} +
diff --git a/src/components/ClearFilter.tsx b/src/components/ClearFilter.tsx index a260108..12c4530 100644 --- a/src/components/ClearFilter.tsx +++ b/src/components/ClearFilter.tsx @@ -1,45 +1,36 @@ import '../App.css'; -import { filterCourses } from '../index'; import { getWidth } from '../utils'; -import { FC } from 'react'; +import { FC, useEffect } from 'react'; -export const ClearFilter: FC = (): JSX.Element => { - const courseContainer = document.getElementById( - 'course-container' - ) as HTMLDivElement; - courseContainer.style.display = 'flex'; - courseContainer.style.justifyContent = 'center'; +interface ClearFilterProps { + onClearFilters: () => void; +} - const adjustCourseContainerHeight = () => { - if (getWidth() >= 525) { - courseContainer.style.height = 'calc(100vh - 180px)'; - } else { - courseContainer.style.height = - 'calc(100vh - (45.5px + 65.5px + (clamp(15px, 5vw, 20px) * 2)))'; - } - }; - - adjustCourseContainerHeight(); - window.addEventListener('resize', adjustCourseContainerHeight); +export const ClearFilter: FC = ({ onClearFilters }): JSX.Element => { + 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 = ''; + }; + }, []); return (

No Results

-
diff --git a/src/components/Course.tsx b/src/components/Course.tsx index ca36c1d..9fc114e 100644 --- a/src/components/Course.tsx +++ b/src/components/Course.tsx @@ -1,13 +1,8 @@ import '../App.css'; -import { firebaseConfig } from '../config'; +import { db } from '../config'; import { CourseType } from '../types'; -import firebase from 'firebase/compat/app'; -import 'firebase/compat/database'; import { FC, useCallback, useRef, useState, useMemo } from 'react'; -firebase.initializeApp(firebaseConfig); -firebase.database().ref(); - interface CourseProps { course: CourseType; authLevel: number; @@ -114,12 +109,9 @@ export const Course: FC = ({ }) ); - firebase - .database() - .ref('courses') - .update({ - [loopName]: { ...course, ...overwriteCourse }, - }); + db.ref('courses').update({ + [loopName]: { ...course, ...overwriteCourse }, + }); // Exit the edit menu setIsEditing(false); @@ -133,9 +125,8 @@ export const Course: FC = ({ const isVenture = course.coursename.includes('Venture'); const Style = { - gridRow: `span ${ - course.courses?.match(/[A-Z][A-Z][A-Z][0-9][0-9][0-9]/gm)?.length ?? 1 - }`, + gridRow: `span ${course.courses?.match(/[A-Z][A-Z][A-Z][0-9][0-9][0-9]/gm)?.length ?? 1 + }`, backgroundColor: isVenture ? 'var(--primary-light)' : '', }; diff --git a/src/components/TagBar.tsx b/src/components/TagBar.tsx index b7e3923..a30a524 100644 --- a/src/components/TagBar.tsx +++ b/src/components/TagBar.tsx @@ -1,5 +1,5 @@ import '../App.css'; -import { FC, useCallback } from 'react'; +import { FC } from 'react'; const TAGS = [ { id: 'ADP', label: 'AP' }, @@ -20,25 +20,20 @@ const TAGS = [ { id: 'VEN', label: 'Venture' }, ] as const; -export const TagBar: FC = (): JSX.Element => { - const handleTagToggle = useCallback((id: string): void => { - document.getElementById(id)?.classList.toggle('tag-true'); - }, []); - - const handleTagTrueRemove = useCallback(() => { - const tags = document.getElementsByClassName('tag'); - for (let i = 0; i < tags.length; i++) { - tags[i].classList.remove('tag-true'); - } - }, []); +interface TagBarProps { + activeTags: string[]; + onTagToggle: (id: string) => void; + onClearTags: () => void; +} +export const TagBar: FC = ({ activeTags, onTagToggle, onClearTags }): JSX.Element => { return (
{userElement} diff --git a/src/config/firebase.ts b/src/config/firebase.ts index 3881a80..b31a098 100644 --- a/src/config/firebase.ts +++ b/src/config/firebase.ts @@ -1,3 +1,7 @@ +import firebase from 'firebase/compat/app'; +import 'firebase/compat/auth'; +import 'firebase/compat/database'; + export const firebaseConfig = { apiKey: 'AIzaSyDL5M-oQos8ZS499eWgEWElT9YctSeWWiU', authDomain: 'magnifyyyyy.firebaseapp.com', @@ -8,3 +12,8 @@ export const firebaseConfig = { appId: '1:798318460321:web:4290be6078dddfe8221fe0', measurementId: 'G-5X5FRMHDHE', }; + +firebase.initializeApp(firebaseConfig); +export const auth = firebase.auth(); +export const db = firebase.database(); +export const provider = new firebase.auth.GoogleAuthProvider(); diff --git a/src/config/index.ts b/src/config/index.ts index 6dd5205..1de1228 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,2 +1,2 @@ -export { firebaseConfig } from './firebase'; +export { auth, db, firebaseConfig, provider } from './firebase'; export { formSubmit } from './formSubmit'; diff --git a/src/index.tsx b/src/index.tsx index 0472abd..4d37f2c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,350 +1,13 @@ import App from './App'; -import { ClearFilter, Course, Loader } from './components'; -import { firebaseConfig } from './config'; -import localCourseData from './data/coursedata.json'; import './index.css'; -import { CourseDataType, CourseType } from './types'; -import { getWidth } from './utils'; -import firebase from 'firebase/compat/app'; -import 'firebase/compat/auth'; -import 'firebase/compat/database'; import React from 'react'; -import ReactDOM from 'react-dom'; +import { createRoot } from 'react-dom/client'; -const renderLoader = (): void => { - ReactDOM.render( - - - , - document.getElementById('root') - ); -}; +const container = document.getElementById('root'); +if (!container) throw new Error('Root element not found'); -renderLoader(); - -let courseData: CourseDataType; -let user: firebase.User | null; -let authData: { [key: string]: { email: string; level: number } }; -let authLevel = 0; - -firebase.initializeApp(firebaseConfig); -firebase.auth(); -const provider = new firebase.auth.GoogleAuthProvider(); -const dbRef = firebase.database().ref(); - -dbRef - .get() - .then((snapshot) => { - courseData = snapshot.exists() ? snapshot.val().courses : localCourseData; - initializeCourseViewer(); - }) - .catch(() => { - courseData = localCourseData; - initializeCourseViewer(); - }); - -const renderDOM = (courseItems: JSX.Element[], userData = user): void => { - let numColumns = 1; - - let width = getWidth(); - - if (width > 1400) { - numColumns = 4; - } else if (width > 1100) { - numColumns = 3; - } else if (width > 800) { - numColumns = 2; - } else if (width > 600) { - numColumns = 1; - } - - let flexParents = Array(numColumns) - .fill(0) - .map((_, i) => ( -
- {courseItems.filter((_, j) => j % numColumns === i)} -
- )); - - ReactDOM.render( - - {flexParents}
} - authLevel={authLevel} - /> - , - document.getElementById('root') - ); -}; - -const initializeCourseViewer = (): void => { - // Read the cookie and check whether the given user is authorized - if (getCookieValue('user')) { - // There shouldn't be an error here, but to prevent unnecessary crashes or security issues a try catch here is useful - try { - user = JSON.parse(getCookieValue('user')); - } catch (error) { - console.error(error); - } - } - - if (user) { - dbRef.get().then((snapshot) => { - if (snapshot.exists()) { - authData = snapshot.val().users; - - Object.keys(authData).forEach((key) => { - if (user?.email === authData[key].email) { - authLevel = authData[key].level; - } - }); - - filterCourses(); - } - }); - } - - const courseIDtoNameMap = new Map(); - for (const courseName in courseData) { - courseData[courseName].courseid - .match(/[A-Z][A-Z][A-Z][0-9][0-9][0-9]/g) - ?.forEach((id) => { - courseIDtoNameMap.set(id, courseName); - }); - } - - console.log(courseIDtoNameMap); - - const courseIDtoCourse = (courseID: string): CourseType => { - let courseid = courseID.match(/[A-Z][A-Z][A-Z][0-9][0-9][0-9]/)?.[0] ?? ''; - return courseData[courseIDtoNameMap.get(courseid) ?? ''] ?? ''; - }; - - const courseArray = Object.keys(courseData); - - const courseItems = courseArray.map((name) => ( - - )); - - renderDOM(courseItems); - - const search = document.getElementById('searchbar'); - const tagButtons = document.getElementsByClassName('tag'); - const signInButton = document.getElementById('signer'); - const topButton = document.getElementById('to-top'); - - search?.addEventListener('input', filterCourses); - signInButton?.addEventListener('click', signInWithPopup); - - let prevScrollpos = window.scrollY; - window.onscroll = () => { - handleDesktopScrollToTopDisplay(); - handleMobileNavigationOnScroll(); - }; - - window.addEventListener('resize', () => { - renderDOM(courseItems); - filterCourses(); - // This is REALLY bad for performance... - // Need to implement subgrid at some point - }); - - const handleDesktopScrollToTopDisplay = (): void => { - if (!topButton) { - throw new Error('Supposed element with id to-top nonexistent'); - } - - if (window.scrollY >= 80 && getWidth() >= 525) { - topButton.style.visibility = 'visible'; - topButton.style.opacity = '1'; - } else { - topButton.style.visibility = 'hidden'; - topButton.style.opacity = '0'; - } - }; - - const handleMobileNavigationOnScroll = (): void => { - if (getWidth() >= 525) return; - - const nav = document.getElementById('mobile-nav'); - if (!nav) { - throw new Error('Supposed element with id mobile-nav nonexistent'); - } - - const currentScrollPos = window.scrollY; - if (prevScrollpos > currentScrollPos) { - nav.style.bottom = '0'; - } else { - nav.style.bottom = `-${nav.offsetHeight}px`; - } - prevScrollpos = currentScrollPos; - }; - - for (let i = 0; i < tagButtons.length; i++) { - const btn = tagButtons[i]; - btn.addEventListener('click', filterCourses); - } - - // This is for the search bar to reload results on enter instead of every time a new input is detected - // Uncomment this for the above behavior, and comment out the other event listener - // search?.addEventListener('keydown', function (e) { - // if (e.code === 'Enter') { - // filterArr(e); - // } - // }); -}; - -export const filterCourses = (): void => { - setTimeout(() => { - const tagCodes = { - ADP: 'Advanced Placement', - CSC: 'Computer Science', - MAT: 'Math', - BUS: 'Business', - SOC: 'Social Studies', - ENG: 'English', - IND: 'Engineering', - FAM: 'Family Consumer Sciences', - AGR: 'Agriculture', - SCI: 'Science', - HPE: 'Health/PE', - ART: 'Art', - FOR: 'Foreign Language', - MUS: 'Music', - TAG: 'Talented and Gifted', - VEN: 'Venture', - }; - - const search = document.getElementById('searchbar') as HTMLInputElement; - let renderedItems = Object.keys(courseData); - const tags = document.getElementsByClassName('tag'); - const tagAll = document.getElementById('ALL'); - const trueTags: string[] = []; - - for (let i = 0; i < tags.length; i++) { - if (tags[i].classList.contains('tag-true')) { - trueTags.push(tags[i].id); - tagAll?.classList.remove('tag-all'); - } - } - - const isValidKey = (key: string): key is keyof typeof tagCodes => { - return key in tagCodes; - }; - - // If this is not true, all the tags are not true and no filtering action needs to be done - if (trueTags.length) { - renderedItems = renderedItems.filter((name) => { - let isPresent = false; - - for (let i = 0; i < trueTags.length; i++) { - const tag = trueTags[i]; - if (!isValidKey(tag)) { - throw new Error(`${tag} is an invalid index for tagCodes`); - } - - if (courseData[name].tags?.includes(tagCodes[tag])) { - isPresent = true; - } - } - - return isPresent; - }); - } else { - tagAll?.classList.add('tag-all'); - } - - const courseIDtoNameMap = new Map(); - for (const courseName in courseData) { - courseIDtoNameMap.set(courseData[courseName].courseid, courseName); - } - - const courseIDtoCourse = (courseID: string): CourseType => - courseData[courseIDtoNameMap.get(courseID) ?? ''] ?? ''; - - // Used to convert normal text to the encoded database keys in the firebase db - const key = encodeURIComponent( - search?.value.toLowerCase().replaceAll(' ', '-') - ).replace(/\./g, '%2E'); - const renderedElements = renderedItems - .filter((name) => name.search(key) !== -1) - .map((name) => { - return ( - - ); - }); - - if (!renderedElements.length) { - renderedElements.push(); - } else { - const courseContainer = document.getElementById( - 'course-container' - ) as HTMLDivElement; - courseContainer.style.display = ''; - courseContainer.style.justifyContent = ''; - courseContainer.style.height = ''; - } - renderDOM(renderedElements); - window.location.hash = ''; - }, 20); -}; - -const signInWithPopup = (): void => { - const button = document.getElementById('signer'); - if (button?.textContent === 'Login') { - firebase - .auth() - .signInWithPopup(provider) - .then((result) => { - // The signed-in user info. - user = result.user; - - if (user) { - document.cookie = `user=${JSON.stringify(user)}`; - } - - dbRef.get().then((snapshot) => { - if (!snapshot.exists()) return; - authData = snapshot.val().users; - - // Authorize the user if the user has been logged-in - if (user !== null) { - Object.keys(authData).forEach((key) => { - if (user?.email === authData[key].email) { - authLevel = authData[key].level; - } - }); - filterCourses(); - } - }); - }); - } else { - firebase - .auth() - .signOut() - .then(() => { - user = null; - authLevel = 0; - document.cookie = - 'user=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; - filterCourses(); // Reload the DOM to update sign-in status - // Sign-out successful - }); - } -}; - -const getCookieValue = (name: string): string => - document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)')?.pop() || ''; +createRoot(container).render( + + + +); diff --git a/src/utils/getCookieValue.ts b/src/utils/getCookieValue.ts new file mode 100644 index 0000000..7156f4a --- /dev/null +++ b/src/utils/getCookieValue.ts @@ -0,0 +1,2 @@ +export const getCookieValue = (name: string): string => + document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)')?.pop() || ''; diff --git a/src/utils/index.ts b/src/utils/index.ts index 4acce19..0e17d0f 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,2 +1,3 @@ +export { getCookieValue } from './getCookieValue'; export { getWidth } from './getWidth'; export { scrollToTop } from './scrollToTop'; From 5bb122dd6ff186b62e22e082ee0ce41677a3581f Mon Sep 17 00:00:00 2001 From: Ascent817 Date: Thu, 19 Feb 2026 16:37:01 -0800 Subject: [PATCH 10/15] Run prettier --- src/App.tsx | 31 +++++++++++++++++++------------ src/components/ClearFilter.tsx | 4 +++- src/components/Course.tsx | 5 +++-- src/components/TagBar.tsx | 6 +++++- 4 files changed, 30 insertions(+), 16 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 11f6a86..f023e0b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -67,7 +67,9 @@ const App: FC = (): JSX.Element => { db.ref() .get() .then((snapshot) => { - setCourseData(snapshot.exists() ? snapshot.val().courses : localCourseData); + setCourseData( + snapshot.exists() ? snapshot.val().courses : localCourseData + ); }) .catch(() => setCourseData(localCourseData)); }, []); @@ -138,7 +140,10 @@ const App: FC = (): JSX.Element => { const courseIDtoCourse = useCallback( (courseID: string): CourseType => { const id = courseID.match(COURSE_ID_REGEX_SINGLE)?.[0] ?? ''; - return courseData?.[courseIDtoNameMap.get(id) ?? ''] ?? ('' as unknown as CourseType); + return ( + courseData?.[courseIDtoNameMap.get(id) ?? ''] ?? + ('' as unknown as CourseType) + ); }, [courseData, courseIDtoNameMap] ); @@ -153,7 +158,9 @@ const App: FC = (): JSX.Element => { const matchesSearch = name.search(key) !== -1; const matchesTags = activeTags.length === 0 || - activeTags.some((tag) => courseData[name].tags?.includes(TAG_CODES[tag])); + activeTags.some((tag) => + courseData[name].tags?.includes(TAG_CODES[tag]) + ); return matchesSearch && matchesTags; }) .map((name) => ( @@ -183,7 +190,8 @@ const App: FC = (): JSX.Element => { auth.signOut().then(() => { setUser(null); setAuthLevel(0); - document.cookie = 'user=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; + document.cookie = + 'user=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; }); } else { auth.signInWithPopup(provider).then((result) => { @@ -218,14 +226,13 @@ const App: FC = (): JSX.Element => { return
{flexParents}
; }, [filteredCourseItems, numColumns, handleClearFilters]); - const userElement: JSX.Element = - user?.photoURL ? ( -
- User profile -
- ) : ( -
- ); + const userElement: JSX.Element = user?.photoURL ? ( +
+ User profile +
+ ) : ( +
+ ); if (!courseData) { return ; diff --git a/src/components/ClearFilter.tsx b/src/components/ClearFilter.tsx index 12c4530..3098c31 100644 --- a/src/components/ClearFilter.tsx +++ b/src/components/ClearFilter.tsx @@ -6,7 +6,9 @@ interface ClearFilterProps { onClearFilters: () => void; } -export const ClearFilter: FC = ({ onClearFilters }): JSX.Element => { +export const ClearFilter: FC = ({ + onClearFilters, +}): JSX.Element => { useEffect(() => { const courseContainer = document.getElementById( 'course-container' diff --git a/src/components/Course.tsx b/src/components/Course.tsx index 9fc114e..1fd0644 100644 --- a/src/components/Course.tsx +++ b/src/components/Course.tsx @@ -125,8 +125,9 @@ export const Course: FC = ({ const isVenture = course.coursename.includes('Venture'); const Style = { - gridRow: `span ${course.courses?.match(/[A-Z][A-Z][A-Z][0-9][0-9][0-9]/gm)?.length ?? 1 - }`, + gridRow: `span ${ + course.courses?.match(/[A-Z][A-Z][A-Z][0-9][0-9][0-9]/gm)?.length ?? 1 + }`, backgroundColor: isVenture ? 'var(--primary-light)' : '', }; diff --git a/src/components/TagBar.tsx b/src/components/TagBar.tsx index a30a524..cfa9e6b 100644 --- a/src/components/TagBar.tsx +++ b/src/components/TagBar.tsx @@ -26,7 +26,11 @@ interface TagBarProps { onClearTags: () => void; } -export const TagBar: FC = ({ activeTags, onTagToggle, onClearTags }): JSX.Element => { +export const TagBar: FC = ({ + activeTags, + onTagToggle, + onClearTags, +}): JSX.Element => { return (
)); return
{flexParents}
; - }, [filteredCourseItems, numColumns, handleClearFilters]); + }, [filteredCourseItems, numColumns]); const userElement: JSX.Element = user?.photoURL ? (
From e320c924d8d15f404cbea891c146cb3eef23a6c8 Mon Sep 17 00:00:00 2001 From: GoogolGenius <81032623+GoogolGenius@users.noreply.github.com> Date: Sun, 22 Feb 2026 18:51:38 -0600 Subject: [PATCH 15/15] Revert "remove handleClearFilters dependency" This reverts commit f94cfa3c507a2762cef0f866d30873e7939dc6ed. --- src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index 38e1d2a..8c8a987 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -208,7 +208,7 @@ const App: FC = (): JSX.Element => {
)); return
{flexParents}
; - }, [filteredCourseItems, numColumns]); + }, [filteredCourseItems, numColumns, handleClearFilters]); const userElement: JSX.Element = user?.photoURL ? (