diff --git a/src/app/private/chapter-leader/users/MembersHomePage.tsx b/src/app/private/chapter-leader/users/MembersHomePage.tsx index 78b2e4d1..ef707721 100644 --- a/src/app/private/chapter-leader/users/MembersHomePage.tsx +++ b/src/app/private/chapter-leader/users/MembersHomePage.tsx @@ -11,6 +11,7 @@ import { Dropdown } from "@components/selector"; import { Popup } from "@components/container"; import { useRouter } from "next/navigation"; import { sortedStudents } from "@utils"; +import { useApiThrottle } from "@hooks"; type MembersHomePageProps = { members: User[]; @@ -18,7 +19,7 @@ type MembersHomePageProps = { const EBOARD_POSITIONS = [ "Social Coordinator", - "Senior Outreach Coordinator", + "Outreach Coordinator", "Head of Media", "Secretary", "Treasurer", @@ -37,6 +38,14 @@ const MembersHomePage = ({ members }: MembersHomePageProps) => { setSelectedPosition([]); }; + const { fn: throttleEditPosition } = useApiThrottle({ + fn: editPosition, + callback: () => { + resetAssignment(); + router.refresh(); + }, + }); + const displayMembers = (elem: User, index: number) => ( { display={(element) => <>{element.position}} selected={selectedPosition} setSelected={setSelectedPosition} - onSave={async () => { - await editPosition( - { - body: { position: selectedPosition[0]?.position ?? "" }, - }, + onSave={async () => + await throttleEditPosition( + { body: { position: selectedPosition[0]?.position ?? "" } }, uidToEdit - ); - resetAssignment(); - router.refresh(); - }} + ) + } multipleChoice={false} /> diff --git a/src/components/ChapterRequest.tsx b/src/components/ChapterRequest.tsx index 06f8fb8a..9ba730b8 100644 --- a/src/components/ChapterRequest.tsx +++ b/src/components/ChapterRequest.tsx @@ -1,24 +1,11 @@ "use client"; -import { useState } from "react"; -import { faEnvelope, faPhone } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + import { handleChapterRequest } from "src/app/api/handle-chapter-request/route.client"; import { useRouter } from "next/navigation"; import { ChapterRequest } from "@prisma/client"; import { InfoTile } from "./TileGrid"; - -type ButtonProps = { - chapterRequestId: string; - universityName: string; - universityAddress: string; - name: string; - email: string; - phoneNumber: string; - leadershipExperience: string; - motivation: string; - availabilities: string; - questions: string; -}; +import { useApiThrottle } from "@hooks"; +import { Spinner } from "./skeleton"; interface ChapterRequestMoreInformation { question: string; @@ -44,6 +31,11 @@ const ChapterRequest = (props: ChapterRequestProps) => { const router = useRouter(); + const { fetching, fn: throttleChapterRequest } = useApiThrottle({ + fn: handleChapterRequest, + callback: () => router.refresh(), + }); + const qas: ChapterRequestMoreInformation[] = [ { question: @@ -92,165 +84,44 @@ const ChapterRequest = (props: ChapterRequestProps) => { {qas.map((question) => ( ))} -
-
{ - handleChapterRequest({ - body: { - chapterRequestId: request.id, - approved: true, - }, - }).then((res) => { - if (res.code === "SUCCESS") { - router.refresh(); - } else { - alert("Please refresh the page and try again"); - } - }); - }} - > - Accept + {!fetching ? ( +
+
+ throttleChapterRequest({ + body: { + chapterRequestId: request.id, + approved: true, + }, + }) + } + > + Accept +
+
+ throttleChapterRequest({ + body: { + chapterRequestId: request.id, + approved: false, + }, + }) + } + > + Decline +
-
{ - handleChapterRequest({ - body: { - chapterRequestId: request.id, - approved: false, - }, - }).then((res) => { - if (res.code === "SUCCESS") { - router.refresh(); - } else { - alert("Please refresh the page and try again"); - } - }); - }} - > - Decline + ) : ( +
+
-
+ )}
} /> ); - // const router = useRouter(); - // const [showMore, setShowMore] = useState(false); - // return ( - //
- //
- // {universityName}
- //
- //
- //
- //
- // Location: {universityAddress} - //
- //
- // Requester: {name} - //
- //
- //
- //
- // - // {email}
- //
- //
- // - // {phoneNumber}
- //
- //
- //
- - // {showMore ? ( - //
- //
setShowMore((b) => !b)} - // > - // Show less - //
- //
- //

- // Do you have any experience in student leadership / club - // organizations / storytelling? - //

- //

{leadershipExperience}

- //
- //
- //

- // What motivates you to start this initiative in your community? - //

- //

{motivation}

- //
- //
- //

- // Please list three 1 hour time blocks with your availability in the - // next week. - //

- //

{availabilities}

- //
- //
- //

- // What questions or comments do you have for us? - //

- //

{questions}

- //
- //
- //
{ - // handleChapterRequest({ - // body: { - // chapterRequestId: chapterRequestId, - // approved: true, - // }, - // }).then((res) => { - // if (res.code === "SUCCESS") { - // router.refresh(); - // } else { - // alert("Please refresh the page and try again"); - // } - // }); - // }} - // > - // Accept - //
- //
{ - // handleChapterRequest({ - // body: { - // chapterRequestId: chapterRequestId, - // approved: false, - // }, - // }).then((res) => { - // if (res.code === "SUCCESS") { - // router.refresh(); - // } else { - // alert("Please refresh the page and try again"); - // } - // }); - // }} - // > - // Decline - //
- //
- //
- // ) : ( - //
- //
setShowMore((b) => !b)} - // > - // Show more - //
- //
- // )} - //
- // ); }; export default ChapterRequest; diff --git a/src/components/DisplayChapterInfo.tsx b/src/components/DisplayChapterInfo.tsx index 9867c850..44af854e 100644 --- a/src/components/DisplayChapterInfo.tsx +++ b/src/components/DisplayChapterInfo.tsx @@ -14,6 +14,7 @@ import { Dropdown } from "./selector"; import { editRole } from "@api/admin/edit-role/route.client"; import { useRouter } from "next/navigation"; import DropDownContainer from "@components/container/DropDownContainer"; +import { useApiThrottle } from "@hooks"; type ChapterWithUser = Prisma.ChapterGetPayload<{ include: { students: true }; @@ -52,19 +53,22 @@ const DisplayChapterInfo = ({ React.useState(false); const [assignedPresidents, setAssignedPresidents] = React.useState(currentPresidents); + const { fn: throttleEditRole } = useApiThrottle({ + fn: editRole, + callback: () => router.refresh(), + }); const onSaveNewPresidents = async () => { const previousPresidents = currentPresidents.filter( (student) => assignedPresidents.find((other) => student.id === other.id) == undefined ); - await editRole({ + await throttleEditRole({ body: { chapterLeaders: assignedPresidents.map((student) => student.id), users: previousPresidents.map((student) => student.id), }, }); - router.refresh(); }; const resetAssignment = () => { diff --git a/src/components/TileGrid/UserTile.tsx b/src/components/TileGrid/UserTile.tsx index f028fc10..cf9d5545 100644 --- a/src/components/TileGrid/UserTile.tsx +++ b/src/components/TileGrid/UserTile.tsx @@ -54,39 +54,41 @@ export function UserTile({
-
-
-

- {student - ? fullName(student) - : senior - ? seniorFullName(senior) - : null} -

- {student ? ( -

- {student.role === "CHAPTER_LEADER" - ? "Chapter Leader" - : student.position === "" - ? "Member" - : student.position} -

- ) : null} - {/* @TODO: Add pronouns once we add to student field */} - {senior ? ( -

- {senior.location} +

+
+
+

+ {student + ? fullName(student) + : senior + ? seniorFullName(senior) + : null}

- ) : null} +
+ {dropdownComponent}
- {dropdownComponent} + {student ? ( +

+ {student.role === "CHAPTER_LEADER" + ? "Chapter Leader" + : student.position === "" + ? "Member" + : student.position} +

+ ) : null} + {/* @TODO: Add pronouns once we add to student field */} + {senior ? ( +

+ {senior.location} +

+ ) : null}
); diff --git a/src/components/selector/Dropdown.tsx b/src/components/selector/Dropdown.tsx index 4acdbe81..bc1466df 100644 --- a/src/components/selector/Dropdown.tsx +++ b/src/components/selector/Dropdown.tsx @@ -31,7 +31,10 @@ const Dropdown = (props: DropdownProps) => { const onSave = () => { setLoading(true); - props.onSave().then(() => setLoading(false)); + props.onSave().then(() => { + setLoading(false); + setDisplayDropdown(false); + }); }; const onCheck = ( @@ -90,7 +93,7 @@ const Dropdown = (props: DropdownProps) => {
{loading ? (
- +
) : ( diff --git a/src/components/user/DisplayUserSeniors.tsx b/src/components/user/DisplayUserSeniors.tsx index 3e3f5c8b..2bb2726d 100644 --- a/src/components/user/DisplayUserSeniors.tsx +++ b/src/components/user/DisplayUserSeniors.tsx @@ -5,6 +5,7 @@ import { UserTile } from "@components/TileGrid"; import { CardGrid } from "@components/container"; import Assignment from "@components/senior/assignment"; import { RoleToUrlSegment } from "@constants/RoleAlias"; +import { useApiThrottle } from "@hooks"; import { Prisma, Senior } from "@prisma/client"; import { compareSenior, fullName, seniorFullName } from "@utils"; import React from "react"; @@ -31,8 +32,10 @@ const DisplayUserSenior = (props: DisplayProps) => { const [assigned, setAssigned] = React.useState(() => getAssignments()); + const { fn: throttleEditSeniorIds } = useApiThrottle({ fn: editSeniorIDs }); + const onSave = async () => { - await editSeniorIDs( + await throttleEditSeniorIds( { body: { SeniorIDs: assigned.map((senior) => senior.id), @@ -56,7 +59,6 @@ const DisplayUserSenior = (props: DisplayProps) => { setSelected={setAssigned} onSave={onSave} /> - ( { const { @@ -22,19 +24,23 @@ const EditProfileForm = () => { resolver: zodResolver(EditProfileRequest), }); - const onSubmit: SubmitHandler = async (data, event) => { - event?.preventDefault(); - const response = await editProfile({ body: data }, uid); - if (response.code == "SUCCESS") { + const { fetching, fn: throttleEditProfile } = useApiThrottle({ + fn: editProfile, + callback: () => { setEdited(false); router.refresh(); - } + }, + }); + + const onSubmit: SubmitHandler = async (data, event) => { + event?.preventDefault(); + throttleEditProfile({ body: data }, uid); }; return ( <>

Edit my profile

-
+
@@ -83,17 +89,24 @@ const EditProfileForm = () => { >
- + +
+ {!fetching ? ( + + ) : ( + + )} +
); diff --git a/src/hooks/index.tsx b/src/hooks/index.tsx index c4f4796e..e768df1a 100644 --- a/src/hooks/index.tsx +++ b/src/hooks/index.tsx @@ -1,2 +1,3 @@ export { default as useAuth } from "./useAuth"; export { default as useUserRedirect } from "./useUserRedirect"; +export { default as useApiThrottle } from "./useApiThrottle"; diff --git a/src/hooks/useApiThrottle.tsx b/src/hooks/useApiThrottle.tsx new file mode 100644 index 00000000..3a246178 --- /dev/null +++ b/src/hooks/useApiThrottle.tsx @@ -0,0 +1,31 @@ +import React from "react"; + +type ApiCall = (...args: any[]) => Promise; + +interface UseApiThrottleProps { + fn: T; + callback?: (res: Awaited>) => void; +} + +/** + * Prevent additional API call until the most recent one has completed. + */ +const useApiThrottle = (props: UseApiThrottleProps) => { + const [fetching, setFetching] = React.useState(false); + + const fn: (...args: Parameters) => Promise = React.useCallback( + async (...args) => { + if (fetching) { + return; + } + setFetching(true); + await props.fn(...args).then(props.callback); + setFetching(false); + }, + [props, fetching] + ); + + return { fetching, fn }; +}; + +export default useApiThrottle;