From 4969be247d20c77e86600979907cbbbd01a85058 Mon Sep 17 00:00:00 2001 From: Leo <77555511+LBF-2004@users.noreply.github.com> Date: Fri, 3 Apr 2026 12:23:25 -0700 Subject: [PATCH 01/15] frontend interfaces and click flows --- apps/frontend/src/app/Catalog/index.tsx | 6 ++++- .../components/ClassBrowser/List/index.tsx | 11 ++++++--- .../src/components/ClassBrowser/index.tsx | 3 ++- apps/frontend/src/lib/recent.ts | 24 ++++++++++++++++++- 4 files changed, 38 insertions(+), 6 deletions(-) diff --git a/apps/frontend/src/app/Catalog/index.tsx b/apps/frontend/src/app/Catalog/index.tsx index f36407758..4b3b65972 100644 --- a/apps/frontend/src/app/Catalog/index.tsx +++ b/apps/frontend/src/app/Catalog/index.tsx @@ -34,6 +34,8 @@ import { compareCollectionsByBookmarksOrder } from "@/utils/collections"; import styles from "./Catalog.module.scss"; import useCatalogLayoutMode from "./hooks/useCatalogLayoutMode"; +import {saveCourseClick} from "@/lib/recent"; + // Semester hierarchy for chronological ordering (latest to earliest in year) const SEMESTER_ORDER: Record = { [Semester.Spring]: 0, @@ -508,9 +510,11 @@ export default function Catalog() { subject: string, courseNumber: string, number: string, - sessionId: string + sessionId: string, + searchQuery: string ) => { if (!term) return; + saveCourseClick(searchQuery, `${subject}${courseNumber}`); setCatalogDrawerOpen(false); // Close drawer when selecting a class diff --git a/apps/frontend/src/components/ClassBrowser/List/index.tsx b/apps/frontend/src/components/ClassBrowser/List/index.tsx index 6d459dff0..8eeb6789a 100644 --- a/apps/frontend/src/components/ClassBrowser/List/index.tsx +++ b/apps/frontend/src/components/ClassBrowser/List/index.tsx @@ -63,7 +63,8 @@ interface ListProps { subject: string, courseNumber: string, number: string, - sessionId: string + sessionId: string, + searchQuery: string, ) => void; } @@ -108,6 +109,8 @@ export default function List({ onSelect }: ListProps) { const recentlyViewedListRef = useRef(null); const recentlyViewedMeasureRef = useRef(null); + const { query } = useLayoutContext(); + useEffect(() => { setResolvedSessionIds({}); }, [year, semester]); @@ -285,7 +288,8 @@ export default function List({ onSelect }: ListProps) { selected.subject, selected.courseNumber, selected.number, - selected.sessionId + selected.sessionId, + query, ); }; @@ -336,7 +340,8 @@ export default function List({ onSelect }: ListProps) { recentClass.subject, recentClass.courseNumber, recentClass.number, - recentClass.sessionId + recentClass.sessionId, + query, ); }} > diff --git a/apps/frontend/src/components/ClassBrowser/index.tsx b/apps/frontend/src/components/ClassBrowser/index.tsx index 605374726..d5036c9fa 100644 --- a/apps/frontend/src/components/ClassBrowser/index.tsx +++ b/apps/frontend/src/components/ClassBrowser/index.tsx @@ -18,7 +18,8 @@ interface ClassBrowserProps { subject: string, courseNumber: string, number: string, - sessionId: string + sessionId: string, + searchQuery: string ) => void; forceMode?: CatalogLayoutMode; semester: Semester; diff --git a/apps/frontend/src/lib/recent.ts b/apps/frontend/src/lib/recent.ts index dddfcef82..b9a290e35 100644 --- a/apps/frontend/src/lib/recent.ts +++ b/apps/frontend/src/lib/recent.ts @@ -8,6 +8,7 @@ export enum RecentType { CatalogTerm = "recent-catalog-term", GradesPage = "recent-grades-page", EnrollmentPage = "recent-enrollment-page", + CourseClick = "recent-course-click" } const MaxLength = { @@ -17,6 +18,7 @@ const MaxLength = { [RecentType.CatalogTerm]: 1, [RecentType.GradesPage]: 1, [RecentType.EnrollmentPage]: 1, + [RecentType.CourseClick]: 50, }; interface RecentClass { @@ -56,6 +58,12 @@ interface RecentPageUrl { timestamp: number; } +interface RecentCourseClick { + searchTerm: string; + courseNumber: string; + timestamp: number; +} + export type Recent = T extends RecentType.Class ? RecentClass : T extends RecentType.Schedule @@ -68,7 +76,9 @@ export type Recent = T extends RecentType.Class ? RecentPageUrl : T extends RecentType.EnrollmentPage ? RecentPageUrl - : never; + : T extends RecentType.CourseClick + ? RecentCourseClick + : never; export const getRecents = ( type: T, @@ -204,3 +214,15 @@ export const getPageUrl = ( const saved = getRecents(type) as RecentPageUrl[]; return saved[0]?.url ?? null; }; + +export const saveCourseClick = (courseNumber: string, searchTerm: string) => { + addRecent(RecentType.CourseClick, { + searchTerm, + courseNumber, + timestamp: Date.now(), + }); +}; + +export const getCourseClicks = (): RecentCourseClick[] => { + return getRecents(RecentType.CourseClick) as RecentCourseClick[]; +}; \ No newline at end of file From 55905fbd93e355ca5d9e1b01e45b38b7b0f3ca87 Mon Sep 17 00:00:00 2001 From: Leo <77555511+LBF-2004@users.noreply.github.com> Date: Fri, 3 Apr 2026 12:24:36 -0700 Subject: [PATCH 02/15] Format code --- apps/frontend/src/app/Catalog/index.tsx | 5 ++--- .../frontend/src/app/GradTrak/Dashboard/LabelMenu/index.tsx | 2 +- apps/frontend/src/components/ClassBrowser/List/index.tsx | 6 +++--- apps/frontend/src/lib/recent.ts | 4 ++-- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/apps/frontend/src/app/Catalog/index.tsx b/apps/frontend/src/app/Catalog/index.tsx index 4b3b65972..d9c3d2e8b 100644 --- a/apps/frontend/src/app/Catalog/index.tsx +++ b/apps/frontend/src/app/Catalog/index.tsx @@ -29,13 +29,12 @@ import { Semester, } from "@/lib/generated/graphql"; import { RecentType, addRecent, getRecents } from "@/lib/recent"; +import { saveCourseClick } from "@/lib/recent"; import { compareCollectionsByBookmarksOrder } from "@/utils/collections"; import styles from "./Catalog.module.scss"; import useCatalogLayoutMode from "./hooks/useCatalogLayoutMode"; -import {saveCourseClick} from "@/lib/recent"; - // Semester hierarchy for chronological ordering (latest to earliest in year) const SEMESTER_ORDER: Record = { [Semester.Spring]: 0, @@ -514,7 +513,7 @@ export default function Catalog() { searchQuery: string ) => { if (!term) return; - saveCourseClick(searchQuery, `${subject}${courseNumber}`); + saveCourseClick(searchQuery, `${subject}${courseNumber}`); setCatalogDrawerOpen(false); // Close drawer when selecting a class diff --git a/apps/frontend/src/app/GradTrak/Dashboard/LabelMenu/index.tsx b/apps/frontend/src/app/GradTrak/Dashboard/LabelMenu/index.tsx index 42da90636..aec5da795 100644 --- a/apps/frontend/src/app/GradTrak/Dashboard/LabelMenu/index.tsx +++ b/apps/frontend/src/app/GradTrak/Dashboard/LabelMenu/index.tsx @@ -5,9 +5,9 @@ import { Plus, Trash } from "iconoir-react"; import { Button, Color, Dialog, Flex, IconButton, Input } from "@repo/theme"; import ColorSelector from "@/components/ColorSelector"; +import { DeleteScheduleDialog } from "@/components/ScheduleCard/DeleteScheduleDialog"; import { ILabel } from "@/lib/api"; -import { DeleteScheduleDialog } from "@/components/ScheduleCard/DeleteScheduleDialog"; import styles from "./LabelMenu.module.scss"; type LabelMenuProps = { diff --git a/apps/frontend/src/components/ClassBrowser/List/index.tsx b/apps/frontend/src/components/ClassBrowser/List/index.tsx index 8eeb6789a..3d774e74c 100644 --- a/apps/frontend/src/components/ClassBrowser/List/index.tsx +++ b/apps/frontend/src/components/ClassBrowser/List/index.tsx @@ -64,7 +64,7 @@ interface ListProps { courseNumber: string, number: string, sessionId: string, - searchQuery: string, + searchQuery: string ) => void; } @@ -289,7 +289,7 @@ export default function List({ onSelect }: ListProps) { selected.courseNumber, selected.number, selected.sessionId, - query, + query ); }; @@ -341,7 +341,7 @@ export default function List({ onSelect }: ListProps) { recentClass.courseNumber, recentClass.number, recentClass.sessionId, - query, + query ); }} > diff --git a/apps/frontend/src/lib/recent.ts b/apps/frontend/src/lib/recent.ts index b9a290e35..88e0b66b5 100644 --- a/apps/frontend/src/lib/recent.ts +++ b/apps/frontend/src/lib/recent.ts @@ -8,7 +8,7 @@ export enum RecentType { CatalogTerm = "recent-catalog-term", GradesPage = "recent-grades-page", EnrollmentPage = "recent-enrollment-page", - CourseClick = "recent-course-click" + CourseClick = "recent-course-click", } const MaxLength = { @@ -225,4 +225,4 @@ export const saveCourseClick = (courseNumber: string, searchTerm: string) => { export const getCourseClicks = (): RecentCourseClick[] => { return getRecents(RecentType.CourseClick) as RecentCourseClick[]; -}; \ No newline at end of file +}; From b0f9686c1ca665b2c5024e83fcbdc101b7c456a5 Mon Sep 17 00:00:00 2001 From: Leo <77555511+LBF-2004@users.noreply.github.com> Date: Fri, 3 Apr 2026 18:34:02 -0700 Subject: [PATCH 03/15] backend prep --- apps/frontend/src/lib/api/catalog.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/frontend/src/lib/api/catalog.ts b/apps/frontend/src/lib/api/catalog.ts index 93edc5f8e..543061e1b 100644 --- a/apps/frontend/src/lib/api/catalog.ts +++ b/apps/frontend/src/lib/api/catalog.ts @@ -54,6 +54,7 @@ export const GET_CATALOG_SEARCH = gql` decal { title } + searchScore } totalCount } From 09a709801621872e733f3c2d2155b840b57ae061 Mon Sep 17 00:00:00 2001 From: Leo <77555511+LBF-2004@users.noreply.github.com> Date: Fri, 3 Apr 2026 18:44:57 -0700 Subject: [PATCH 04/15] backend --- apps/frontend/src/lib/api/catalog.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/frontend/src/lib/api/catalog.ts b/apps/frontend/src/lib/api/catalog.ts index 543061e1b..93edc5f8e 100644 --- a/apps/frontend/src/lib/api/catalog.ts +++ b/apps/frontend/src/lib/api/catalog.ts @@ -54,7 +54,6 @@ export const GET_CATALOG_SEARCH = gql` decal { title } - searchScore } totalCount } From 86b14faad4a5b04bf0b3f8eabba3079bf080851d Mon Sep 17 00:00:00 2001 From: Leo <77555511+LBF-2004@users.noreply.github.com> Date: Tue, 7 Apr 2026 13:44:24 -0700 Subject: [PATCH 05/15] backend handling --- apps/backend/src/modules/catalog/controller.ts | 18 +++++++++++++++++- .../src/modules/catalog/typedefs/catalog.ts | 1 + apps/frontend/src/app/Catalog/index.tsx | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/apps/backend/src/modules/catalog/controller.ts b/apps/backend/src/modules/catalog/controller.ts index 408c212ec..1f4ee4e6b 100644 --- a/apps/backend/src/modules/catalog/controller.ts +++ b/apps/backend/src/modules/catalog/controller.ts @@ -55,6 +55,7 @@ export interface CatalogQueryParams { sortOrder?: string | null; page?: number | null; pageSize?: number | null; + recentCourseNumbers?: string[] | null; } type CatalogFilterCondition = Record; @@ -108,6 +109,7 @@ export const getCatalogSearch = async (params: CatalogQueryParams) => { filters, limit: effectivePageSize, skip, + recentCourseNumbers: params.recentCourseNumbers, }); } @@ -130,6 +132,17 @@ export const getCatalogSearch = async (params: CatalogQueryParams) => { return { results, totalCount }; }; +const rerank = ( + items: ICatalogClassItem[], + recentCourseNumbers?: string[] | null +): ICatalogClassItem[] => { + if (!recentCourseNumbers || recentCourseNumbers.length === 0) return items; + const viewed = new Set(recentCourseNumbers); + const tier1 = items.filter((item) => viewed.has(item.courseNumber)); + const tier2 = items.filter((item) => !viewed.has(item.courseNumber)); + return [...tier1, ...tier2]; +}; + const getCatalogWithSearch = async ({ year, semester, @@ -137,6 +150,7 @@ const getCatalogWithSearch = async ({ filters, limit, skip, + recentCourseNumbers }: { year: number; semester: string; @@ -144,13 +158,15 @@ const getCatalogWithSearch = async ({ filters: CatalogQueryParams["filters"]; limit: number; skip: number; + recentCourseNumbers?: string[] | null; }) => { const index = await getSearchIndex(year, semester); const hits = index.search(searchTerm); const items = hits.map((r) => r.item); const filtered = applyInMemoryFilters(items, filters); const totalCount = filtered.length; - const results = filtered.slice(skip, skip + limit); + const reranked = rerank(filtered, recentCourseNumbers); + const results = reranked.slice(skip, skip + limit); return { results, totalCount }; }; diff --git a/apps/backend/src/modules/catalog/typedefs/catalog.ts b/apps/backend/src/modules/catalog/typedefs/catalog.ts index 67cc1c3f1..db7851283 100644 --- a/apps/backend/src/modules/catalog/typedefs/catalog.ts +++ b/apps/backend/src/modules/catalog/typedefs/catalog.ts @@ -205,6 +205,7 @@ export const catalogTypeDef = gql` sortOrder: SortOrder page: Int pageSize: Int + recentCourseNumbers: [String] ): CatalogResult! @cacheControl(maxAge: 300, scope: PUBLIC) catalogClassIdentities( diff --git a/apps/frontend/src/app/Catalog/index.tsx b/apps/frontend/src/app/Catalog/index.tsx index d9c3d2e8b..4de6565ca 100644 --- a/apps/frontend/src/app/Catalog/index.tsx +++ b/apps/frontend/src/app/Catalog/index.tsx @@ -513,7 +513,7 @@ export default function Catalog() { searchQuery: string ) => { if (!term) return; - saveCourseClick(searchQuery, `${subject}${courseNumber}`); + saveCourseClick(`${subject}${courseNumber}`, searchQuery); setCatalogDrawerOpen(false); // Close drawer when selecting a class From 1aa6972db1c57ff6fe12f730966702edc63a9c6d Mon Sep 17 00:00:00 2001 From: Leo <77555511+LBF-2004@users.noreply.github.com> Date: Tue, 7 Apr 2026 13:45:16 -0700 Subject: [PATCH 06/15] format --- apps/backend/src/modules/catalog/controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/backend/src/modules/catalog/controller.ts b/apps/backend/src/modules/catalog/controller.ts index 1f4ee4e6b..cbf7b8b64 100644 --- a/apps/backend/src/modules/catalog/controller.ts +++ b/apps/backend/src/modules/catalog/controller.ts @@ -150,7 +150,7 @@ const getCatalogWithSearch = async ({ filters, limit, skip, - recentCourseNumbers + recentCourseNumbers, }: { year: number; semester: string; From 55ddf05cbfa0ac8b8729a912deebab82a7ee0fbf Mon Sep 17 00:00:00 2001 From: Leo <77555511+LBF-2004@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:35:10 -0700 Subject: [PATCH 07/15] fine tuning now --- .../backend/src/modules/catalog/controller.ts | 72 +++++++++++++++---- .../src/modules/catalog/typedefs/catalog.ts | 9 ++- .../ClassBrowser/hooks/useCatalogQuery.ts | 4 +- apps/frontend/src/lib/api/catalog.ts | 2 + packages/gql-typedefs/catalog.ts | 8 +++ 5 files changed, 81 insertions(+), 14 deletions(-) diff --git a/apps/backend/src/modules/catalog/controller.ts b/apps/backend/src/modules/catalog/controller.ts index ed8bea9e2..1a063709f 100644 --- a/apps/backend/src/modules/catalog/controller.ts +++ b/apps/backend/src/modules/catalog/controller.ts @@ -57,7 +57,7 @@ export interface CatalogQueryParams { page?: number | null; pageSize?: number | null; semanticSearch?: boolean | null; - recentCourseNumbers?: string[] | null; + recentClicks?: { courseNumber: string; timestamp: number; searchTerm: string }[] | null; } type CatalogFilterCondition = Record; @@ -124,7 +124,7 @@ export const getCatalogSearch = async (params: CatalogQueryParams) => { filters, limit: effectivePageSize, skip, - recentCourseNumbers: params.recentCourseNumbers, + recentClicks: params.recentClicks, }); } @@ -199,15 +199,36 @@ const getCatalogWithSemanticSearch = async ({ return { results, totalCount }; }; -const rerank = ( +const HALF_LIFE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days +const MIN_WEIGHT = 0.1; +const MAX_BOOST = 0.6; + +const decayWeight = (timestamp: number): number => + Math.exp((-Math.LN2 * (Date.now() - timestamp)) / HALF_LIFE_MS); + +// Given fuzzy search returns, assign a score to each item and sort by decay +const scoreAndSort = ( items: ICatalogClassItem[], - recentCourseNumbers?: string[] | null + scoreMap: Map, + recentClicks?: { courseNumber: string; timestamp: number, searchTerm: string }[] | null ): ICatalogClassItem[] => { - if (!recentCourseNumbers || recentCourseNumbers.length === 0) return items; - const viewed = new Set(recentCourseNumbers); - const tier1 = items.filter((item) => viewed.has(item.courseNumber)); - const tier2 = items.filter((item) => !viewed.has(item.courseNumber)); - return [...tier1, ...tier2]; + const clickWeightMap = new Map(); + for (const click of recentClicks ?? []) { + const w = decayWeight(click.timestamp); + if (w >= MIN_WEIGHT) { + const existing = clickWeightMap.get(click.searchTerm) ?? 0; + clickWeightMap.set(click.searchTerm, Math.max(existing, w)); + } + } + + return [...items].sort((a, b) => { + const fuzzyA = scoreMap.get(a) ?? 1; + const fuzzyB = scoreMap.get(b) ?? 1; + const key = (item: ICatalogClassItem) => `${item.subject}${item.courseNumber}`; + const scoreA = fuzzyA - (clickWeightMap.get(key(a)) ?? 0) * MAX_BOOST; + const scoreB = fuzzyB - (clickWeightMap.get(key(b)) ?? 0) * MAX_BOOST; + return scoreA - scoreB; + }); }; const getCatalogWithSearch = async ({ @@ -217,7 +238,7 @@ const getCatalogWithSearch = async ({ filters, limit, skip, - recentCourseNumbers, + recentClicks, }: { year: number; semester: string; @@ -225,14 +246,41 @@ const getCatalogWithSearch = async ({ filters: CatalogQueryParams["filters"]; limit: number; skip: number; - recentCourseNumbers?: string[] | null; + recentClicks?: { courseNumber: string; timestamp: number, searchTerm: string }[] | null; }) => { const index = await getSearchIndex(year, semester); const hits = index.search(searchTerm); + const scoreMap = new Map(); + for (const hit of hits) scoreMap.set(hit.item, hit.score ?? 1); const items = hits.map((r) => r.item); + const coveredIds = new Set(items.map((item) => `${item.subject}${item.courseNumber}`)); + // Check if there are any recently clicked class missing from fuzzy search + const lowerSearch = searchTerm.toLowerCase(); + const missingClicks = (recentClicks ?? []).filter((click) => { + const w = decayWeight(click.timestamp); + return ( + w >= MIN_WEIGHT && + click.courseNumber.toLowerCase().startsWith(lowerSearch) && + !coveredIds.has(click.searchTerm) + ); + }); + // do a full catalog search and bring back the missing ones, assign scores + if (missingClicks.length > 0) { + const allCatalog = await getCachedCatalog(year, semester); + const catalogById = new Map(allCatalog.map((item) => [`${item.subject}${item.courseNumber}`, item])); + + for (const click of missingClicks) { + const item = catalogById.get(click.searchTerm); + if (!item) continue; + scoreMap.set(item, 0.6); + items.push(item); + } +} + + const filtered = applyInMemoryFilters(items, filters); const totalCount = filtered.length; - const reranked = rerank(filtered, recentCourseNumbers); + const reranked = scoreAndSort(filtered, scoreMap, recentClicks); const results = reranked.slice(skip, skip + limit); return { results, totalCount }; diff --git a/apps/backend/src/modules/catalog/typedefs/catalog.ts b/apps/backend/src/modules/catalog/typedefs/catalog.ts index db7851283..14224f039 100644 --- a/apps/backend/src/modules/catalog/typedefs/catalog.ts +++ b/apps/backend/src/modules/catalog/typedefs/catalog.ts @@ -34,6 +34,12 @@ export const catalogTypeDef = gql` online: Boolean } + input RecentClick { + courseNumber: String! + searchTerm: String! + timestamp: Float! + } + type CatalogMeeting { days: [Boolean!] startTime: String @@ -205,7 +211,8 @@ export const catalogTypeDef = gql` sortOrder: SortOrder page: Int pageSize: Int - recentCourseNumbers: [String] + recentClicks: [RecentClick] + ): CatalogResult! @cacheControl(maxAge: 300, scope: PUBLIC) catalogClassIdentities( diff --git a/apps/frontend/src/components/ClassBrowser/hooks/useCatalogQuery.ts b/apps/frontend/src/components/ClassBrowser/hooks/useCatalogQuery.ts index c8e8b3894..efc6c876b 100644 --- a/apps/frontend/src/components/ClassBrowser/hooks/useCatalogQuery.ts +++ b/apps/frontend/src/components/ClassBrowser/hooks/useCatalogQuery.ts @@ -1,6 +1,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useQuery } from "@apollo/client/react"; +import {getCourseClicks} from "@/lib/recent"; import type { ICatalogClassServer, @@ -125,6 +126,7 @@ export default function useCatalogQuery({ sortBy: debouncedQuery ? undefined : mapSortBy(sortBy), sortOrder: debouncedQuery ? undefined : mapSortOrder(effectiveOrder), semanticSearch: semanticSearch || undefined, + recentClicks: debouncedQuery ? getCourseClicks() : undefined, }), [ currentYear, @@ -215,7 +217,7 @@ export default function useCatalogQuery({ const isFirstPageLoading = loading && localPage === 1 && !isLoadingNextPage; const semanticError = semanticSearch && error - ? (error.graphQLErrors?.[0]?.message ?? error.message ?? "AI search failed") + ? (error.message ?? "AI search failed") // pre-existing error, not relatd to this pr : null; return { diff --git a/apps/frontend/src/lib/api/catalog.ts b/apps/frontend/src/lib/api/catalog.ts index 8cf0da05f..265037a15 100644 --- a/apps/frontend/src/lib/api/catalog.ts +++ b/apps/frontend/src/lib/api/catalog.ts @@ -17,6 +17,7 @@ export const GET_CATALOG_SEARCH = gql` $page: Int $pageSize: Int $semanticSearch: Boolean + $recentClicks: [RecentClick] ) { catalogSearch( year: $year @@ -28,6 +29,7 @@ export const GET_CATALOG_SEARCH = gql` page: $page pageSize: $pageSize semanticSearch: $semanticSearch + recentClicks: $recentClicks ) { results { year diff --git a/packages/gql-typedefs/catalog.ts b/packages/gql-typedefs/catalog.ts index c06f44632..baf463004 100644 --- a/packages/gql-typedefs/catalog.ts +++ b/packages/gql-typedefs/catalog.ts @@ -34,6 +34,13 @@ export const catalogTypeDef = gql` online: Boolean } + input RecentClick { + courseNumber: String! + searchTerm: String! + timestamp: Float! + } + + type CatalogMeeting { days: [Boolean!] startTime: String @@ -213,6 +220,7 @@ export const catalogTypeDef = gql` page: Int pageSize: Int semanticSearch: Boolean + recentClicks: [RecentClick] ): CatalogResult! catalogClassIdentities( From 08656b4f005206230a36f3c355dadc7a8bc31daa Mon Sep 17 00:00:00 2001 From: Leo <77555511+LBF-2004@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:35:46 -0700 Subject: [PATCH 08/15] format --- .../backend/src/modules/catalog/controller.ts | 38 ++++++++++++------- .../src/modules/catalog/typedefs/catalog.ts | 1 - .../ClassBrowser/hooks/useCatalogQuery.ts | 7 +++- packages/gql-typedefs/catalog.ts | 1 - 4 files changed, 29 insertions(+), 18 deletions(-) diff --git a/apps/backend/src/modules/catalog/controller.ts b/apps/backend/src/modules/catalog/controller.ts index 1a063709f..81479efea 100644 --- a/apps/backend/src/modules/catalog/controller.ts +++ b/apps/backend/src/modules/catalog/controller.ts @@ -25,13 +25,13 @@ import { } from "@repo/common/models"; import { getFields, hasFieldPath } from "../../utils/graphql"; -import { searchSemantic } from "../semantic-search/client"; import { formatClass, formatSection } from "../class/formatter"; import type { ClassModule } from "../class/generated-types/module-types"; import { formatCourse } from "../course/formatter"; import { formatEnrollment } from "../enrollment/formatter"; import type { EnrollmentModule } from "../enrollment/generated-types/module-types"; import type { GradeDistributionModule } from "../grade-distribution/generated-types/module-types"; +import { searchSemantic } from "../semantic-search/client"; import { getCachedCatalog, getSearchIndex } from "./catalog-cache"; export interface CatalogQueryParams { @@ -57,7 +57,9 @@ export interface CatalogQueryParams { page?: number | null; pageSize?: number | null; semanticSearch?: boolean | null; - recentClicks?: { courseNumber: string; timestamp: number; searchTerm: string }[] | null; + recentClicks?: + | { courseNumber: string; timestamp: number; searchTerm: string }[] + | null; } type CatalogFilterCondition = Record; @@ -210,7 +212,9 @@ const decayWeight = (timestamp: number): number => const scoreAndSort = ( items: ICatalogClassItem[], scoreMap: Map, - recentClicks?: { courseNumber: string; timestamp: number, searchTerm: string }[] | null + recentClicks?: + | { courseNumber: string; timestamp: number; searchTerm: string }[] + | null ): ICatalogClassItem[] => { const clickWeightMap = new Map(); for (const click of recentClicks ?? []) { @@ -224,7 +228,8 @@ const scoreAndSort = ( return [...items].sort((a, b) => { const fuzzyA = scoreMap.get(a) ?? 1; const fuzzyB = scoreMap.get(b) ?? 1; - const key = (item: ICatalogClassItem) => `${item.subject}${item.courseNumber}`; + const key = (item: ICatalogClassItem) => + `${item.subject}${item.courseNumber}`; const scoreA = fuzzyA - (clickWeightMap.get(key(a)) ?? 0) * MAX_BOOST; const scoreB = fuzzyB - (clickWeightMap.get(key(b)) ?? 0) * MAX_BOOST; return scoreA - scoreB; @@ -246,28 +251,34 @@ const getCatalogWithSearch = async ({ filters: CatalogQueryParams["filters"]; limit: number; skip: number; - recentClicks?: { courseNumber: string; timestamp: number, searchTerm: string }[] | null; + recentClicks?: + | { courseNumber: string; timestamp: number; searchTerm: string }[] + | null; }) => { const index = await getSearchIndex(year, semester); const hits = index.search(searchTerm); const scoreMap = new Map(); for (const hit of hits) scoreMap.set(hit.item, hit.score ?? 1); const items = hits.map((r) => r.item); - const coveredIds = new Set(items.map((item) => `${item.subject}${item.courseNumber}`)); + const coveredIds = new Set( + items.map((item) => `${item.subject}${item.courseNumber}`) + ); // Check if there are any recently clicked class missing from fuzzy search const lowerSearch = searchTerm.toLowerCase(); const missingClicks = (recentClicks ?? []).filter((click) => { - const w = decayWeight(click.timestamp); - return ( - w >= MIN_WEIGHT && - click.courseNumber.toLowerCase().startsWith(lowerSearch) && - !coveredIds.has(click.searchTerm) + const w = decayWeight(click.timestamp); + return ( + w >= MIN_WEIGHT && + click.courseNumber.toLowerCase().startsWith(lowerSearch) && + !coveredIds.has(click.searchTerm) ); }); // do a full catalog search and bring back the missing ones, assign scores if (missingClicks.length > 0) { const allCatalog = await getCachedCatalog(year, semester); - const catalogById = new Map(allCatalog.map((item) => [`${item.subject}${item.courseNumber}`, item])); + const catalogById = new Map( + allCatalog.map((item) => [`${item.subject}${item.courseNumber}`, item]) + ); for (const click of missingClicks) { const item = catalogById.get(click.searchTerm); @@ -275,8 +286,7 @@ const getCatalogWithSearch = async ({ scoreMap.set(item, 0.6); items.push(item); } -} - + } const filtered = applyInMemoryFilters(items, filters); const totalCount = filtered.length; diff --git a/apps/backend/src/modules/catalog/typedefs/catalog.ts b/apps/backend/src/modules/catalog/typedefs/catalog.ts index 14224f039..8b80fbb9b 100644 --- a/apps/backend/src/modules/catalog/typedefs/catalog.ts +++ b/apps/backend/src/modules/catalog/typedefs/catalog.ts @@ -212,7 +212,6 @@ export const catalogTypeDef = gql` page: Int pageSize: Int recentClicks: [RecentClick] - ): CatalogResult! @cacheControl(maxAge: 300, scope: PUBLIC) catalogClassIdentities( diff --git a/apps/frontend/src/components/ClassBrowser/hooks/useCatalogQuery.ts b/apps/frontend/src/components/ClassBrowser/hooks/useCatalogQuery.ts index efc6c876b..2f81c230c 100644 --- a/apps/frontend/src/components/ClassBrowser/hooks/useCatalogQuery.ts +++ b/apps/frontend/src/components/ClassBrowser/hooks/useCatalogQuery.ts @@ -1,7 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useQuery } from "@apollo/client/react"; -import {getCourseClicks} from "@/lib/recent"; import type { ICatalogClassServer, @@ -15,6 +14,7 @@ import type { GetCatalogSearchQueryVariables, Semester, } from "@/lib/generated/graphql"; +import { getCourseClicks } from "@/lib/recent"; import { SortBy } from "../browser"; import { mapSortBy } from "./useCatalogFilters"; @@ -140,7 +140,10 @@ export default function useCatalogQuery({ ); // Server-side catalog query (always requests first page) - const { data, loading, error, fetchMore } = useQuery(GET_CATALOG_SEARCH, { + const { data, loading, error, fetchMore } = useQuery< + GetCatalogSearchQuery, + GetCatalogSearchQueryVariables + >(GET_CATALOG_SEARCH, { variables: { ...catalogQueryVariables, page: 1, diff --git a/packages/gql-typedefs/catalog.ts b/packages/gql-typedefs/catalog.ts index baf463004..d6937a01d 100644 --- a/packages/gql-typedefs/catalog.ts +++ b/packages/gql-typedefs/catalog.ts @@ -40,7 +40,6 @@ export const catalogTypeDef = gql` timestamp: Float! } - type CatalogMeeting { days: [Boolean!] startTime: String From 5b0c06bdacbaf60b59bbe3dce9118a14abaa94bc Mon Sep 17 00:00:00 2001 From: Leo <77555511+LBF-2004@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:43:27 -0700 Subject: [PATCH 09/15] prefix matching --- apps/backend/src/modules/catalog/controller.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/backend/src/modules/catalog/controller.ts b/apps/backend/src/modules/catalog/controller.ts index 81479efea..fc1f0217a 100644 --- a/apps/backend/src/modules/catalog/controller.ts +++ b/apps/backend/src/modules/catalog/controller.ts @@ -220,8 +220,8 @@ const scoreAndSort = ( for (const click of recentClicks ?? []) { const w = decayWeight(click.timestamp); if (w >= MIN_WEIGHT) { - const existing = clickWeightMap.get(click.searchTerm) ?? 0; - clickWeightMap.set(click.searchTerm, Math.max(existing, w)); + const existing = clickWeightMap.get(click.courseNumber) ?? 0; + clickWeightMap.set(click.courseNumber, Math.max(existing, w)); } } @@ -269,8 +269,8 @@ const getCatalogWithSearch = async ({ const w = decayWeight(click.timestamp); return ( w >= MIN_WEIGHT && - click.courseNumber.toLowerCase().startsWith(lowerSearch) && - !coveredIds.has(click.searchTerm) + click.searchTerm.toLowerCase().startsWith(lowerSearch) && + !coveredIds.has(click.courseNumber) ); }); // do a full catalog search and bring back the missing ones, assign scores @@ -281,7 +281,7 @@ const getCatalogWithSearch = async ({ ); for (const click of missingClicks) { - const item = catalogById.get(click.searchTerm); + const item = catalogById.get(click.courseNumber); if (!item) continue; scoreMap.set(item, 0.6); items.push(item); From 90360291aefa3174984ef2cfeffd5026d7d77936 Mon Sep 17 00:00:00 2001 From: Leo <77555511+LBF-2004@users.noreply.github.com> Date: Thu, 23 Apr 2026 15:32:28 -0700 Subject: [PATCH 10/15] caching --- apps/backend/src/modules/catalog/resolver.ts | 6 +++++- .../components/ClassBrowser/hooks/useCatalogQuery.ts | 10 +++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/apps/backend/src/modules/catalog/resolver.ts b/apps/backend/src/modules/catalog/resolver.ts index d24608ca7..2017cd9e5 100644 --- a/apps/backend/src/modules/catalog/resolver.ts +++ b/apps/backend/src/modules/catalog/resolver.ts @@ -18,7 +18,10 @@ const resolvers = { ) => { return await getCatalogLegacy(year, semester, info); }, - catalogSearch: async (_: unknown, args: CatalogQueryParams) => { + catalogSearch: async (_: unknown, args: CatalogQueryParams, __: unknown, info: GraphQLResolveInfo) => { + if (args.recentClicks && args.recentClicks.length > 0) { + (info as any).cacheControl.setCacheHint({maxAge: 0, scope: "PRIVATE" }); + } return await getCatalogSearch(args); }, catalogClassIdentities: async ( @@ -33,6 +36,7 @@ const resolvers = { ) => { return await getCatalogFilterOptions(year, semester); }, + }, }; diff --git a/apps/frontend/src/components/ClassBrowser/hooks/useCatalogQuery.ts b/apps/frontend/src/components/ClassBrowser/hooks/useCatalogQuery.ts index 2f81c230c..e21d65450 100644 --- a/apps/frontend/src/components/ClassBrowser/hooks/useCatalogQuery.ts +++ b/apps/frontend/src/components/ClassBrowser/hooks/useCatalogQuery.ts @@ -87,6 +87,13 @@ export default function useCatalogQuery({ const isLoadingNextPageRef = useRef(false); const queryGenerationRef = useRef(0); + const [recentClicksSnapshot, setRecentClicksSnapshot] = useState(getCourseClicks); + useEffect(() => { + const refresh = () => setRecentClicksSnapshot(getCourseClicks()); + window.addEventListener("focus", refresh); + return () => window.removeEventListener("focus", refresh); + }, []); + // In semantic mode the query is committed externally — no debounce needed. // In normal mode, debounce to avoid firing on every keystroke. const [debouncedQuery, setDebouncedQuery] = useState(rawQuery); @@ -126,7 +133,7 @@ export default function useCatalogQuery({ sortBy: debouncedQuery ? undefined : mapSortBy(sortBy), sortOrder: debouncedQuery ? undefined : mapSortOrder(effectiveOrder), semanticSearch: semanticSearch || undefined, - recentClicks: debouncedQuery ? getCourseClicks() : undefined, + recentClicks: debouncedQuery ? recentClicksSnapshot : undefined, }), [ currentYear, @@ -136,6 +143,7 @@ export default function useCatalogQuery({ sortBy, effectiveOrder, semanticSearch, + recentClicksSnapshot, ] ); From 49a1e77691fc944daaba0cb7e6d0772fff9c1be0 Mon Sep 17 00:00:00 2001 From: Leo <77555511+LBF-2004@users.noreply.github.com> Date: Thu, 23 Apr 2026 15:39:57 -0700 Subject: [PATCH 11/15] caching --- apps/backend/src/modules/catalog/resolver.ts | 13 ++++++++++--- .../ClassBrowser/hooks/useCatalogQuery.ts | 3 ++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/apps/backend/src/modules/catalog/resolver.ts b/apps/backend/src/modules/catalog/resolver.ts index 2017cd9e5..9e0dcac7c 100644 --- a/apps/backend/src/modules/catalog/resolver.ts +++ b/apps/backend/src/modules/catalog/resolver.ts @@ -18,9 +18,17 @@ const resolvers = { ) => { return await getCatalogLegacy(year, semester, info); }, - catalogSearch: async (_: unknown, args: CatalogQueryParams, __: unknown, info: GraphQLResolveInfo) => { + catalogSearch: async ( + _: unknown, + args: CatalogQueryParams, + __: unknown, + info: GraphQLResolveInfo + ) => { if (args.recentClicks && args.recentClicks.length > 0) { - (info as any).cacheControl.setCacheHint({maxAge: 0, scope: "PRIVATE" }); + (info as any).cacheControl.setCacheHint({ + maxAge: 0, + scope: "PRIVATE", + }); } return await getCatalogSearch(args); }, @@ -36,7 +44,6 @@ const resolvers = { ) => { return await getCatalogFilterOptions(year, semester); }, - }, }; diff --git a/apps/frontend/src/components/ClassBrowser/hooks/useCatalogQuery.ts b/apps/frontend/src/components/ClassBrowser/hooks/useCatalogQuery.ts index e21d65450..3c2682c46 100644 --- a/apps/frontend/src/components/ClassBrowser/hooks/useCatalogQuery.ts +++ b/apps/frontend/src/components/ClassBrowser/hooks/useCatalogQuery.ts @@ -87,7 +87,8 @@ export default function useCatalogQuery({ const isLoadingNextPageRef = useRef(false); const queryGenerationRef = useRef(0); - const [recentClicksSnapshot, setRecentClicksSnapshot] = useState(getCourseClicks); + const [recentClicksSnapshot, setRecentClicksSnapshot] = + useState(getCourseClicks); useEffect(() => { const refresh = () => setRecentClicksSnapshot(getCourseClicks()); window.addEventListener("focus", refresh); From 9ea889c79df05ca3a7f1f1269e779bc75a1bf31d Mon Sep 17 00:00:00 2001 From: Leo <77555511+LBF-2004@users.noreply.github.com> Date: Thu, 23 Apr 2026 15:43:17 -0700 Subject: [PATCH 12/15] caching --- apps/backend/src/modules/catalog/resolver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/backend/src/modules/catalog/resolver.ts b/apps/backend/src/modules/catalog/resolver.ts index 9e0dcac7c..ece3f0385 100644 --- a/apps/backend/src/modules/catalog/resolver.ts +++ b/apps/backend/src/modules/catalog/resolver.ts @@ -25,7 +25,7 @@ const resolvers = { info: GraphQLResolveInfo ) => { if (args.recentClicks && args.recentClicks.length > 0) { - (info as any).cacheControl.setCacheHint({ + (info as GraphQLResolveInfo & { cacheControl: { setCacheHint: (hint: { maxAge: number; scope: string }) => void } }).cacheControl.setCacheHint({ maxAge: 0, scope: "PRIVATE", }); From 2239f0960c56f4326e10c7bb044eb4ffd0896637 Mon Sep 17 00:00:00 2001 From: Leo <77555511+LBF-2004@users.noreply.github.com> Date: Thu, 23 Apr 2026 15:44:50 -0700 Subject: [PATCH 13/15] wtf is formating --- apps/backend/src/modules/catalog/resolver.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/backend/src/modules/catalog/resolver.ts b/apps/backend/src/modules/catalog/resolver.ts index ece3f0385..e14a8de2f 100644 --- a/apps/backend/src/modules/catalog/resolver.ts +++ b/apps/backend/src/modules/catalog/resolver.ts @@ -25,7 +25,13 @@ const resolvers = { info: GraphQLResolveInfo ) => { if (args.recentClicks && args.recentClicks.length > 0) { - (info as GraphQLResolveInfo & { cacheControl: { setCacheHint: (hint: { maxAge: number; scope: string }) => void } }).cacheControl.setCacheHint({ + ( + info as GraphQLResolveInfo & { + cacheControl: { + setCacheHint: (hint: { maxAge: number; scope: string }) => void; + }; + } + ).cacheControl.setCacheHint({ maxAge: 0, scope: "PRIVATE", }); From adcc344c36fe74396fcc8e84e55ae882e62bc81e Mon Sep 17 00:00:00 2001 From: Leo <77555511+LBF-2004@users.noreply.github.com> Date: Thu, 23 Apr 2026 17:05:29 -0700 Subject: [PATCH 14/15] reverse what i did --- .../backend/src/modules/catalog/controller.ts | 72 ++++--------------- apps/backend/src/modules/catalog/resolver.ts | 19 +---- .../src/modules/catalog/typedefs/catalog.ts | 9 +-- .../ClassBrowser/hooks/useCatalogQuery.ts | 12 ---- 4 files changed, 15 insertions(+), 97 deletions(-) diff --git a/apps/backend/src/modules/catalog/controller.ts b/apps/backend/src/modules/catalog/controller.ts index fc1f0217a..8d482db87 100644 --- a/apps/backend/src/modules/catalog/controller.ts +++ b/apps/backend/src/modules/catalog/controller.ts @@ -57,9 +57,6 @@ export interface CatalogQueryParams { page?: number | null; pageSize?: number | null; semanticSearch?: boolean | null; - recentClicks?: - | { courseNumber: string; timestamp: number; searchTerm: string }[] - | null; } type CatalogFilterCondition = Record; @@ -126,7 +123,6 @@ export const getCatalogSearch = async (params: CatalogQueryParams) => { filters, limit: effectivePageSize, skip, - recentClicks: params.recentClicks, }); } @@ -201,37 +197,26 @@ const getCatalogWithSemanticSearch = async ({ return { results, totalCount }; }; -const HALF_LIFE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days -const MIN_WEIGHT = 0.1; -const MAX_BOOST = 0.6; +const VIEWCOUNT_BOOST = 0.05; -const decayWeight = (timestamp: number): number => - Math.exp((-Math.LN2 * (Date.now() - timestamp)) / HALF_LIFE_MS); - -// Given fuzzy search returns, assign a score to each item and sort by decay +// Given fuzzy search returns, assign a score to each item and sort by viewCount const scoreAndSort = ( items: ICatalogClassItem[], - scoreMap: Map, - recentClicks?: - | { courseNumber: string; timestamp: number; searchTerm: string }[] - | null + scoreMap: Map ): ICatalogClassItem[] => { - const clickWeightMap = new Map(); - for (const click of recentClicks ?? []) { - const w = decayWeight(click.timestamp); - if (w >= MIN_WEIGHT) { - const existing = clickWeightMap.get(click.courseNumber) ?? 0; - clickWeightMap.set(click.courseNumber, Math.max(existing, w)); - } - } + const maxViewCount = Math.max(...items.map((i) => i.viewCount ?? 0), 1); return [...items].sort((a, b) => { const fuzzyA = scoreMap.get(a) ?? 1; const fuzzyB = scoreMap.get(b) ?? 1; - const key = (item: ICatalogClassItem) => - `${item.subject}${item.courseNumber}`; - const scoreA = fuzzyA - (clickWeightMap.get(key(a)) ?? 0) * MAX_BOOST; - const scoreB = fuzzyB - (clickWeightMap.get(key(b)) ?? 0) * MAX_BOOST; + const viewA = + (Math.log((a.viewCount ?? 0) + 1) / Math.log(maxViewCount + 1)) * + VIEWCOUNT_BOOST; + const viewB = + (Math.log((b.viewCount ?? 0) + 1) / Math.log(maxViewCount + 1)) * + VIEWCOUNT_BOOST; + const scoreA = fuzzyA - viewA; + const scoreB = fuzzyB - viewB; return scoreA - scoreB; }); }; @@ -243,7 +228,6 @@ const getCatalogWithSearch = async ({ filters, limit, skip, - recentClicks, }: { year: number; semester: string; @@ -251,46 +235,16 @@ const getCatalogWithSearch = async ({ filters: CatalogQueryParams["filters"]; limit: number; skip: number; - recentClicks?: - | { courseNumber: string; timestamp: number; searchTerm: string }[] - | null; }) => { const index = await getSearchIndex(year, semester); const hits = index.search(searchTerm); const scoreMap = new Map(); for (const hit of hits) scoreMap.set(hit.item, hit.score ?? 1); const items = hits.map((r) => r.item); - const coveredIds = new Set( - items.map((item) => `${item.subject}${item.courseNumber}`) - ); - // Check if there are any recently clicked class missing from fuzzy search - const lowerSearch = searchTerm.toLowerCase(); - const missingClicks = (recentClicks ?? []).filter((click) => { - const w = decayWeight(click.timestamp); - return ( - w >= MIN_WEIGHT && - click.searchTerm.toLowerCase().startsWith(lowerSearch) && - !coveredIds.has(click.courseNumber) - ); - }); - // do a full catalog search and bring back the missing ones, assign scores - if (missingClicks.length > 0) { - const allCatalog = await getCachedCatalog(year, semester); - const catalogById = new Map( - allCatalog.map((item) => [`${item.subject}${item.courseNumber}`, item]) - ); - - for (const click of missingClicks) { - const item = catalogById.get(click.courseNumber); - if (!item) continue; - scoreMap.set(item, 0.6); - items.push(item); - } - } const filtered = applyInMemoryFilters(items, filters); const totalCount = filtered.length; - const reranked = scoreAndSort(filtered, scoreMap, recentClicks); + const reranked = scoreAndSort(filtered, scoreMap); const results = reranked.slice(skip, skip + limit); return { results, totalCount }; diff --git a/apps/backend/src/modules/catalog/resolver.ts b/apps/backend/src/modules/catalog/resolver.ts index e14a8de2f..d24608ca7 100644 --- a/apps/backend/src/modules/catalog/resolver.ts +++ b/apps/backend/src/modules/catalog/resolver.ts @@ -18,24 +18,7 @@ const resolvers = { ) => { return await getCatalogLegacy(year, semester, info); }, - catalogSearch: async ( - _: unknown, - args: CatalogQueryParams, - __: unknown, - info: GraphQLResolveInfo - ) => { - if (args.recentClicks && args.recentClicks.length > 0) { - ( - info as GraphQLResolveInfo & { - cacheControl: { - setCacheHint: (hint: { maxAge: number; scope: string }) => void; - }; - } - ).cacheControl.setCacheHint({ - maxAge: 0, - scope: "PRIVATE", - }); - } + catalogSearch: async (_: unknown, args: CatalogQueryParams) => { return await getCatalogSearch(args); }, catalogClassIdentities: async ( diff --git a/apps/backend/src/modules/catalog/typedefs/catalog.ts b/apps/backend/src/modules/catalog/typedefs/catalog.ts index 8b80fbb9b..bb4051298 100644 --- a/apps/backend/src/modules/catalog/typedefs/catalog.ts +++ b/apps/backend/src/modules/catalog/typedefs/catalog.ts @@ -34,13 +34,7 @@ export const catalogTypeDef = gql` online: Boolean } - input RecentClick { - courseNumber: String! - searchTerm: String! - timestamp: Float! - } - - type CatalogMeeting { +type CatalogMeeting { days: [Boolean!] startTime: String endTime: String @@ -211,7 +205,6 @@ export const catalogTypeDef = gql` sortOrder: SortOrder page: Int pageSize: Int - recentClicks: [RecentClick] ): CatalogResult! @cacheControl(maxAge: 300, scope: PUBLIC) catalogClassIdentities( diff --git a/apps/frontend/src/components/ClassBrowser/hooks/useCatalogQuery.ts b/apps/frontend/src/components/ClassBrowser/hooks/useCatalogQuery.ts index 3c2682c46..358d94e49 100644 --- a/apps/frontend/src/components/ClassBrowser/hooks/useCatalogQuery.ts +++ b/apps/frontend/src/components/ClassBrowser/hooks/useCatalogQuery.ts @@ -14,8 +14,6 @@ import type { GetCatalogSearchQueryVariables, Semester, } from "@/lib/generated/graphql"; -import { getCourseClicks } from "@/lib/recent"; - import { SortBy } from "../browser"; import { mapSortBy } from "./useCatalogFilters"; @@ -87,14 +85,6 @@ export default function useCatalogQuery({ const isLoadingNextPageRef = useRef(false); const queryGenerationRef = useRef(0); - const [recentClicksSnapshot, setRecentClicksSnapshot] = - useState(getCourseClicks); - useEffect(() => { - const refresh = () => setRecentClicksSnapshot(getCourseClicks()); - window.addEventListener("focus", refresh); - return () => window.removeEventListener("focus", refresh); - }, []); - // In semantic mode the query is committed externally — no debounce needed. // In normal mode, debounce to avoid firing on every keystroke. const [debouncedQuery, setDebouncedQuery] = useState(rawQuery); @@ -134,7 +124,6 @@ export default function useCatalogQuery({ sortBy: debouncedQuery ? undefined : mapSortBy(sortBy), sortOrder: debouncedQuery ? undefined : mapSortOrder(effectiveOrder), semanticSearch: semanticSearch || undefined, - recentClicks: debouncedQuery ? recentClicksSnapshot : undefined, }), [ currentYear, @@ -144,7 +133,6 @@ export default function useCatalogQuery({ sortBy, effectiveOrder, semanticSearch, - recentClicksSnapshot, ] ); From 9552a87c879427b8c5bf3705b4d94b2a45496a94 Mon Sep 17 00:00:00 2001 From: Leo <77555511+LBF-2004@users.noreply.github.com> Date: Thu, 23 Apr 2026 17:06:54 -0700 Subject: [PATCH 15/15] format --- apps/backend/src/modules/catalog/typedefs/catalog.ts | 2 +- .../src/components/ClassBrowser/hooks/useCatalogQuery.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/backend/src/modules/catalog/typedefs/catalog.ts b/apps/backend/src/modules/catalog/typedefs/catalog.ts index bb4051298..67cc1c3f1 100644 --- a/apps/backend/src/modules/catalog/typedefs/catalog.ts +++ b/apps/backend/src/modules/catalog/typedefs/catalog.ts @@ -34,7 +34,7 @@ export const catalogTypeDef = gql` online: Boolean } -type CatalogMeeting { + type CatalogMeeting { days: [Boolean!] startTime: String endTime: String diff --git a/apps/frontend/src/components/ClassBrowser/hooks/useCatalogQuery.ts b/apps/frontend/src/components/ClassBrowser/hooks/useCatalogQuery.ts index 358d94e49..2a286f739 100644 --- a/apps/frontend/src/components/ClassBrowser/hooks/useCatalogQuery.ts +++ b/apps/frontend/src/components/ClassBrowser/hooks/useCatalogQuery.ts @@ -14,6 +14,7 @@ import type { GetCatalogSearchQueryVariables, Semester, } from "@/lib/generated/graphql"; + import { SortBy } from "../browser"; import { mapSortBy } from "./useCatalogFilters";