diff --git a/api/src/team/team.controller.ts b/api/src/team/team.controller.ts index 3b1445ec..ba1e96f0 100644 --- a/api/src/team/team.controller.ts +++ b/api/src/team/team.controller.ts @@ -240,6 +240,20 @@ export class TeamController { return this.teamService.joinQueue(team.id); } + @UseGuards(UserGuard) + @Delete("event/:eventId/queue/join") + async leaveQueue( + @UserId() userId: string, + @Param("eventId", new ParseUUIDPipe()) eventId: string, + ) { + const team = await this.teamService.getTeamOfUserForEvent(eventId, userId); + if (!team) throw new NotFoundException("You are not part of a team for this event."); + if (team.locked) throw new BadRequestException("You cannot leave the queue with a locked team."); + if (!team.inQueue) throw new BadRequestException("You already left the queue."); + + return this.teamService.leaveQueue(team.id); + } + @UseGuards(UserGuard) @Get("event/:eventId/queue/state") async getQueueState( diff --git a/api/src/team/team.service.ts b/api/src/team/team.service.ts index 180098c6..3028509e 100644 --- a/api/src/team/team.service.ts +++ b/api/src/team/team.service.ts @@ -258,6 +258,10 @@ export class TeamService { return this.teamRepository.update(teamId, {inQueue: true}); } + async leaveQueue(teamId: string) { + return this.teamRepository.update(teamId, {inQueue: false}); + } + async getTeamsForEvent(eventId: string, relations: FindOptionsRelations = {}): Promise { return this.teamRepository.find({ where: { diff --git a/frontend/app/actions/team.ts b/frontend/app/actions/team.ts index 05566fe0..0673e806 100644 --- a/frontend/app/actions/team.ts +++ b/frontend/app/actions/team.ts @@ -61,6 +61,14 @@ export async function joinQueue( ); } +export async function leaveQueue( + eventId: string, +): Promise> { + return await handleError( + axiosInstance.delete(`team/event/${eventId}/queue/join`), + ); +} + export async function getTeamById(teamId: string): Promise { const team = (await axiosInstance.get(`team/${teamId}`)).data; diff --git a/frontend/app/events/[id]/queue/queueState.tsx b/frontend/app/events/[id]/queue/queueState.tsx index 7468aade..243e6b1e 100644 --- a/frontend/app/events/[id]/queue/queueState.tsx +++ b/frontend/app/events/[id]/queue/queueState.tsx @@ -1,20 +1,20 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { Button, cn } from "@heroui/react"; // @ts-ignore import { QueueState } from "@/app/actions/team.model"; -import { getQueueState, joinQueue, Team } from "@/app/actions/team"; -import { Match, MatchState } from "@/app/actions/tournament-model"; +import { getQueueState, joinQueue, leaveQueue, Team } from "@/app/actions/team"; +import { MatchState, Match } from "@/app/actions/tournament-model"; import { Spinner } from "@heroui/spinner"; import QueueMatchesList from "@/components/QueueMatchesList"; import { useParams, useRouter } from "next/navigation"; import { usePlausible } from "next-plausible"; export default function QueueState(props: { - queueState: QueueState; - eventId: string; - team: Team; - queueMatches: Match[]; + queueState: QueueState; + eventId: string; + team: Team; + queueMatches: Match[]; }) { const plausible = usePlausible(); @@ -25,79 +25,112 @@ export default function QueueState(props: { const { id } = useParams(); const eventId = id as string; - useEffect(() => { + const intervalRef = useRef(null); + const stateRef = useRef(queueState); + stateRef.current = queueState; + + const shouldPoll = (s: QueueState) => + !!s.inQueue || s.match?.state === MatchState.IN_PROGRESS; + + const startPolling = () => { + if (intervalRef.current) return; + intervalRef.current = setInterval(fetchQueueState, 600); + }; + + const stopPolling = () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + }; + async function fetchQueueState() { - const newQueueState = await getQueueState(props.eventId); - if ( - queueState.match?.state === MatchState.IN_PROGRESS && - newQueueState.match?.state !== MatchState.IN_PROGRESS - ) { - if (newQueueState.match) { - router.push(`/events/${eventId}/match/${newQueueState?.match?.id}`); + const newQueueState = await getQueueState(eventId); + const prev = stateRef.current; + const wasInProgress = prev.match?.state === MatchState.IN_PROGRESS; + const isInProgress = newQueueState.match?.state === MatchState.IN_PROGRESS; + + if (wasInProgress && !isInProgress && newQueueState.match) { + router.push(`/events/${eventId}/match/${newQueueState.match.id}`); } - } - setQueueState(newQueueState); + + setQueueState(newQueueState); } - const interval = setInterval(fetchQueueState, 600); - return () => clearInterval(interval); - }); - - return ( -
-

Queue State

-
- {queueState?.match?.state === MatchState.IN_PROGRESS ? ( - - ) : ( - <> -

Team: {props.team.name}

-

- Status: {queueState.inQueue ? "In Queue" : "Not in Queue"} -

- {!queueState.inQueue ? ( - - ) : ( - <> + useEffect(() => { + if (shouldPoll(queueState)) startPolling(); + else stopPolling(); + + return () => stopPolling(); + }, [queueState.inQueue, queueState.match?.state]); + + useEffect(() => { + setQueueState(props.queueState); + }, [props.queueState]); + + const handleToggleQueue = async () => { + setJoiningQueue(true); + + if (!queueState.inQueue) { + plausible("join_queue"); + await joinQueue(eventId); + setQueueState((s) => ({ + ...s, + inQueue: true, + queueCount: s.queueCount + 1, + })); + } else { + plausible("leave_queue"); + if (typeof leaveQueue === "function") { + await leaveQueue(eventId); + } + setQueueState((s) => ({ + ...s, + inQueue: false, + queueCount: Math.max(s.queueCount - 1, 0), + })); + } + + setJoiningQueue(false); + }; + + return ( +
+

Queue State

+ +
+ {queueState?.match?.state === MatchState.IN_PROGRESS ? ( + + ) : ( + <> +

Team: {props.team.name}

+

+ Status: {queueState.inQueue ? "In Queue" : "Not in Queue"} +

+ + + + {queueState.inQueue && (

Queue Count: {queueState.queueCount}

- )} )}
- -
-

Past Matches

- -
-
- ); +
+

Past Matches

+ +
+
+ ); }