Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 32 additions & 24 deletions apps/antalmanac/src/backend/routers/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { z } from 'zod';

import { procedure, router } from '../trpc';

// eslint-disable-next-line import/no-unresolved
import * as searchData from '$generated/searchData';

const MAX_AUTOCOMPLETE_RESULTS = 12;
Expand Down Expand Up @@ -39,14 +40,14 @@ const toGESearchResult = (key: GECategoryKey): [string, SearchResult] => [
const toMutable = <T>(arr: readonly T[]): T[] => arr as T[];

const isCourseOffered = (
department: string,
courseNumber: string,
termSectionCodes: Record<string, SectionSearchResult>): boolean => {
return Object.values(termSectionCodes).some(
(section) => {
return section.department === department && section.courseNumber === courseNumber;
})
}
department: string,
courseNumber: string,
termSectionCodes: Record<string, SectionSearchResult>
): boolean => {
return Object.values(termSectionCodes).some((section) => {
return section.department === department && section.courseNumber === courseNumber;
});
};

const searchRouter = router({
doSearch: procedure
Expand Down Expand Up @@ -94,25 +95,32 @@ const searchRouter = router({
keys: ['id', 'alias'],
limit: MAX_AUTOCOMPLETE_RESULTS - matchedSections.length,
});

const matchedCourses =
matchedSections.length + matchedDepts.length === MAX_AUTOCOMPLETE_RESULTS
? []
: fuzzysort.go(query, searchData.courses, {
keys: ['id', 'name', 'alias', 'metadata.department', 'metadata.number'],
limit: MAX_AUTOCOMPLETE_RESULTS - matchedDepts.length - matchedSections.length,
}).map((course) => {
return {...course, obj: {...course.obj, isOffered: isCourseOffered(
course.obj.metadata.department,
course.obj.metadata.number,
termSectionCodes)
}}
}).sort((a, b) => {
if (a.obj.isOffered === b.obj.isOffered) return 0;
return a.obj.isOffered ? -1 : 1;
});


: fuzzysort
.go(query, searchData.courses, {
keys: ['id', 'name', 'alias', 'metadata.department', 'metadata.number'],
limit: MAX_AUTOCOMPLETE_RESULTS - matchedDepts.length - matchedSections.length,
})
.map((course) => {
return {
...course,
obj: {
...course.obj,
isOffered: isCourseOffered(
course.obj.metadata.department,
course.obj.metadata.number,
termSectionCodes
),
},
};
})
.sort((a, b) => {
if (a.obj.isOffered === b.obj.isOffered) return 0;
return a.obj.isOffered ? -1 : 1;
});

return Object.fromEntries([
...matchedSections.map((x) => [x.sectionCode, x]),
Expand Down
20 changes: 19 additions & 1 deletion apps/antalmanac/src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { useSessionStore } from '$stores/SessionStore';

export function Header() {
const [openSuccessfulSaved, setOpenSuccessfulSaved] = useState(false);
const [openSignoutDialog, setOpenSignoutDialog] = useState(false);
const importedUser = getLocalStorageImportedUser() ?? '';
const { session, sessionIsValid } = useSessionStore();

Expand All @@ -33,6 +34,15 @@ export function Header() {
clearStorage();
};

const handleLogoutComplete = () => {
setOpenSignoutDialog(true);
};

const handleCloseSignoutDialog = () => {
setOpenSignoutDialog(false);
window.location.reload();
};

useEffect(() => {
const dataCache = getLocalStorageDataCache() ?? '';

Expand Down Expand Up @@ -67,7 +77,7 @@ export function Header() {
<Stack direction="row" sx={{ alignItems: 'center' }}>
<Import key="studylist" />
<Save />
{sessionIsValid ? <Signout /> : <Signin />}
{sessionIsValid ? <Signout onLogoutComplete={handleLogoutComplete} /> : <Signin />}
<AppDrawer key="settings" />
</Stack>

Expand All @@ -79,6 +89,14 @@ export function Header() {
>
NOTE: All changes made to your schedules will be saved to your Google account
</AlertDialog>
<AlertDialog
open={openSignoutDialog}
title="Signed out successfully"
severity="info"
onClose={handleCloseSignoutDialog}
>
You have successfully signed out. Close to continue browsing AntAlmanac.
</AlertDialog>
</Box>
</AppBar>
);
Expand Down
49 changes: 27 additions & 22 deletions apps/antalmanac/src/components/Header/Signout.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,59 @@
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
import LogoutIcon from '@mui/icons-material/Logout';
import { Avatar, Menu, ListItemIcon, ListItemText, MenuItem, IconButton } from '@mui/material';
import { useEffect, useState, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { User } from '@packages/antalmanac-types';
import { useEffect, useState, useCallback, type MouseEvent } from 'react';

import trpc from '$lib/api/trpc';
import { useSessionStore } from '$stores/SessionStore';

export function Signout() {
interface SignoutProps {
onLogoutComplete?: () => void;
}

export function Signout({ onLogoutComplete }: SignoutProps) {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [user, setUser] = useState<{ name?: string | null; avatar?: string | null } | null>(null);
const navigate = useNavigate();
const [user, setUser] = useState<Pick<User, 'name' | 'avatar'> | null>(null);
const { session, sessionIsValid, clearSession } = useSessionStore();

const open = Boolean(anchorEl);
const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};

const handleLogout = async () => {
setAnchorEl(null);
if (!session) {
navigate('/');
await clearSession();
onLogoutComplete?.();
return;
}

try {
const { logoutUrl } = await trpc.userData.logout.mutate({
await trpc.userData.logout.mutate({
sessionToken: session,
redirectUrl: window.location.origin,
});
clearSession();

window.location.href = logoutUrl;
await clearSession();
onLogoutComplete?.();
} catch (error) {
console.error('Error during logout', error);
clearSession();
navigate('/');
// Even on error, clear session and show dialog
await clearSession();
onLogoutComplete?.();
}
};

const { session, sessionIsValid, clearSession } = useSessionStore();

const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};

const handleAuthChange = useCallback(async () => {
if (sessionIsValid) {
const userData = await trpc.userData.getUserAndAccountBySessionToken
.query({ token: session ?? '' })
.then((res) => res.users);
setUser(userData);
setUser({ name: userData.name ?? undefined, avatar: userData.avatar ?? undefined });
}
}, [session, sessionIsValid, setUser]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,13 +299,12 @@ class FuzzySearch extends PureComponent<FuzzySearchProps, FuzzySearchState> {
renderOption = (props: React.HTMLAttributes<HTMLLIElement>, option: SearchOption) => {
const object = option.result;
const { key, ...restProps } = props as React.HTMLAttributes<HTMLLIElement> & { key: string };
if (!object) {
if (!object)
return (
<Box component="li" key={key} {...restProps}>
{option.key}
</Box>
);
}

const label = this.getOptionLabel(option);
const isCourse = object.type === resultType.COURSE;
Expand Down
1 change: 1 addition & 0 deletions apps/antalmanac/src/lib/localStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export function getLocalStorageSessionId() {
export function removeLocalStorageSessionId() {
window.localStorage.removeItem(LSK.sessionId);
}

// Helper functions for patchNotesKey
export function setLocalStoragePatchNotesKey(value: string) {
window.localStorage.setItem(LSK.patchNotesKey, value);
Expand Down
4 changes: 1 addition & 3 deletions apps/antalmanac/src/stores/SessionStore.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { create } from 'zustand';

import trpc from '$lib/api/trpc';
import { getLocalStorageSessionId, removeLocalStorageSessionId, setLocalStorageSessionId } from '$lib/localStorage';
import { getLocalStorageSessionId, setLocalStorageSessionId, removeLocalStorageSessionId } from '$lib/localStorage';

interface SessionState {
session: string | null;
Expand Down Expand Up @@ -31,8 +31,6 @@ export const useSessionStore = create<SessionState>((set) => {
if (currentSession) {
await trpc.auth.invalidateSession.mutate({ token: currentSession });
removeLocalStorageSessionId();
set({ session: null, sessionIsValid: false });
window.location.reload();
}
},
};
Expand Down
Loading