Skip to content

Commit b7153da

Browse files
authored
Merge branch 'main' into main
2 parents ab1d8cb + 8db869b commit b7153da

8 files changed

Lines changed: 166 additions & 87 deletions

File tree

.storybook/fetch-mock.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Must load before any module that calls fetch() (e.g. GitHubStatsService) evaluates.
2+
// GitHubStatsService hits the real GitHub API on mount; in Chromatic's sandboxed
3+
// browser that request hangs/fails with no network access, timing out story capture.
4+
const realFetch = globalThis.fetch;
5+
6+
const MOCKED_HOSTS = ['github.cachedapi.com', 'api.github.com'];
7+
8+
function resolveUrl(input: RequestInfo | URL): string {
9+
if (typeof input === 'string') return input;
10+
if (input instanceof URL) return input.href;
11+
return input.url;
12+
}
13+
14+
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
15+
const url = resolveUrl(input);
16+
17+
if (MOCKED_HOSTS.some(host => url.includes(host))) {
18+
return new Response(JSON.stringify([]), {
19+
status: 200,
20+
headers: { 'Content-Type': 'application/json' },
21+
});
22+
}
23+
24+
return realFetch(input, init);
25+
};

.storybook/preview.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
// Must be first importsets up globalThis.chrome before any other module evaluates
1+
// Must be first importsset up globalThis.chrome/fetch before any other module evaluates
22
import './chrome-mock';
3+
import './fetch-mock';
34

45
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
56
import type { Preview } from '@storybook/react-vite';

src/shared/util/checkLoginStatus.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,15 @@ import { UTRP_LOGIN_URL } from '@shared/util/appUrls';
22

33
/**
44
* Checks whether the user is logged in to the UT Registrar.
5-
* If not logged in, opens a new tab to the login page and returns `false`.
6-
* When `silent` is true, skips opening the login tab so background refreshes can fail quietly.
5+
* Performs a cookie-only auth check and returns a pure boolean; callers own any UI affordance.
76
*
8-
* @param options.silent when true, suppresses the login tab on 401/403
97
* @returns A promise that resolves to `true` if the user is logged in, otherwise `false`.
108
*/
11-
export async function validateLoginStatus(options?: { silent?: boolean }) {
9+
export async function validateLoginStatus(): Promise<boolean> {
1210
try {
1311
const response = await fetch(UTRP_LOGIN_URL, { credentials: 'include' });
1412

1513
if (response.redirected || response.status === 401 || response.status === 403) {
16-
if (!options?.silent) {
17-
chrome.tabs.create({ url: UTRP_LOGIN_URL });
18-
}
1914
return false;
2015
}
2116

src/views/components/calendar/CalendarHeader/CalendarHeader.tsx

Lines changed: 28 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react';
22
import { OptionsStore } from '@shared/storage/OptionsStore';
3+
import { UTRP_LOGIN_URL } from '@shared/util/appUrls';
34
import styles from '@views/components/calendar/CalendarHeader/CalendarHeader.module.scss';
45
import { Button } from '@views/components/common/Button';
56
import Divider from '@views/components/common/Divider';
@@ -13,7 +14,7 @@ import { useActiveSchedule } from '@views/hooks/useSchedules';
1314
import refreshCourses from '@views/lib/refreshCourses';
1415
import clsx from 'clsx';
1516
import type { JSX } from 'react';
16-
import { useCallback, useEffect, useRef, useState } from 'react';
17+
import { useCallback, useState } from 'react';
1718
import ArrowsClockwiseIcon from '~icons/ph/arrows-clockwise';
1819
import CalendarDotsIcon from '~icons/ph/calendar-dots';
1920
import ExportIcon from '~icons/ph/export';
@@ -37,28 +38,25 @@ export default function CalendarHeader({ sidebarOpen, onSidebarToggle }: Calenda
3738
const activeSchedule = useActiveSchedule();
3839
const lastCheckedText = useRelativeTime(activeSchedule.lastCheckedAt);
3940
const [isRefreshing, setIsRefreshing] = useState(false);
41+
const [showLoginPrompt, setShowLoginPrompt] = useState(false);
4042
// track per-schedule cooldowns so switching schedules allows immediate refresh
4143
const [cooldownIds, setCooldownIds] = useState<Set<string>>(new Set());
4244
const enableDataRefreshing = OptionsStore.useStore(store => store.enableDataRefreshing);
4345

4446
const isCooldown = cooldownIds.has(activeSchedule.id);
4547
const hasRightHandSide = enableDataRefreshing;
46-
const isRefreshingRef = useRef(false);
47-
isRefreshingRef.current = isRefreshing;
4848

4949
const handleRefresh = useCallback(async () => {
50+
if (isRefreshing) return;
5051
setIsRefreshing(true);
5152
// Ensure the spinner is visible long enough to provide visual feedback
52-
const minSpin = new Promise(r => setTimeout(r, 400));
53+
const minSpin = new Promise(resolve => setTimeout(resolve, 400));
5354
try {
54-
await chrome.storage.session.set({ pendingRefresh: true });
5555
const [success] = await Promise.all([refreshCourses(), minSpin]);
56-
if (success) {
57-
await chrome.storage.session.remove('pendingRefresh');
58-
}
56+
setShowLoginPrompt(!success);
5957
} catch (error) {
6058
console.error('Failed to refresh courses:', error);
61-
await chrome.storage.session.remove('pendingRefresh');
59+
setShowLoginPrompt(true);
6260
} finally {
6361
setIsRefreshing(false);
6462
const scheduleId = activeSchedule.id;
@@ -71,27 +69,7 @@ export default function CalendarHeader({ sidebarOpen, onSidebarToggle }: Calenda
7169
});
7270
}, 3000);
7371
}
74-
}, [activeSchedule]);
75-
76-
// Auto-retry refresh after login redirect
77-
useEffect(() => {
78-
const checkPendingRefresh = async () => {
79-
const { pendingRefresh } = await chrome.storage.session.get('pendingRefresh');
80-
if (pendingRefresh && !isRefreshingRef.current) {
81-
handleRefresh();
82-
}
83-
};
84-
85-
checkPendingRefresh();
86-
87-
const onVisibilityChange = () => {
88-
if (document.visibilityState === 'visible') {
89-
checkPendingRefresh();
90-
}
91-
};
92-
document.addEventListener('visibilitychange', onVisibilityChange);
93-
return () => document.removeEventListener('visibilitychange', onVisibilityChange);
94-
}, [handleRefresh]);
72+
}, [activeSchedule, isRefreshing]);
9573

9674
return (
9775
<div
@@ -206,11 +184,26 @@ export default function CalendarHeader({ sidebarOpen, onSidebarToggle }: Calenda
206184
</div>
207185
{hasRightHandSide && <Divider className='self-center' size='1.75rem' orientation='vertical' />}
208186
<div className={clsx(styles.secondaryActions, 'flex items-center gap-3 ml-auto')}>
209-
{enableDataRefreshing && lastCheckedText && (
210-
<Text variant='mini' className='whitespace-nowrap text-theme-black/50 !font-normal'>
211-
Last checked: {lastCheckedText}
212-
</Text>
213-
)}
187+
{enableDataRefreshing &&
188+
(showLoginPrompt ? (
189+
<Text variant='mini' className='whitespace-nowrap text-theme-black/50 !font-normal'>
190+
<a
191+
href={UTRP_LOGIN_URL}
192+
target='_blank'
193+
rel='noreferrer'
194+
className='text-ut-burntorange underline'
195+
>
196+
Log in
197+
</a>
198+
{' to refresh course data'}
199+
</Text>
200+
) : (
201+
lastCheckedText && (
202+
<Text variant='mini' className='whitespace-nowrap text-theme-black/50 !font-normal'>
203+
Last checked: {lastCheckedText}
204+
</Text>
205+
)
206+
))}
214207
{enableDataRefreshing && (
215208
<Button
216209
color='ut-black'

src/views/components/common/ScheduleListItem.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type { UserSchedule } from '@shared/types/UserSchedule';
77
import Text from '@views/components/common/Text/Text';
88
import { useEnforceScheduleLimit } from '@views/hooks/useEnforceScheduleLimit';
99
import { useIsActiveSchedule } from '@views/hooks/useSchedules';
10-
import { LONGHORN_DEVELOPERS_ADMINS, LONGHORN_DEVELOPERS_SWE } from '@views/lib/getGitHubStats';
10+
import { LONGHORN_DEVELOPERS_ADMINS, LONGHORN_DEVELOPERS_HARDCODED } from '@views/lib/getGitHubStats';
1111
import clsx from 'clsx';
1212
import React, { useEffect, useState } from 'react';
1313
import CircleIcon from '~icons/ph/circle';
@@ -33,7 +33,7 @@ interface ScheduleListItemProps {
3333
}
3434

3535
const IS_STORYBOOK = import.meta.env.STORYBOOK;
36-
const teamMembers = [...LONGHORN_DEVELOPERS_ADMINS, ...LONGHORN_DEVELOPERS_SWE];
36+
const teamMembers = [...LONGHORN_DEVELOPERS_ADMINS, ...LONGHORN_DEVELOPERS_HARDCODED];
3737

3838
/**
3939
* This is a reusable dropdown component that can be used to toggle the visiblity of information

src/views/components/settings/Settings.tsx

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { useActiveSchedule } from '@views/hooks/useSchedules';
2020
import {
2121
GitHubStatsService,
2222
LONGHORN_DEVELOPERS_ADMINS,
23-
LONGHORN_DEVELOPERS_SWE,
23+
LONGHORN_DEVELOPERS_HARDCODED,
2424
UTRP_ALUMNI,
2525
UTRP_LEADS,
2626
} from '@views/lib/getGitHubStats';
@@ -42,6 +42,25 @@ import { useDevMode } from './useDevMode';
4242

4343
const manifest = chrome.runtime.getManifest();
4444

45+
/**
46+
* Function that returns sort order of a provided member, for the settings page stats.
47+
* This is located outside of the component because we use useMemo and there is no need
48+
* to use useCallback with a function that deals with no components
49+
* @param member A member of LHD
50+
* @returns
51+
*/
52+
const getContributorPriority = (member: { role: string[] }) => {
53+
const roles = member.role.map(role => role.toLowerCase());
54+
55+
if (roles.some(role => role.includes('founder'))) return 0;
56+
if (roles.some(role => role.includes('co-founder'))) return 1;
57+
if (roles.some(role => role.includes('advisor'))) return 2;
58+
if (roles.some(role => role.includes('former'))) return 3;
59+
if (roles.some(role => role.includes('alumni'))) return 4;
60+
61+
return 5;
62+
};
63+
4564
/**
4665
* Main Settings Component for managing user settings and preferences.
4766
*
@@ -148,20 +167,25 @@ export default function Settings(): React.JSX.Element {
148167
}, []);
149168

150169
const sortedContributors = useMemo(() => {
151-
const base = [...LONGHORN_DEVELOPERS_SWE, ...UTRP_LEADS, ...UTRP_ALUMNI];
170+
const base = [...LONGHORN_DEVELOPERS_HARDCODED, ...UTRP_LEADS, ...UTRP_ALUMNI];
152171
if (!githubStats) return base;
153-
return [...base].sort(
154-
(a, b) =>
172+
173+
return [...base].sort((a, b) => {
174+
const priorityDiff = getContributorPriority(a) - getContributorPriority(b);
175+
if (priorityDiff !== 0) return priorityDiff;
176+
177+
return (
155178
(githubStats.userGitHubStats[b.githubUsername]?.commits ?? 0) -
156179
(githubStats.userGitHubStats[a.githubUsername]?.commits ?? 0)
157-
);
180+
);
181+
});
158182
}, [githubStats]);
159183

160184
const additionalContributors = useMemo(() => {
161185
if (!githubStats) return [];
162186
const knownUsernames = new Set<string>([
163187
...LONGHORN_DEVELOPERS_ADMINS.map(a => a.githubUsername),
164-
...LONGHORN_DEVELOPERS_SWE.map(s => s.githubUsername),
188+
...LONGHORN_DEVELOPERS_HARDCODED.map(s => s.githubUsername),
165189
...UTRP_LEADS.map(l => l.githubUsername),
166190
...UTRP_ALUMNI.map(a => a.githubUsername),
167191
]);

0 commit comments

Comments
 (0)