Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,18 @@
"prettier:write": "prettier --write ."
},
"dependencies": {
"@acm-uiuc/core-client": "^4.1.9",
"@acm-uiuc/js-shared": "^3.2.0",
"@azure/msal-browser": "^4.15.0",
"@azure/msal-react": "^3.0.15",
"@heroui/react": "2.7.6",
"@marsidev/react-turnstile": "^1.4.1",
"axios": "^0.28.0",
"framer-motion": "^10.17.9",
"lottie-react": "^2.3.1",
"moment": "^2.29.4",
"moment-timezone": "^0.5.45",
"next": "^14.2.25",
"pluralize": "^8.0.0",
"react": "^18.3.1",
"react-big-calendar": "^1.13.1",
"react-dom": "^18.3.1",
Expand All @@ -45,6 +46,7 @@
"@testing-library/react": "^13.3.0",
"@types/jest": "^27.5.2",
"@types/node": "^20",
"@types/pluralize": "^0.0.33",
"@types/react": "^18",
"@types/react-big-calendar": "^1.8.8",
"@types/react-dom": "^18",
Expand Down
37 changes: 7 additions & 30 deletions src/app/(main)/calendar/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import { Suspense, useEffect, useState } from 'react';
import CalendarControls from '@/components/CalendarControls';
import { View, Views } from 'react-big-calendar';
import { transformApiDates } from '@/utils/dateutils';
import { OrganizationName, Organizations } from '@acm-uiuc/js-shared';
import { AllOrganizationNameList, OrganizationName, Organizations } from '@acm-uiuc/js-shared';
import { useSearchParams } from 'next/navigation';
import { eventsApiClient } from '@/utils/api';

const defaultEvent: CalendarEventDetailProps = {
description: 'N/A',
Expand All @@ -30,35 +31,11 @@ const Calendar = () => {
OrganizationList.includes(host as OrganizationName) ? host : '',
);
const [allEvents, setAllEvents] = useState<IEvent[] | null>(null);
const [validOrganizations, setValidOrganizations] =
useState<string[]>(OrganizationList);
useEffect(() => {
const baseurl = process.env.NEXT_PUBLIC_CORE_API_BASE_URL;
if (!baseurl) {
return setAllEvents([]);
}
async function fetcher() {
const urls = [
`${baseurl}/api/v1/events`,
];

try {
const [eventsResponse] =
await Promise.allSettled(urls.map((url) => fetch(url)));
if (eventsResponse.status === 'fulfilled') {
const eventsData = await eventsResponse.value.json();
setAllEvents(transformApiDates(eventsData as IEvent[]));
} else {
setAllEvents([]); // Handle error for events fetch
}
setValidOrganizations(OrganizationList);
} catch (err) {
console.error('Error in processing fetch results:', err);
setAllEvents([]); // Fallback error handling for critical failure
setValidOrganizations(OrganizationList);
}
}
fetcher();
(async () => {
const allEvents = await eventsApiClient.apiV1EventsGet()
setAllEvents(transformApiDates(allEvents));
})();
Comment on lines 34 to +38
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add error handling for the events fetch to avoid unhandled rejections.

The async IIFE does not guard failures; a rejected promise will bubble and can leave the page in a broken state.

🔧 Suggested fix
   useEffect(() => {
     (async () => {
-      const allEvents = await eventsApiClient.apiV1EventsGet()
-      setAllEvents(transformApiDates(allEvents));
+      try {
+        const allEvents = await eventsApiClient.apiV1EventsGet();
+        setAllEvents(transformApiDates(allEvents));
+      } catch (e) {
+        console.error('Failed to load events', e);
+        setAllEvents([]);
+      }
     })();
   }, []);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
const baseurl = process.env.NEXT_PUBLIC_CORE_API_BASE_URL;
if (!baseurl) {
return setAllEvents([]);
}
async function fetcher() {
const urls = [
`${baseurl}/api/v1/events`,
];
try {
const [eventsResponse] =
await Promise.allSettled(urls.map((url) => fetch(url)));
if (eventsResponse.status === 'fulfilled') {
const eventsData = await eventsResponse.value.json();
setAllEvents(transformApiDates(eventsData as IEvent[]));
} else {
setAllEvents([]); // Handle error for events fetch
}
setValidOrganizations(OrganizationList);
} catch (err) {
console.error('Error in processing fetch results:', err);
setAllEvents([]); // Fallback error handling for critical failure
setValidOrganizations(OrganizationList);
}
}
fetcher();
(async () => {
const allEvents = await eventsApiClient.apiV1EventsGet()
setAllEvents(transformApiDates(allEvents));
})();
useEffect(() => {
(async () => {
try {
const allEvents = await eventsApiClient.apiV1EventsGet();
setAllEvents(transformApiDates(allEvents));
} catch (e) {
console.error('Failed to load events', e);
setAllEvents([]);
}
})();
}, []);
🤖 Prompt for AI Agents
In `@src/app/`(main)/calendar/page.tsx around lines 34 - 38, The async IIFE inside
useEffect calling eventsApiClient.apiV1EventsGet can reject and currently isn't
caught; wrap the await call in a try/catch (inside the IIFE) to handle failures,
log or report the error (via console.error or your logger), and set a safe state
(e.g., setAllEvents([]) or an error state) so transformApiDates and setAllEvents
are only called on success and the page doesn't suffer an unhandled rejection.

}, []);

// Function to handle filter changes, moved from Events.tsx
Expand Down Expand Up @@ -112,7 +89,7 @@ const Calendar = () => {
className="px-2 border-2 border-gray-300 rounded-md w-full h-full" // Styling for the dropdown
>
<option value="">{filterLabel}</option>
{validOrganizations.map((org) => (
{AllOrganizationNameList.map((org) => (
<option key={org} value={org}>
{org}
</option>
Expand Down
1 change: 0 additions & 1 deletion src/app/(membership)/admin/sync/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {
import { Spinner } from '@heroui/spinner';

import Lottie from 'lottie-react';
import axios from 'axios';
import Layout from '../../MembershipLayout';
import successAnimation from '../../success.json';
import { useSearchParams } from 'next/navigation';
Expand Down
64 changes: 31 additions & 33 deletions src/app/(membership)/check-membership/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,20 @@ import { Spinner } from '@heroui/spinner';
// import Lottie from 'lottie-react';
import dynamic from 'next/dynamic';
const Lottie = dynamic(() => import('lottie-react'), { ssr: false });
import axios from 'axios';
import Layout from '../MembershipLayout';
import successAnimation from '../success.json';
import { getUserAccessToken, initMsalClient } from '@/utils/msal';
import { MembershipPriceString } from "@acm-uiuc/js-shared"
import { IPublicClientApplication } from '@azure/msal-browser';
import { syncIdentity } from '@/utils/api';
import { membershipApiClient, syncIdentity } from '@/utils/api';
import { Configuration, MembershipApi, MobileWalletApi, ResponseError } from '@acm-uiuc/core-client';
import { coreApiBaseUrl } from '@/constants';

interface ErrorCode {
code?: number | string;
message: string;
}

const baseUrl = process.env.NEXT_PUBLIC_CORE_API_BASE_URL;
const walletApiBaseUrl = process.env.NEXT_PUBLIC_CORE_API_BASE_URL;

const Payment = () => {
const [netId, setNetId] = useState('');
const [fullName, setFullName] = useState<string | null>(null);
Expand Down Expand Up @@ -108,26 +106,31 @@ const Payment = () => {
setNetId(netId);
setIsLoading(true);
await syncIdentity(accessToken)
const url = `${baseUrl}/api/v1/membership`;
axios
.get(url, { headers: { "x-uiuc-token": accessToken } })
.then((response) => {
if (response.data.givenName && response.data.surname) {
setFullName(`${response.data.givenName} ${response.data.surname}`)
}
setIsPaidMember(response.data.isPaidMember || false);
setIsLoading(false);
modalMembershipStatus.onOpen();
})
.catch((error) => {
setIsLoading(false);
try {
const membershipResponse = await membershipApiClient.apiV1MembershipGet({ xUiucToken: accessToken });
if (membershipResponse.givenName && membershipResponse.surname) {
setFullName(`${membershipResponse.givenName} ${membershipResponse.surname}`)
}
setIsPaidMember(membershipResponse.isPaidMember);
setIsLoading(false);
modalMembershipStatus.onOpen();
} catch (e) {
console.error(e)
if (e instanceof ResponseError) {
setErrorMessage({
code: error?.response?.status || 500,
message: error?.message || 'An unknown error occurred',
code: e?.response?.status || 500,
message: (await e.response.json()).message || 'An unknown error occurred',
});
modalErrorMessage.onOpen();
});
};
} else {
setErrorMessage({
code: 400,
message: 'An unknown error occurred',
});
}
modalErrorMessage.onOpen();

}
}
Comment on lines +109 to +133
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reset isLoading on error to avoid a stuck spinner.

If the membership API call fails, the catch block opens an error modal but never clears the loading state.

🔧 Suggested fix
     try {
       const membershipResponse = await membershipApiClient.apiV1MembershipGet({ xUiucToken: accessToken });
       if (membershipResponse.givenName && membershipResponse.surname) {
         setFullName(`${membershipResponse.givenName} ${membershipResponse.surname}`)
       }
       setIsPaidMember(membershipResponse.isPaidMember);
-      setIsLoading(false);
       modalMembershipStatus.onOpen();
     } catch (e) {
       console.error(e)
       if (e instanceof ResponseError) {
         setErrorMessage({
           code: e?.response?.status || 500,
           message: (await e.response.json()).message || 'An unknown error occurred',
         });
       } else {
         setErrorMessage({
           code: 400,
           message: 'An unknown error occurred',
         });
       }
       modalErrorMessage.onOpen();
+    } finally {
+      setIsLoading(false);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
const membershipResponse = await membershipApiClient.apiV1MembershipGet({ xUiucToken: accessToken });
if (membershipResponse.givenName && membershipResponse.surname) {
setFullName(`${membershipResponse.givenName} ${membershipResponse.surname}`)
}
setIsPaidMember(membershipResponse.isPaidMember);
setIsLoading(false);
modalMembershipStatus.onOpen();
} catch (e) {
console.error(e)
if (e instanceof ResponseError) {
setErrorMessage({
code: error?.response?.status || 500,
message: error?.message || 'An unknown error occurred',
code: e?.response?.status || 500,
message: (await e.response.json()).message || 'An unknown error occurred',
});
modalErrorMessage.onOpen();
});
};
} else {
setErrorMessage({
code: 400,
message: 'An unknown error occurred',
});
}
modalErrorMessage.onOpen();
}
}
try {
const membershipResponse = await membershipApiClient.apiV1MembershipGet({ xUiucToken: accessToken });
if (membershipResponse.givenName && membershipResponse.surname) {
setFullName(`${membershipResponse.givenName} ${membershipResponse.surname}`)
}
setIsPaidMember(membershipResponse.isPaidMember);
modalMembershipStatus.onOpen();
} catch (e) {
console.error(e)
if (e instanceof ResponseError) {
setErrorMessage({
code: e?.response?.status || 500,
message: (await e.response.json()).message || 'An unknown error occurred',
});
} else {
setErrorMessage({
code: 400,
message: 'An unknown error occurred',
});
}
modalErrorMessage.onOpen();
} finally {
setIsLoading(false);
}
}
🤖 Prompt for AI Agents
In `@src/app/`(membership)/check-membership/page.tsx around lines 109 - 133, The
catch block for the membershipApiClient.apiV1MembershipGet call fails to clear
loading state, leaving the spinner stuck; update the catch handling in the
function around membershipApiClient.apiV1MembershipGet so that
setIsLoading(false) is called before opening the error modal
(modalErrorMessage.onOpen), and ensure this happens in both the ResponseError
branch and the generic error branch (i.e., call setIsLoading(false) once at the
start or end of the catch before setErrorMessage/modalErrorMessage).


const handleAddToWallet = async () => {
setIsWalletLoading(true);
Expand All @@ -149,15 +152,11 @@ const Payment = () => {
modalErrorMessage.onOpen();
return;
}
let response;
let response: Blob | undefined;
const apiConfig = new Configuration({ basePath: coreApiBaseUrl })
const walletApi = new MobileWalletApi(apiConfig)
try {
response = await axios.get(
`${walletApiBaseUrl}/api/v2/mobileWallet/membership`,
{
headers: { 'Content-Type': 'application/json', 'x-uiuc-token': accessToken },
responseType: 'blob',
},
);
response = await walletApi.apiV2MobileWalletMembershipGet({ xUiucToken: accessToken })
setIsWalletLoading(false);
} catch (error: any) {
setIsWalletLoading(false);
Expand All @@ -173,8 +172,7 @@ const Payment = () => {
if (!response) {
return;
}
const pkpassBlob = response.data;
const url = window.URL.createObjectURL(pkpassBlob);
const url = window.URL.createObjectURL(response);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', 'acm_uiuc_membership.pkpass');
Expand Down
82 changes: 31 additions & 51 deletions src/app/(membership)/membership/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ import {
import { Spinner } from '@heroui/spinner';

import Lottie from 'lottie-react';
import axios from 'axios';
import Layout from '../MembershipLayout';
import successAnimation from '../success.json';
import { useSearchParams } from 'next/navigation';
import config from '@/config.json';
import { type IPublicClientApplication } from "@azure/msal-browser";
import { getUserAccessToken, initMsalClient } from '@/utils/msal';
import { MembershipPriceString } from '@acm-uiuc/js-shared';
import { membershipApiClient } from '@/utils/api';
import { ResponseError, ValidationError } from '@acm-uiuc/core-client';


interface ErrorCode {
Expand Down Expand Up @@ -80,57 +80,37 @@ const Payment = () => {
modalErrorMessage.onOpen();
return;
}
const url = `${baseUrl}/api/v2/membership/checkout`;
axios
.get(url, {
headers: { "Content-Type": "text/plain", 'x-uiuc-token': accessToken }
})
.then((response) => {
window.location.replace(response.data);
})
.catch((error) => {
try {
const response = await membershipApiClient.apiV2MembershipCheckoutGet({
xUiucToken: accessToken,
});
window.location.replace(response)
} catch (e) {
console.error("Error purchasing membership", e)
if (e instanceof ResponseError) {
setIsLoading(false);
if (error.response) {
if (error.response.status === 422) {
const errorObj = error.response.data;
setErrorMessage({
code: errorObj.details[0].issue,
message: errorObj.details[0].description,
});
modalErrorMessage.onOpen();
}
else if (error.response.status === 403) {
setErrorMessage({
code: 409,
message: 'Could not verify NetID.',
});
modalAlreadyMember.onOpen();
} else if (error.response.status === 400) {
const errorObj = error.response.data;
if ((errorObj.message as string).includes("is already a paid member")) {
setErrorMessage({
code: 409,
message: 'The specified user is already a paid member.',
});
modalAlreadyMember.onOpen();
} else {
setErrorMessage({
code: errorObj.id,
message: errorObj.message,
});
modalErrorMessage.onOpen();
}
} else if (error.response.status === 409) {
setErrorMessage({
code: 500,
message:
'Internal server error: ' +
(error.response.data.message || 'could not process request'),
});
modalErrorMessage.onOpen();
}
const responseJson = await e.response.json() as ValidationError;
if (responseJson.message.includes("is already a paid member")) {
modalErrorMessage.onClose();
modalAlreadyMember.onOpen();
return;
}
});
setErrorMessage({
code: responseJson.id ?? 400,
message: responseJson.message ?? "An error occurred creating your checkout session."
})
} else {
setErrorMessage({
code: 400,
message: "An error occurred creating your checkout session."
})
}
modalErrorMessage.onOpen();
}
Comment on lines +83 to +109
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Ensure isLoading is cleared for all error paths.

In the current catch block, the non‑ResponseError path never resets isLoading, leaving the UI stuck in a loading state.

🔧 Suggested fix
-    try {
+    try {
       const response = await membershipApiClient.apiV2MembershipCheckoutGet({
         xUiucToken: accessToken,
       });
       window.location.replace(response)
     } catch (e) {
       console.error("Error purchasing membership", e)
       if (e instanceof ResponseError) {
-        setIsLoading(false);
         const responseJson = await e.response.json() as ValidationError;
         if (responseJson.message.includes("is already a paid member")) {
           modalErrorMessage.onClose();
           modalAlreadyMember.onOpen();
           return;
         }
         setErrorMessage({
           code: responseJson.id ?? 400,
           message: responseJson.message ?? "An error occurred creating your checkout session."
         })
       } else {
         setErrorMessage({
           code: 400,
           message: "An error occurred creating your checkout session."
         })
       }
       modalErrorMessage.onOpen();
+    } finally {
+      setIsLoading(false);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
const response = await membershipApiClient.apiV2MembershipCheckoutGet({
xUiucToken: accessToken,
});
window.location.replace(response)
} catch (e) {
console.error("Error purchasing membership", e)
if (e instanceof ResponseError) {
setIsLoading(false);
if (error.response) {
if (error.response.status === 422) {
const errorObj = error.response.data;
setErrorMessage({
code: errorObj.details[0].issue,
message: errorObj.details[0].description,
});
modalErrorMessage.onOpen();
}
else if (error.response.status === 403) {
setErrorMessage({
code: 409,
message: 'Could not verify NetID.',
});
modalAlreadyMember.onOpen();
} else if (error.response.status === 400) {
const errorObj = error.response.data;
if ((errorObj.message as string).includes("is already a paid member")) {
setErrorMessage({
code: 409,
message: 'The specified user is already a paid member.',
});
modalAlreadyMember.onOpen();
} else {
setErrorMessage({
code: errorObj.id,
message: errorObj.message,
});
modalErrorMessage.onOpen();
}
} else if (error.response.status === 409) {
setErrorMessage({
code: 500,
message:
'Internal server error: ' +
(error.response.data.message || 'could not process request'),
});
modalErrorMessage.onOpen();
}
const responseJson = await e.response.json() as ValidationError;
if (responseJson.message.includes("is already a paid member")) {
modalErrorMessage.onClose();
modalAlreadyMember.onOpen();
return;
}
});
setErrorMessage({
code: responseJson.id ?? 400,
message: responseJson.message ?? "An error occurred creating your checkout session."
})
} else {
setErrorMessage({
code: 400,
message: "An error occurred creating your checkout session."
})
}
modalErrorMessage.onOpen();
}
try {
const response = await membershipApiClient.apiV2MembershipCheckoutGet({
xUiucToken: accessToken,
});
window.location.replace(response)
} catch (e) {
console.error("Error purchasing membership", e)
if (e instanceof ResponseError) {
const responseJson = await e.response.json() as ValidationError;
if (responseJson.message.includes("is already a paid member")) {
modalErrorMessage.onClose();
modalAlreadyMember.onOpen();
return;
}
setErrorMessage({
code: responseJson.id ?? 400,
message: responseJson.message ?? "An error occurred creating your checkout session."
})
} else {
setErrorMessage({
code: 400,
message: "An error occurred creating your checkout session."
})
}
modalErrorMessage.onOpen();
} finally {
setIsLoading(false);
}
🤖 Prompt for AI Agents
In `@src/app/`(membership)/membership/page.tsx around lines 83 - 109, The catch
block for the membership checkout flow fails to clear loading state on the
non-ResponseError path; ensure setIsLoading(false) is called for all error paths
by either calling setIsLoading(false) at the start of the catch body (before
checking instanceof ResponseError) or adding setIsLoading(false) in the else
branch; update the code around the try/catch handling in page.tsx (references:
setIsLoading, ResponseError, responseJson, modalErrorMessage.onOpen,
modalAlreadyMember.onOpen) so that loading is always reset regardless of error
type.





}, [modalAlreadyMember, modalErrorMessage]);

useEffect(() => {
Expand Down
Loading
Loading