From c94f43eaede8ba6002a6873fe901a4e63e45fc26 Mon Sep 17 00:00:00 2001 From: Justin Pham <113923596+justin-phxm@users.noreply.github.com> Date: Tue, 8 Apr 2025 20:49:23 -0600 Subject: [PATCH 1/3] optimize userProfile page --- src/app/participant/check-in/page.tsx | 108 -------------- src/components/PurpleButton.tsx | 3 + src/components/UserProfile/FormInput.tsx | 40 +++++ src/components/UserProfile/TeamForm.tsx | 128 +++++++++------- src/components/UserProfile/TeamProfile.tsx | 3 +- src/components/UserProfile/UserForm.tsx | 26 ++-- src/components/UserProfile/UserProfile.tsx | 165 +++++++-------------- src/components/contexts/Provider.tsx | 62 ++++---- src/components/contexts/UserContext.tsx | 2 +- 9 files changed, 211 insertions(+), 326 deletions(-) delete mode 100644 src/app/participant/check-in/page.tsx create mode 100644 src/components/UserProfile/FormInput.tsx diff --git a/src/app/participant/check-in/page.tsx b/src/app/participant/check-in/page.tsx deleted file mode 100644 index b5d9a139..00000000 --- a/src/app/participant/check-in/page.tsx +++ /dev/null @@ -1,108 +0,0 @@ -"use client"; - -import { generateClient } from "aws-amplify/api"; -import Image from "next/image"; -import Link from "next/link"; -import { useEffect, useState } from "react"; -import { type Schema } from "@/amplify/data/resource"; -import { useUser } from "@/components/contexts/UserContext"; - -const check_mark_icon = "/svgs/checkin/check_mark.svg"; -const cross_icon = "/svgs/checkin/circle_cross.svg"; - -const CHECKIN_STATUS_TILE_STLYES = - "my-20 flex w-4/5 max-w-[1000px] flex-col items-center rounded-xl border-2 border-dark-pink bg-white p-10 shadow-[15px_15px_0px_0px_dark-pink]"; -const CHECKIN_STATUS_HEADER_STYLES = "mb-2 text-2xl font-bold text-dark-pink"; -const CHECKIN_STATUS_TEXT_STYLES = - "mb-6 max-w-[450px] text-center text-lg text-black"; -const CHECKIN_STATUS_BUTTON_STYLES = - "rounded-xl bg-dark-pink p-4 font-bold hover:bg-pastel-pink"; - -enum CheckInStatus { - Loading = "loading", - Error = "error", - Success = "success", -} - -const CheckInPage = () => { - const client = generateClient(); - const [status, setStatus] = useState(CheckInStatus.Loading); - const { currentUser } = useUser(); - - const handleCheckIn = async () => { - try { - const result = await client.mutations.SetUserAsCheckedIn({ - userId: currentUser.username, - }); - - console.log("User checked in:", result); - setStatus(CheckInStatus.Success); - } catch (error) { - console.error("Error checking in:", error); - setStatus(CheckInStatus.Error); - } - }; - - useEffect(() => { - const checkIn = async () => { - await handleCheckIn(); - }; - if (currentUser.username) { - checkIn(); - } - }, [currentUser.username]); - - return ( -
- {status === CheckInStatus.Loading && ( -

- Checking you in... -

- )} - {status === CheckInStatus.Error && ( -
- X icon -

Failed to Check-in

-

- We apologize for the inconvenience. Please try checking in again. -

- - Go To Home - -
- )} - {status === CheckInStatus.Success && ( -
- Check mark icon -

- You're Checked In! -

-

- Thanks for checking in to Hack the Change 2025! Click the button - below to return to your profile. -

- - Go To Profile - -
- )} -
- ); -}; - -export default CheckInPage; diff --git a/src/components/PurpleButton.tsx b/src/components/PurpleButton.tsx index 5b3a3bd8..aa24a519 100644 --- a/src/components/PurpleButton.tsx +++ b/src/components/PurpleButton.tsx @@ -6,15 +6,18 @@ export default function PurpleButton({ type, onClick, className, + tabIndex, }: { children: React.ReactNode; disabled?: boolean; type?: HTMLButtonElement["type"]; onClick?: () => void; className?: string; + tabIndex?: number; }) { return ( - - - )} +
+ + + +
+ {isFetching ? ( +

+ Loading... +

+ ) : ( + <> + {Array.isArray(teamData) && + teamData.map((member) => ( + + ))} + + )} +
+
+ +
+ ); } diff --git a/src/components/UserProfile/TeamProfile.tsx b/src/components/UserProfile/TeamProfile.tsx index 674331c0..7ce60d7e 100644 --- a/src/components/UserProfile/TeamProfile.tsx +++ b/src/components/UserProfile/TeamProfile.tsx @@ -113,5 +113,4 @@ const TeamProfile = () => { )} ); -}; -export default TeamProfile; +} diff --git a/src/components/UserProfile/UserForm.tsx b/src/components/UserProfile/UserForm.tsx index bde4ba63..b278f044 100644 --- a/src/components/UserProfile/UserForm.tsx +++ b/src/components/UserProfile/UserForm.tsx @@ -1,13 +1,11 @@ "use client"; -import type React from "react"; import { useState } from "react"; import { useFormStatus } from "react-dom"; import { type Schema } from "@/amplify/data/resource"; import { type UserFormProp } from "@/components/UserProfile/UserProfile"; export default function UserForm({ - data, userMutation, setIsEditing, isEditing, @@ -15,7 +13,8 @@ export default function UserForm({ setEnableCancelSave, }: UserFormProp) { const { pending } = useFormStatus(); - const [formState, setFormState] = useState(data); + const { currentUser } = useUser(); + const [formState, setFormState] = useState(currentUser); const onChange = (e: React.ChangeEvent): void => { const { name, value } = e.target; @@ -44,10 +43,10 @@ export default function UserForm({ className={`md:text-md my-2 rounded-full border-4 border-white bg-white py-2 ps-3 text-sm ${isEditing ? "text-black" : "text-ehhh-grey"}`} type="text" placeholder={formState.firstName ?? "First Name"} - onChange={(e: React.ChangeEvent) => onChange(e)} + onChange={onChange} value={formState.firstName ?? ""} - name="firstName" - disabled={!isEditing} // Disabled when not in edit mode + name={"firstName"} + disabled={!isEditing} />
@@ -59,7 +58,8 @@ export default function UserForm({ onChange={(e: React.ChangeEvent) => onChange(e)} name="lastName" value={formState.lastName ?? ""} - disabled={!isEditing} // Disabled when not in edit mode + name={"lastName"} + disabled={!isEditing} />
@@ -68,24 +68,25 @@ export default function UserForm({ className={`${"md:text-md my-2 rounded-full border-4 border-white bg-white py-2 ps-3 text-sm"} ${"text-ehhh-grey"}`} type="text" placeholder={formState.email ?? ""} - onChange={(e: React.ChangeEvent) => onChange(e)} + onChange={onChange} value={formState.email ?? ""} - name="email" - disabled // Should not be able to edit email + name={"email"} + disabled /> ) => onChange(e)} + onChange={onChange} value={formState.institution ?? ""} name="institution" disabled={!isEditing} // Disabled when not in edit mode @@ -135,7 +136,6 @@ export default function UserForm({ {enableCancelSave ? ( <> - ) : ( -
-
- Right Squiggly SVG{" "} - Right Squiggly SVG{" "} - Right Squiggly SVG{" "} -
-
- {/* */} -
-

My Details

- -
- {data && ( - - )} -
-
- )} - + + + ); -}; -export default UserProfile; +} diff --git a/src/components/contexts/Provider.tsx b/src/components/contexts/Provider.tsx index cc841419..fe5622c2 100644 --- a/src/components/contexts/Provider.tsx +++ b/src/components/contexts/Provider.tsx @@ -1,6 +1,5 @@ "use client"; -import React from "react"; import { toast } from "react-toastify"; import { Authenticator } from "@aws-amplify/ui-react"; import { @@ -15,39 +14,34 @@ export default function Provider({ }: { children: React.ReactNode | React.ReactNode[]; }) { - const [queryClient] = React.useState( - () => - new QueryClient({ - defaultOptions: { - queries: { - staleTime: 1000 * 60 * 5, - refetchOnWindowFocus: true, - initialDataUpdatedAt: 0, - }, - }, - queryCache: new QueryCache({ - onError: (error, query) => { - console.error("Query Boundary Caught:", error); - toast.error(`Error loading: ${query.queryKey[0]}`); - }, - // onSuccess(data, query) { - // toast.success(`${query.queryKey[0]} loaded`); - // }, - }), - mutationCache: new MutationCache({ - onError: (error, variables, context, mutation) => { - console.error("Mutation Boundary Caught:", error); - toast.error( - `Error processing: ${mutation.options?.mutationKey?.[0]}`, - ); - }, - // onSuccess(data, variables, context, mutation) { - // console.log(data, variables, context, mutation); - // toast.success(`${mutation.options?.mutationKey?.[0]} updated`); - // }, - }), - }), - ); + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 1000 * 60 * 5, + refetchOnWindowFocus: true, + initialDataUpdatedAt: 0, + }, + }, + queryCache: new QueryCache({ + onError: (error, query) => { + console.error("Query Boundary Caught:", error); + toast.error(`Error loading: ${query.queryKey[0]}`); + }, + // onSuccess(data, query) { + // toast.success(`${query.queryKey[0]} loaded`); + // }, + }), + mutationCache: new MutationCache({ + onError: (error, _, __, mutation) => { + console.error("Mutation Boundary Caught:", error); + toast.error(`Error processing: ${mutation.options?.mutationKey?.[0]}`); + }, + // onSuccess(data, variables, context, mutation) { + // console.log(data, variables, context, mutation); + // toast.success(`${mutation.options?.mutationKey?.[0]} updated`); + // }, + }), + }); return ( diff --git a/src/components/contexts/UserContext.tsx b/src/components/contexts/UserContext.tsx index 96625501..f0fc7878 100644 --- a/src/components/contexts/UserContext.tsx +++ b/src/components/contexts/UserContext.tsx @@ -18,7 +18,7 @@ export enum UserType { Guest = "Guest", } -type IUser = Schema["User"]["type"] & { +export type IUser = Schema["User"]["type"] & { type: UserType; username: string; }; From 06bc31e1038fc7b09229bab1a06b10a01d1b0297 Mon Sep 17 00:00:00 2001 From: Justin Pham <113923596+justin-phxm@users.noreply.github.com> Date: Tue, 8 Apr 2025 23:21:08 -0600 Subject: [PATCH 2/3] remove checkin and add devPost attribute --- amplify/auth/PostConfirmation/handler.ts | 1 - amplify/data/resource.ts | 28 +-- amplify/data/user/SetUserAsCheckedIn.js | 14 -- .../CreateTeamWithCode/handler.ts | 174 +++++++++--------- .../function/BusinessLogic/utils/try-catch.ts | 24 +++ src/app/admin/teams/TeamTableSetup.tsx | 8 - .../admin/teams/components/TeamsTablePage.tsx | 32 ++-- src/app/admin/teams/components/ViewButton.tsx | 4 +- src/app/participant/page.tsx | 1 - src/app/register/team/(pending)/new/page.tsx | 58 +++--- .../Dashboard/DevPostLinkUpload.tsx | 68 +++++++ src/components/Dashboard/GoToFoodTicket.tsx | 1 + .../Dashboard/SubmissionDueClock.tsx | 2 + src/components/UserProfile/FormInput.tsx | 8 +- src/components/UserProfile/TeamForm.tsx | 8 +- src/components/judging/ScoresTable.tsx | 2 +- 16 files changed, 252 insertions(+), 181 deletions(-) delete mode 100644 amplify/data/user/SetUserAsCheckedIn.js create mode 100644 amplify/function/BusinessLogic/utils/try-catch.ts create mode 100644 src/components/Dashboard/DevPostLinkUpload.tsx diff --git a/amplify/auth/PostConfirmation/handler.ts b/amplify/auth/PostConfirmation/handler.ts index 702bd92f..000352d9 100644 --- a/amplify/auth/PostConfirmation/handler.ts +++ b/amplify/auth/PostConfirmation/handler.ts @@ -57,7 +57,6 @@ export const handler: PostConfirmationTriggerHandler = async (event) => { role: "Participant", id: event.request.userAttributes.sub, email: event.request.userAttributes.email, - checkedIn: false, willEatMeals: false, allergies: "", institution: "", diff --git a/amplify/data/resource.ts b/amplify/data/resource.ts index cc1d81ab..34ca9fc0 100644 --- a/amplify/data/resource.ts +++ b/amplify/data/resource.ts @@ -34,13 +34,6 @@ const schema = a completedRegistration: a.boolean(), allergies: a.string(), willEatMeals: a.boolean(), - checkedIn: a - .boolean() - .default(false) - .authorization((allow) => [ - allow.ownerDefinedIn("profileOwner").to(["read"]), - allow.groups(["Admin"]).to(["read", "update", "delete", "create"]), - ]), teamId: a .id() .authorization((allow) => [ @@ -94,6 +87,12 @@ const schema = a members: a.hasMany("User", "teamId"), scores: a.hasMany("Score", "teamId"), teamRooms: a.hasMany("TeamRoom", "teamId"), + devPostLink: a + .string() + .authorization((allow) => [ + allow.groups(["Admin"]).to(["read", "update"]), + allow.authenticated().to(["read", "update"]), + ]), }) .authorization((allow) => [ allow.group("Admin").to(["read", "update", "create", "delete"]), @@ -279,21 +278,6 @@ const schema = a .authorization((allow) => [allow.group("Admin")]) .handler(a.handler.function(ResetHackathon)) .returns(a.ref("StatusCodeFunctionResponse")), - - // Custom resolvers - SetUserAsCheckedIn: a - .mutation() - .arguments({ - userId: a.string().required(), - }) - .returns(a.ref("User")) - .authorization((allow) => [allow.authenticated()]) - .handler( - a.handler.custom({ - dataSource: a.ref("User"), - entry: "./user/SetUserAsCheckedIn.js", - }), - ), }) .authorization((allow) => [ diff --git a/amplify/data/user/SetUserAsCheckedIn.js b/amplify/data/user/SetUserAsCheckedIn.js deleted file mode 100644 index e1b46d2c..00000000 --- a/amplify/data/user/SetUserAsCheckedIn.js +++ /dev/null @@ -1,14 +0,0 @@ -export function request(ctx) { - return { - operation: "UpdateItem", - key: util.dynamodb.toMapValues({ id: ctx.args.userId }), - update: { - expression: "SET checkedIn = :checkedIn", - expressionValues: { ":checkedIn": { BOOL: true } }, - }, - }; -} - -export function response(ctx) { - return ctx.result; -} diff --git a/amplify/function/BusinessLogic/CreateTeamWithCode/handler.ts b/amplify/function/BusinessLogic/CreateTeamWithCode/handler.ts index 75c916bd..a372588c 100644 --- a/amplify/function/BusinessLogic/CreateTeamWithCode/handler.ts +++ b/amplify/function/BusinessLogic/CreateTeamWithCode/handler.ts @@ -2,6 +2,7 @@ import { Amplify } from "aws-amplify"; import { generateClient } from "aws-amplify/data"; import type { AppSyncIdentityCognito } from "aws-lambda"; import type { Schema } from "../../../data/resource"; +import { tryCatch } from "../utils/try-catch"; import { createTeam, updateUser } from "./graphql/mutations"; import { getTeam } from "./graphql/queries"; @@ -37,101 +38,106 @@ const client = generateClient({ authMode: "iam", }); +const createNewTeam = (teamName: string, teamId: string) => + client.graphql({ + query: createTeam, + variables: { + input: { + name: teamName, + id: teamId, + }, + }, + }); +const updateUserTeam = (id: string, teamId: string) => + client.graphql({ + query: updateUser, + variables: { + input: { + id, + teamId, + }, + }, + }); +const generateTeamId = () => + Array.from(Array(4), () => + Math.floor(Math.random() * 36) + .toString(36) + .toUpperCase(), + ).join(""); +const getTeamFromId = (teamId: string) => + client.graphql({ + query: getTeam, + variables: { + id: teamId, + }, + }); export const handler: Schema["CreateTeamWithCode"]["functionHandler"] = async ( event, ) => { - let team = null; - let teamId: string | null = null; - try { - do { - teamId = Array.from(Array(4), () => - Math.floor(Math.random() * 36) - .toString(36) - .toUpperCase(), - ).join(""); + const { + arguments: { addCallerToTeam, teamName }, + } = event; - team = ( - await client.graphql({ - query: getTeam, - variables: { - id: teamId, - }, - }) - ).data.getTeam; - } while (team != null); + let teamId = generateTeamId(); + let { error: teamIdTaken } = await tryCatch(getTeamFromId(teamId)); + let attempts = 0; + const MAX_ATTEMPTS = 100; // Define a maximum number of attempts - const teamCreation = await client - .graphql({ - query: createTeam, - variables: { - input: { - name: event.arguments.teamName, - id: teamId, - }, - }, - }) - .catch(() => { - throw new Error( - JSON.stringify({ - body: { value: `Error creating team` }, - statusCode: 500, - headers: { "Content-Type": "application/json" }, - }), - ); - }); + while (teamIdTaken && attempts < MAX_ATTEMPTS) { + teamId = generateTeamId(); + ({ error: teamIdTaken } = await tryCatch(getTeamFromId(teamId))); + attempts++; + } - if (teamCreation) { - if (event.arguments.addCallerToTeam) { - return await client - .graphql({ - query: updateUser, - variables: { - input: { - id: (event.identity as AppSyncIdentityCognito).sub, - teamId: teamId, - }, - }, - }) - .then(() => { - return { - body: { value: teamId }, - statusCode: 200, - headers: { "Content-Type": "application/json" }, - }; - }) - .catch(() => { - throw new Error( - JSON.stringify({ - body: { value: `Error updating user (team was created)` }, - statusCode: 500, - headers: { "Content-Type": "application/json" }, - }), - ); - }); - } else { - return { - body: { value: teamId }, - statusCode: 200, - headers: { "Content-Type": "application/json" }, - }; - } - } else { - throw new Error( - JSON.stringify({ - body: { value: `Error creating team` }, - statusCode: 500, - headers: { "Content-Type": "application/json" }, - }), - ); - } - } catch (error) { - console.error(error); + if (teamIdTaken) { + // Handle the case where a unique team ID could not be generated + throw new Error( + JSON.stringify({ + body: { + value: `Failed to generate a unique team ID after ${MAX_ATTEMPTS} attempts.`, + }, + statusCode: 500, + headers: { "Content-Type": "application/json" }, + }), + ); + } + const { error: createTeamError } = await tryCatch( + createNewTeam(teamName, teamId), + ); + if (createTeamError) { throw new Error( JSON.stringify({ - body: { value: `Unhandled Internal Server Error` }, + body: { value: `Error creating team` }, statusCode: 500, headers: { "Content-Type": "application/json" }, }), ); } + if (!addCallerToTeam) { + return { + body: { value: teamId }, + statusCode: 200, + headers: { "Content-Type": "application/json" }, + }; + } + const { error: updateUserError, data: updateUserSuccess } = await tryCatch( + updateUserTeam((event.identity as AppSyncIdentityCognito).sub, teamId), + ); + if (updateUserSuccess) { + return { + body: { value: teamId }, + statusCode: 200, + headers: { "Content-Type": "application/json" }, + }; + } + + throw new Error( + JSON.stringify({ + body: { + value: `Error updating user ( ${(event.identity as AppSyncIdentityCognito).sub}) (team was created) ${updateUserError}`, + }, + statusCode: 500, + headers: { "Content-Type": "application/json" }, + }), + ); }; diff --git a/amplify/function/BusinessLogic/utils/try-catch.ts b/amplify/function/BusinessLogic/utils/try-catch.ts new file mode 100644 index 00000000..6d5f46ce --- /dev/null +++ b/amplify/function/BusinessLogic/utils/try-catch.ts @@ -0,0 +1,24 @@ +// Types for the result object with discriminated union +type Success = { + data: T; + error: null; +}; + +type Failure = { + data: null; + error: E; +}; + +type Result = Success | Failure; + +// Main wrapper function +export async function tryCatch( + promise: Promise, +): Promise> { + try { + const data = await promise; + return { data, error: null }; + } catch (error) { + return { data: null, error: error as E }; + } +} diff --git a/src/app/admin/teams/TeamTableSetup.tsx b/src/app/admin/teams/TeamTableSetup.tsx index 0e4ff409..88813abf 100644 --- a/src/app/admin/teams/TeamTableSetup.tsx +++ b/src/app/admin/teams/TeamTableSetup.tsx @@ -47,14 +47,6 @@ export const teamColumns = [ filterFn: "includesString", sortingFn: "alphanumeric", }), - columnHelper.accessor("members", { - cell: (info) => - info.getValue().every((member) => member.checkedIn) - ? "Checked In" - : "Not Checked In", - header: "Check-in Status", - sortingFn: "basic", - }), columnHelper.accessor("approved", { cell: ({ getValue, diff --git a/src/app/admin/teams/components/TeamsTablePage.tsx b/src/app/admin/teams/components/TeamsTablePage.tsx index 9c554cf3..5153b7a2 100644 --- a/src/app/admin/teams/components/TeamsTablePage.tsx +++ b/src/app/admin/teams/components/TeamsTablePage.tsx @@ -1,26 +1,20 @@ -import type { Schema } from "@/amplify/data/resource"; import client from "@/components/_Amplify/AmplifyBackendClient"; import TeamsTable from "./TeamsTable"; -type Members = Pick< - Schema["User"]["type"], - "id" | "firstName" | "lastName" | "checkedIn" ->; -export type Team = Pick & { - members: Members[]; -}; +const getTeams = client.models.Team.list({ + selectionSet: [ + "name", + "approved", + "id", + "devPostLink", + "members.id", + "members.firstName", + "members.lastName", + ], +}); +export type Team = Awaited["data"][number]; export default async function TeamsTablePage() { - const { data: teams } = await client.models.Team.list({ - selectionSet: [ - "name", - "approved", - "id", - "members.id", - "members.firstName", - "members.lastName", - "members.checkedIn", - ], - }); + const { data: teams } = await getTeams; if (!teams || !Array.isArray(teams)) return "No teams were found"; return ; } diff --git a/src/app/admin/teams/components/ViewButton.tsx b/src/app/admin/teams/components/ViewButton.tsx index c886583a..d8ae999b 100644 --- a/src/app/admin/teams/components/ViewButton.tsx +++ b/src/app/admin/teams/components/ViewButton.tsx @@ -31,9 +31,7 @@ export default function ViewButton({ team }: { team: Team }) { {`${member.firstName} ${member.lastName}`} - - {member.checkedIn ? "Checked In" : "Not Checked In"} - + {member.id} ))} diff --git a/src/app/participant/page.tsx b/src/app/participant/page.tsx index 02998236..0c64d1d8 100644 --- a/src/app/participant/page.tsx +++ b/src/app/participant/page.tsx @@ -28,7 +28,6 @@ export default function page() {
- {/* */}
diff --git a/src/app/register/team/(pending)/new/page.tsx b/src/app/register/team/(pending)/new/page.tsx index ab68277b..d7fc0b64 100644 --- a/src/app/register/team/(pending)/new/page.tsx +++ b/src/app/register/team/(pending)/new/page.tsx @@ -8,49 +8,52 @@ import { client } from "@/app/QueryProvider"; import { useUser } from "@/components/contexts/UserContext"; import PurpleButton from "@/components/PurpleButton"; import { Underline } from "@/utils/text-utils"; -import { useMutation } from "@tanstack/react-query"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; export default function page() { + const queryClient = useQueryClient(); const [teamName, setTeamName] = useState(""); const router = useRouter(); - const user = useUser(); - const toastRef = useRef(""); + const { + currentUser: { teamId, id }, + revalidateUser, + } = useUser(); const teamMutation = useMutation({ - mutationFn: async (input: string) => { - if (user.currentUser.teamId) { - throw new Error("User already has a team"); - } + mutationKey: ["CreateTeam"], + mutationFn: async (teamName: string) => { + if (teamId) throw new Error("User already has a team"); + const toastObj = toast.loading("Creating team..."); const res = await client.mutations.CreateTeamWithCode({ - teamName: input, - addCallerToTeam: true, + teamName, + addCallerToTeam: false, // broken in lambda function. }); - if (res.errors) throw new Error(res.errors[0].message); - return res.data; + toast.dismiss(toastObj); + if (!res.errors && res.data?.body) { + const teamId = JSON.parse(res.data.body!.toString()).value; + await client.models.User.update({ + id, + teamId, + }); + revalidateUser(); + return res.data; + } + throw new Error(res.errors?.[0]?.message); }, onSuccess: (data) => { - if (data?.statusCode === 200) { - toast.update(toastRef.current, { - render: "Team created successfully", + if (data.statusCode === 200) { + toast.success("Team created successfully", { type: "success", isLoading: false, autoClose: 3000, }); - const teamID = JSON.parse(data.body?.toString() || "").value; + queryClient.invalidateQueries({ queryKey: ["User"] }); + const teamID = JSON.parse(data.body!.toString()).value; router.push(`/register/team/${teamID}`); } }, - onError: (error) => { - toast.update(toastRef.current, { - render: error.message, - type: "error", - isLoading: false, - autoClose: 3000, - }); - }, }); async function handleFormSubmit(e: React.FormEvent) { e.preventDefault(); - toastRef.current = toast.loading("Creating team..."); teamMutation.mutate(teamName); } return ( @@ -77,8 +80,11 @@ export default function page() { />
- - Back + + Back {teamMutation.isPending ? "Registering..." : "Register"} diff --git a/src/components/Dashboard/DevPostLinkUpload.tsx b/src/components/Dashboard/DevPostLinkUpload.tsx new file mode 100644 index 00000000..867e0ed8 --- /dev/null +++ b/src/components/Dashboard/DevPostLinkUpload.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { useState } from "react"; +import { FaCircleArrowRight } from "react-icons/fa6"; +import { toast } from "react-toastify"; + +import { client } from "@/app/QueryProvider"; +import { useMutation, useQuery } from "@tanstack/react-query"; + +import FormInput from "../UserProfile/FormInput"; +import { useUser } from "../contexts/UserContext"; + +export default function DevPostLinkUpload() { + const { + currentUser: { team, id: userId }, + } = useUser(); + const { data: userTeam } = useQuery({ + queryKey: ["Team", userId], + queryFn: async () => { + const userTeam = await team(); + if (!userTeam || userTeam.errors) + throw new Error( + userTeam?.errors?.[0]?.message ?? "Error fetching team data", + ); + return userTeam.data; + }, + }); + const [devPostLink, setDevPostLink] = useState(""); + const devPostMutation = useMutation({ + mutationKey: ["DevPostLink"], + mutationFn: async () => { + if (!devPostLink || !userTeam) return; + const { data, errors } = await client.models.Team.update({ + id: userTeam.id, + devPostLink, + }); + if (errors) { + toast.error(errors[0].message ?? "Error updating DevPost link"); + throw new Error(errors[0].message); + } + return data; + }, + onSuccess: () => { + toast.success("DevPost link updated successfully!"); + }, + onError: () => { + toast.error("Error updating DevPost link. Please try again."); + }, + }); + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + devPostMutation.mutate(); + }; + return ( +
+ setDevPostLink(e.target.value)} + /> + + + ); +} diff --git a/src/components/Dashboard/GoToFoodTicket.tsx b/src/components/Dashboard/GoToFoodTicket.tsx index cf3806ca..0629e241 100644 --- a/src/components/Dashboard/GoToFoodTicket.tsx +++ b/src/components/Dashboard/GoToFoodTicket.tsx @@ -4,6 +4,7 @@ import { FaCircleArrowRight } from "react-icons/fa6"; import ParticipantTicketIcon from "@/images/dashboard/ParticipantTicketIcon.png"; import Card from "./Card"; +const href = "/participant/profile/food-ticket"; export default function GoToFoodTicket() { return ( diff --git a/src/components/Dashboard/SubmissionDueClock.tsx b/src/components/Dashboard/SubmissionDueClock.tsx index a3ac7480..31f35ce6 100644 --- a/src/components/Dashboard/SubmissionDueClock.tsx +++ b/src/components/Dashboard/SubmissionDueClock.tsx @@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from "react"; import { hackathonTimeRemaining } from "@/utils/date-utils"; import CountdownTimer from "../LandingPage/CountdownTimer"; import Card from "./Card"; +import DevPostLinkUpload from "./DevPostLinkUpload"; export default function SubmissionDueClock({ submissionTime, @@ -50,6 +51,7 @@ export default function SubmissionDueClock({ className="w-32 bg-dark-green md:w-52 lg:w-52" />
+ ); } diff --git a/src/components/UserProfile/FormInput.tsx b/src/components/UserProfile/FormInput.tsx index 4a0be345..1ff80332 100644 --- a/src/components/UserProfile/FormInput.tsx +++ b/src/components/UserProfile/FormInput.tsx @@ -2,6 +2,7 @@ import { twMerge } from "tailwind-merge"; export default function FormInput({ onChange, + onBlur, type = "text", name, disabled = false, @@ -9,8 +10,10 @@ export default function FormInput({ value = "", readOnly = false, label, + className, }: { onChange?: (e: React.ChangeEvent) => void; + onBlur?: (e: React.FocusEvent) => void; type?: string; name: string; disabled?: boolean; @@ -18,6 +21,7 @@ export default function FormInput({ value?: string; readOnly?: boolean; label?: React.ReactNode; + className?: string; }) { return ( <> @@ -26,10 +30,12 @@ export default function FormInput({ className={twMerge( `md:text-md my-2 rounded-full border-4 border-white bg-white/30 py-2 ps-3 text-sm text-gray-400`, !disabled && "text-black", + className, )} type={type} placeholder={placeholder} - onChange={(e: React.ChangeEvent) => onChange?.(e)} + onBlur={(e) => onBlur?.(e)} + onChange={(e) => onChange?.(e)} value={value} name={name} disabled={disabled} diff --git a/src/components/UserProfile/TeamForm.tsx b/src/components/UserProfile/TeamForm.tsx index cd5efbb4..5d0bd6d8 100644 --- a/src/components/UserProfile/TeamForm.tsx +++ b/src/components/UserProfile/TeamForm.tsx @@ -56,7 +56,6 @@ export default function TeamForm({ data, teamMutation }: TeamFormProp) { e.preventDefault(); teamMutation.mutate(); }; - if (!userTeam) return null; return ( <> @@ -73,6 +72,13 @@ export default function TeamForm({ data, teamMutation }: TeamFormProp) { value={userTeam.name} label={"Team Name"} /> +
{isFetching ? ( diff --git a/src/components/judging/ScoresTable.tsx b/src/components/judging/ScoresTable.tsx index facde86a..f4408219 100644 --- a/src/components/judging/ScoresTable.tsx +++ b/src/components/judging/ScoresTable.tsx @@ -265,7 +265,7 @@ export default function JudgingTable(props: JudgingTableProps) { scoreObject[columnId] )} Edit From 2b27b8469d91bb314a54701057872103286fec73 Mon Sep 17 00:00:00 2001 From: justin-phxm <113923596+justin-phxm@users.noreply.github.com> Date: Wed, 6 Aug 2025 19:48:15 -0600 Subject: [PATCH 3/3] updated to rebase with main --- .../Dashboard/DevPostLinkUpload.tsx | 12 +- src/components/Dashboard/GoToFoodTicket.tsx | 1 - src/components/UserProfile/FormInput.tsx | 46 ------ src/components/UserProfile/TeamForm.tsx | 136 ++++++++---------- src/components/UserProfile/TeamProfile.tsx | 11 +- src/components/UserProfile/UserForm.tsx | 13 +- src/components/UserProfile/UserProfile.tsx | 14 +- 7 files changed, 81 insertions(+), 152 deletions(-) delete mode 100644 src/components/UserProfile/FormInput.tsx diff --git a/src/components/Dashboard/DevPostLinkUpload.tsx b/src/components/Dashboard/DevPostLinkUpload.tsx index 867e0ed8..1e31f6d0 100644 --- a/src/components/Dashboard/DevPostLinkUpload.tsx +++ b/src/components/Dashboard/DevPostLinkUpload.tsx @@ -3,11 +3,8 @@ import { useState } from "react"; import { FaCircleArrowRight } from "react-icons/fa6"; import { toast } from "react-toastify"; - import { client } from "@/app/QueryProvider"; import { useMutation, useQuery } from "@tanstack/react-query"; - -import FormInput from "../UserProfile/FormInput"; import { useUser } from "../contexts/UserContext"; export default function DevPostLinkUpload() { @@ -53,13 +50,14 @@ export default function DevPostLinkUpload() { }; return (
- setDevPostLink(e.target.value)} + value={devPostLink} + name={devPostLink} /> + diff --git a/src/components/Dashboard/GoToFoodTicket.tsx b/src/components/Dashboard/GoToFoodTicket.tsx index 0629e241..cf3806ca 100644 --- a/src/components/Dashboard/GoToFoodTicket.tsx +++ b/src/components/Dashboard/GoToFoodTicket.tsx @@ -4,7 +4,6 @@ import { FaCircleArrowRight } from "react-icons/fa6"; import ParticipantTicketIcon from "@/images/dashboard/ParticipantTicketIcon.png"; import Card from "./Card"; -const href = "/participant/profile/food-ticket"; export default function GoToFoodTicket() { return ( diff --git a/src/components/UserProfile/FormInput.tsx b/src/components/UserProfile/FormInput.tsx deleted file mode 100644 index 1ff80332..00000000 --- a/src/components/UserProfile/FormInput.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { twMerge } from "tailwind-merge"; - -export default function FormInput({ - onChange, - onBlur, - type = "text", - name, - disabled = false, - placeholder, - value = "", - readOnly = false, - label, - className, -}: { - onChange?: (e: React.ChangeEvent) => void; - onBlur?: (e: React.FocusEvent) => void; - type?: string; - name: string; - disabled?: boolean; - placeholder?: string; - value?: string; - readOnly?: boolean; - label?: React.ReactNode; - className?: string; -}) { - return ( - <> - - onBlur?.(e)} - onChange={(e) => onChange?.(e)} - value={value} - name={name} - disabled={disabled} - readOnly={readOnly} - /> - - ); -} diff --git a/src/components/UserProfile/TeamForm.tsx b/src/components/UserProfile/TeamForm.tsx index 5d0bd6d8..d5f34061 100644 --- a/src/components/UserProfile/TeamForm.tsx +++ b/src/components/UserProfile/TeamForm.tsx @@ -24,91 +24,71 @@ export default function TeamForm({ data, teamMutation }: TeamFormProp) { const client = generateClient(); const { data: teamData, isFetching } = useQuery({ + initialData: null, + initialDataUpdatedAt: 0, queryKey: ["TeamWithMembers"], queryFn: async () => { - const teamWithMembers = await userTeam?.members(); - if (!teamWithMembers) throw new Error("No team members found"); + const { data: teamWithMembers } = await client.models.Team.get( + { id: data.id }, + { selectionSet: ["id", "members.*"] }, + ); - return teamWithMembers.data; + return teamWithMembers; }, - enabled: !!userTeam, + enabled: !!data, }); - const teamMutation = useMutation({ - mutationFn: async () => { - if (!userTeamId) throw new Error("No team ID found"); - const { data, errors } = await client.models.User.update({ - id: currentUser.id, - teamId: null, - }); - console.log(data); - if (errors) throw new Error("TeamID Not found" + errors[0].message); - }, - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: ["Team", userTeamId], - }); - toast.success("You have left the team"); - revalidateUser(); - refetchTeam(); - }, - }); - const handleLeaveTeamClick = (e: FormEvent) => { - e.preventDefault(); - teamMutation.mutate(); - }; - if (!userTeam) return null; + return ( <> - - - - - -
- {isFetching ? ( -

- Loading... -

- ) : ( - <> - {Array.isArray(teamData) && - teamData.map((member) => ( - - ))} - - )} -
-
- -
- + {data && ( + <> +
+ + + + + +
+ {isFetching ? ( +

Loading...

+ ) : ( + <> + {Array.isArray(teamData?.members) && + teamData?.members.map( + (member: Partial) => ( + + ), + )} + + )} +
+
+
+ +
+ + )} ); } diff --git a/src/components/UserProfile/TeamProfile.tsx b/src/components/UserProfile/TeamProfile.tsx index 7ce60d7e..a0c6b195 100644 --- a/src/components/UserProfile/TeamProfile.tsx +++ b/src/components/UserProfile/TeamProfile.tsx @@ -9,15 +9,11 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; const BUTTON_STYLES = " rounded-full border-4 border-white bg-grapefruit px-10 md:px-12 py-2 my-2 text-white"; - const TEAM_INSTRUCTION_STYLES = "bg-pink bg-white/30 mx-10 my-10 rounded-3xl border-4 border-white bg-white px-10 py-20 md:px-20 md:py-16"; - const TeamProfile = () => { const queryClient = useQueryClient(); - const userTeamId = useUser().currentUser.teamId as string; - const { data, isFetching } = useQuery({ initialData: {} as Schema["Team"]["type"], initialDataUpdatedAt: 0, @@ -26,14 +22,11 @@ const TeamProfile = () => { const teamResponse = await client.models.Team.get({ id: userTeamId, }); - if (teamResponse.errors) throw new Error(teamResponse.errors[0].message); - return teamResponse.data; }, enabled: !!userTeamId, }); - const teamMutation = useMutation({ mutationFn: async () => { try { @@ -49,7 +42,6 @@ const TeamProfile = () => { }); }, }); - return ( <> {isFetching || !userTeamId ? ( @@ -113,4 +105,5 @@ const TeamProfile = () => { )} ); -} +}; +export default TeamProfile; diff --git a/src/components/UserProfile/UserForm.tsx b/src/components/UserProfile/UserForm.tsx index b278f044..3799d7a2 100644 --- a/src/components/UserProfile/UserForm.tsx +++ b/src/components/UserProfile/UserForm.tsx @@ -2,9 +2,15 @@ import { useState } from "react"; import { useFormStatus } from "react-dom"; -import { type Schema } from "@/amplify/data/resource"; -import { type UserFormProp } from "@/components/UserProfile/UserProfile"; +import { useUser } from "../contexts/UserContext"; +interface UserFormProp { + setIsEditing: React.Dispatch>; + setEnableCancelSave: React.Dispatch>; + enableCancelSave: boolean; + isEditing: boolean; + userMutation: any; +} export default function UserForm({ userMutation, setIsEditing, @@ -58,7 +64,6 @@ export default function UserForm({ onChange={(e: React.ChangeEvent) => onChange(e)} name="lastName" value={formState.lastName ?? ""} - name={"lastName"} disabled={!isEditing} />
@@ -128,7 +133,7 @@ export default function UserForm({ ) => onChange(e)} readOnly /> diff --git a/src/components/UserProfile/UserProfile.tsx b/src/components/UserProfile/UserProfile.tsx index 02994776..0b009ea4 100644 --- a/src/components/UserProfile/UserProfile.tsx +++ b/src/components/UserProfile/UserProfile.tsx @@ -2,23 +2,23 @@ import Image from "next/image"; import { useState } from "react"; -import { type Schema } from "@/amplify/data/resource"; import { client } from "@/app/QueryProvider"; +import type { IUser } from "@/components/contexts/UserContext"; import { useUser } from "@/components/contexts/UserContext"; import KevinLoadingRing from "@/components/KevinLoadingRing"; import UserForm from "@/components/UserProfile/UserForm"; -import { useMutation, useQuery } from "@tanstack/react-query"; +import { useMutation } from "@tanstack/react-query"; export default function UserProfile() { - const { currentUser: data, isFetching: userContextIsFetching } = useUser(); + const { isFetching: userContextIsFetching } = useUser(); const userMutation = useMutation({ mutationKey: ["User"], - mutationFn: async (input: typeof data) => { + mutationFn: async (input: IUser) => { try { await client.models.User.update(input); } catch (error) { - throw new Error("Failed to update user"); + throw new Error("Failed to update user " + error); } }, }); @@ -65,10 +65,10 @@ export default function UserProfile() { />{" "}
-
+

My Details