Skip to content

Commit

Permalink
awesome scrim page
Browse files Browse the repository at this point in the history
  • Loading branch information
lowtorola committed Feb 2, 2024
1 parent d1ea5d7 commit 8aabb18
Show file tree
Hide file tree
Showing 25 changed files with 1,219 additions and 535 deletions.
13 changes: 13 additions & 0 deletions frontend2/src/api/compete/competeApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
type CompeteMatchTournamentListRequest,
type CompeteMatchListRequest,
type CompeteSubmissionTournamentListRequest,
type CompeteRequestDestroyRequest,
} from "../_autogen";
import { DEFAULT_API_CONFIGURATION, downloadFile } from "../helpers";

Expand Down Expand Up @@ -111,6 +112,18 @@ export const rejectScrimmage = async ({
await API.competeRequestRejectCreate({ episodeId, id });
};

/**
* Cancel a scrimmage request.
* @param episodeId The current episode's ID.
* @param id The scrimmage's ID to cancel.
*/
export const cancelScrimmage = async ({
episodeId,
id,
}: CompeteRequestDestroyRequest): Promise<void> => {
await API.competeRequestDestroy({ episodeId, id });
};

/**
* Get a paginated list of the currently logged in user's incoming scrimmage requests.
* @param episodeId The current episode's ID.
Expand Down
3 changes: 3 additions & 0 deletions frontend2/src/api/compete/competeKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,7 @@ export const competeMutationKeys = {

rejectScrim: ({ episodeId }: { episodeId: string }) =>
["compete", episodeId, "scrimmage", "reject"] as const,

cancelScrim: ({ episodeId }: { episodeId: string }) =>
["compete", episodeId, "scrimmage", "cancel"] as const,
};
44 changes: 41 additions & 3 deletions frontend2/src/api/compete/useCompete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
CompeteMatchTournamentListRequest,
CompeteRequestAcceptCreateRequest,
CompeteRequestCreateRequest,
CompeteRequestDestroyRequest,
CompeteRequestInboxListRequest,
CompeteRequestOutboxListRequest,
CompeteRequestRejectCreateRequest,
Expand All @@ -29,6 +30,7 @@ import type {
} from "../_autogen";
import {
acceptScrimmage,
cancelScrimmage,
getAllUserTournamentSubmissions,
getMatchesList,
getScrimmagesListByTeam,
Expand Down Expand Up @@ -403,7 +405,6 @@ export const useRequestScrimmage = (
});

// Invalidate the outbox query
// TODO: ensure correct invalidation behavior!
queryClient
.invalidateQueries({
queryKey: competeQueryKeys.outbox({ episodeId }),
Expand All @@ -425,7 +426,7 @@ export const useRequestScrimmage = (
return await toast.promise(toastFn(), {
loading: "Requesting scrimmage...",
success: "Scrimmage requested!",
error: "Error requesting scrimmage.",
error: "Error requesting scrimmage. Is the requested team eligible?",
});
},
});
Expand Down Expand Up @@ -497,7 +498,6 @@ export const useRejectScrimmage = (
await rejectScrimmage({ episodeId, id });

// Invalidate the inbox query
// TODO: ensure correct invalidation behavior!
queryClient
.invalidateQueries({
queryKey: competeQueryKeys.inbox({ episodeId }),
Expand All @@ -521,3 +521,41 @@ export const useRejectScrimmage = (
});
},
});

/**
* For cancelling a scrimmage request.
*/
export const useCancelScrimmage = (
{ episodeId }: { episodeId: string },
queryClient: QueryClient,
): UseMutationResult<void, Error, CompeteRequestDestroyRequest, unknown> =>
useMutation({
mutationKey: competeMutationKeys.cancelScrim({ episodeId }),
mutationFn: async ({ episodeId, id }: CompeteRequestDestroyRequest) => {
const toastFn = async (): Promise<void> => {
await cancelScrimmage({ episodeId, id });

// Invalidate the outbox query
queryClient
.invalidateQueries({
queryKey: competeQueryKeys.outbox({ episodeId }),
})
.catch((e) => toast.error((e as Error).message));

// Prefetch the first page of the outbox list
queryClient
.prefetchQuery({
queryKey: competeQueryKeys.outbox({ episodeId, page: 1 }),
queryFn: async () =>
await getUserScrimmagesOutboxList({ episodeId, page: 1 }),
})
.catch((e) => toast.error((e as Error).message));
};

await toast.promise(toastFn(), {
loading: "Cancelling scrimmage...",
success: "Scrimmage cancelled!",
error: "Error cancelling scrimmage.",
});
},
});
2 changes: 1 addition & 1 deletion frontend2/src/components/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const Modal: React.FC<ModalProps> = ({
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-white p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Panel className="w-full max-w-lg transform overflow-visible rounded-md bg-white p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title
as="div"
className="mb-2 text-lg font-semibold tracking-wide"
Expand Down
41 changes: 41 additions & 0 deletions frontend2/src/components/compete/MatchRatingDelta.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { type MatchParticipant } from "api/_autogen";
import React from "react";
import TeamWithRating from "./TeamWithRating";

interface RatingDeltaProps {
includeTeamName?: boolean;
participant: MatchParticipant;
ranked: boolean;
}

const MatchRatingDelta: React.FC<RatingDeltaProps> = ({
includeTeamName,
participant,
ranked,
}) => {
let newRating = 0;
if (ranked) {
newRating =
participant.rating !== null
? Math.round(participant.rating)
: Math.round(participant.old_rating);
} else {
newRating = Math.round(participant.old_rating);
}
const oldRating =
participant.old_rating !== null ? Math.round(participant.old_rating) : 0;
const ratingDelta = Math.abs(newRating - oldRating);

const includeName = includeTeamName === undefined || includeTeamName;
return (
<TeamWithRating
teamName={participant.teamname}
teamId={participant.team}
includeTeamName={includeName}
rating={newRating}
ratingDelta={ratingDelta}
/>
);
};

export default MatchRatingDelta;
45 changes: 0 additions & 45 deletions frontend2/src/components/compete/RatingDelta.tsx

This file was deleted.

62 changes: 62 additions & 0 deletions frontend2/src/components/compete/TeamWithRating.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React, { useMemo } from "react";
import { useEpisodeId } from "../../contexts/EpisodeContext";
import { NavLink } from "react-router-dom";

interface TeamWithRatingProps {
teamName: string;
teamId: number;
includeTeamName: boolean;
rating: number;
ratingDelta?: number;
}

const TeamWithRating: React.FC<TeamWithRatingProps> = ({
teamName,
teamId,
includeTeamName,
rating,
ratingDelta,
}) => {
const { episodeId } = useEpisodeId();

const ratingComponent = useMemo(() => {
if (ratingDelta !== undefined) {
const deltaClass =
ratingDelta > 0
? "text-xs font-semibold slashed-zero text-green-700"
: rating < ratingDelta
? "text-xs font-semibold slashed-zero text-red-700"
: "text-xs font-semibold slashed-zero text-gray-700";
return (
<span className={deltaClass}>
{" "}
{includeTeamName && <span>{"("}</span>}
{`${
ratingDelta > 0 ? " +" : ratingDelta < 0 ? " -" : " ±"
}${ratingDelta.toFixed(0)}`}
{includeTeamName && <span>{")"}</span>}
</span>
);
} else {
return (
<span>
{" "}
{includeTeamName && <span>{"("}</span>}
{rating.toFixed(0)}
{includeTeamName && <span>{")"}</span>}
</span>
);
}
}, [rating, ratingDelta, includeTeamName]);

return (
<>
<NavLink to={`/${episodeId}/team/${teamId}`} className="hover:underline">
{includeTeamName && <span>{teamName}</span>}
{ratingComponent}
</NavLink>
</>
);
};

export default TeamWithRating;
8 changes: 5 additions & 3 deletions frontend2/src/components/elements/DescriptiveCheckbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ export const getCheckboxState = (
};

interface DescriptiveCheckboxProps {
disabled?: boolean;
status: CheckboxState;
onChange: (checked: boolean) => void;
title: string;
description: string;
}

const DescriptiveCheckbox: React.FC<DescriptiveCheckboxProps> = ({
disabled = false,
status,
onChange,
title,
Expand All @@ -41,13 +43,13 @@ const DescriptiveCheckbox: React.FC<DescriptiveCheckboxProps> = ({
<Switch
checked={status === CheckboxState.CHECKED}
onChange={onChange}
disabled={status === CheckboxState.LOADING}
disabled={disabled || status === CheckboxState.LOADING}
className={`flex w-full
flex-row items-center justify-between gap-3 rounded-lg px-6 py-4 shadow ring-2 ring-inset
ring-cyan-600/20 transition-all ui-checked:bg-cyan-900/80 ui-checked:ring-0`}
ring-cyan-600/20 transition-all disabled:cursor-not-allowed disabled:bg-gray-200 disabled:text-gray-400 disabled:ring-gray-400 ui-checked:bg-cyan-900/80 ui-checked:ring-0`}
>
<div className="flex flex-col gap-2 text-left">
<div className="font-semibold ui-checked:text-white ">{title}</div>
<div className="font-semibold ui-checked:text-white">{title}</div>
<div className="text-sm text-cyan-700 ui-checked:text-cyan-100">
{description}
</div>
Expand Down
19 changes: 12 additions & 7 deletions frontend2/src/components/elements/Pill.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Icon from "./Icon";
interface PillProps {
text: string;
deletable?: boolean;
onDelete?: () => void;
onDelete?: (ev?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
className?: string;
}

Expand All @@ -19,12 +19,17 @@ const Pill: React.FC<PillProps> = ({
className={`gap flex max-w-max flex-row items-center justify-center gap-x-1 rounded-full bg-cyan-50 py-1 pl-3 pr-2 text-sm text-cyan-700 ring-1 ring-inset ring-cyan-600/20 ${className}`}
>
<span>{text}</span>
<button
onClick={onDelete}
className="cursor-pointer items-center rounded "
>
<Icon name="x_mark" size="xs" />
</button>
{deletable && (
<button
onClick={(ev) => {
onDelete?.(ev);
ev.stopPropagation();
}}
className="cursor-pointer items-center rounded "
>
<Icon name="x_mark" size="xs" />
</button>
)}
</div>
);
};
Expand Down
Loading

0 comments on commit 8aabb18

Please sign in to comment.