Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
14 changes: 14 additions & 0 deletions api/src/team/team.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
4 changes: 4 additions & 0 deletions api/src/team/team.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TeamEntity> = {}): Promise<TeamEntity[]> {
return this.teamRepository.find({
where: {
Expand Down
8 changes: 8 additions & 0 deletions frontend/app/actions/team.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ export async function joinQueue(
);
}

export async function leaveQueue(
eventId: string,
): Promise<ServerActionResponse<void>> {
return await handleError(
axiosInstance.delete(`team/event/${eventId}/queue/join`),
);
}

export async function getTeamById(teamId: string): Promise<Team | null> {
const team = (await axiosInstance.get(`team/${teamId}`)).data;

Expand Down
179 changes: 106 additions & 73 deletions frontend/app/events/[id]/queue/queueState.tsx
Original file line number Diff line number Diff line change
@@ -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();

Expand All @@ -25,79 +25,112 @@ export default function QueueState(props: {
const { id } = useParams();
const eventId = id as string;

useEffect(() => {
const intervalRef = useRef<NodeJS.Timeout | null>(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 (
<div className="flex flex-col items-center justify-center gap-4 py-8 md:py-10">
<h1 className="text-2xl font-bold">Queue State</h1>
<div className="mt-4 flex flex-col items-center justify-center gap-2">
{queueState?.match?.state === MatchState.IN_PROGRESS ? (
<Spinner color="success" />
) : (
<>
<p className="text-lg">Team: {props.team.name}</p>
<p
className={cn(
"text-sm text-default-500",
queueState.inQueue ? "text-green-500" : "",
)}
>
Status: {queueState.inQueue ? "In Queue" : "Not in Queue"}
</p>
{!queueState.inQueue ? (
<Button
isDisabled={joiningQueue}
onPress={() => {
setJoiningQueue(true);
plausible("join_queue");
joinQueue(props.eventId)
.then(() => {
setQueueState({
...queueState,
inQueue: true,
queueCount: queueState.queueCount + 1,
});
})
.finally(() => {
setJoiningQueue(false);
});
}}
color="success"
>
play
</Button>
) : (
<>
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 (
<div className="flex flex-col items-center justify-center gap-4 py-8 md:py-10">
<h1 className="text-2xl font-bold">Queue State</h1>

<div className="mt-4 flex flex-col items-center justify-center gap-2">
{queueState?.match?.state === MatchState.IN_PROGRESS ? (
<Spinner color="success" />
) : (
<>
<p className="text-lg">Team: {props.team.name}</p>
<p
className={cn(
"text-sm text-default-500",
queueState.inQueue ? "text-green-500" : ""
)}
>
Status: {queueState.inQueue ? "In Queue" : "Not in Queue"}
</p>

<Button
isDisabled={joiningQueue}
onPress={handleToggleQueue}
color={queueState.inQueue ? "danger" : "success"}
>
{queueState.inQueue ? "Leave Queue" : "Join Queue"}
</Button>

{queueState.inQueue && (
<p className="text-sm">Queue Count: {queueState.queueCount}</p>
</>
)}
</>
)}
</div>

<div className="mt-8 w-full max-w-2xl">
<h2 className="mb-4 text-xl font-semibold">Past Matches</h2>
<QueueMatchesList
eventId={props.eventId}
matches={props.queueMatches}
/>
</div>
</div>
);
<div className="mt-8 w-full max-w-2xl">
<h2 className="mb-4 text-xl font-semibold">Past Matches</h2>
<QueueMatchesList eventId={eventId} matches={props.queueMatches} />
</div>
</div>
);
}