predictions
This commit is contained in:
@@ -4,11 +4,12 @@ import {
|
||||
useTournament,
|
||||
} from "@/features/tournaments/queries";
|
||||
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
||||
import { Box, Container, Flex, Skeleton, Stack } from "@mantine/core";
|
||||
import { Container } from "@mantine/core";
|
||||
import { useMemo } from "react";
|
||||
import { BracketData } from "@/features/bracket/types";
|
||||
import { Match } from "@/features/matches/types";
|
||||
import { groupMatchesIntoBracket } from "@/features/bracket/utils/group";
|
||||
import BracketView from "@/features/bracket/components/bracket-view";
|
||||
import { BracketPending } from "@/features/bracket/components/bracket-pending";
|
||||
|
||||
export const Route = createFileRoute("/_authed/tournaments/$id/bracket")({
|
||||
beforeLoad: async ({ context, params }) => {
|
||||
@@ -34,84 +35,14 @@ export const Route = createFileRoute("/_authed/tournaments/$id/bracket")({
|
||||
pendingComponent: BracketPending,
|
||||
});
|
||||
|
||||
function BracketPending() {
|
||||
const columns = [4, 2, 1];
|
||||
|
||||
return (
|
||||
<Container size="md" px={0}>
|
||||
<Box
|
||||
p={0}
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
backgroundImage: `radial-gradient(circle, var(--mantine-color-default-border) 1px, transparent 1px)`,
|
||||
backgroundSize: "16px 16px",
|
||||
backgroundPosition: "0 0, 8px 8px",
|
||||
minHeight: "70dvh",
|
||||
}}
|
||||
>
|
||||
<Skeleton height={18} width={140} radius="sm" m={16} />
|
||||
<Flex gap="xl" px={16} align="stretch">
|
||||
{columns.map((count, columnIndex) => (
|
||||
<Stack
|
||||
key={`bracket-pending-round-${columnIndex}`}
|
||||
gap="xl"
|
||||
justify="space-around"
|
||||
style={{ opacity: 1 - columnIndex * 0.25 }}
|
||||
>
|
||||
{Array.from({ length: count }).map((_, matchIndex) => (
|
||||
<Skeleton
|
||||
key={`bracket-pending-match-${columnIndex}-${matchIndex}`}
|
||||
height={84}
|
||||
width={220}
|
||||
radius="md"
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
))}
|
||||
</Flex>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
function RouteComponent() {
|
||||
const { id } = Route.useParams();
|
||||
const { data: tournament } = useTournament(id);
|
||||
|
||||
const bracket: BracketData = useMemo(() => {
|
||||
if (!tournament.matches || tournament.matches.length === 0) {
|
||||
return { winners: [], losers: [] };
|
||||
}
|
||||
|
||||
const winnersMap = new Map<number, Match[]>();
|
||||
const losersMap = new Map<number, Match[]>();
|
||||
|
||||
tournament.matches
|
||||
.filter((match) => match.round !== -1)
|
||||
.sort((a, b) => a.lid - b.lid)
|
||||
.forEach((match) => {
|
||||
if (!match.is_losers_bracket) {
|
||||
if (!winnersMap.has(match.round)) {
|
||||
winnersMap.set(match.round, []);
|
||||
}
|
||||
winnersMap.get(match.round)!.push(match);
|
||||
} else {
|
||||
if (!losersMap.has(match.round)) {
|
||||
losersMap.set(match.round, []);
|
||||
}
|
||||
losersMap.get(match.round)!.push(match);
|
||||
}
|
||||
});
|
||||
|
||||
const winners = Array.from(winnersMap.entries())
|
||||
.sort(([a], [b]) => a - b)
|
||||
.map(([, matches]) => matches);
|
||||
|
||||
const losers = Array.from(losersMap.entries())
|
||||
.sort(([a], [b]) => a - b)
|
||||
.map(([, matches]) => matches);
|
||||
return { winners, losers };
|
||||
}, [tournament.matches]);
|
||||
const bracket: BracketData = useMemo(
|
||||
() => groupMatchesIntoBracket(tournament.matches),
|
||||
[tournament.matches]
|
||||
);
|
||||
|
||||
return (
|
||||
<Container size="md" px={0}>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
import { tournamentQueries, useTournament } from "@/features/tournaments/queries";
|
||||
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
||||
import { Container } from "@mantine/core";
|
||||
import { PredictionLeaderboard } from "@/features/predictions/components/prediction-leaderboard";
|
||||
|
||||
export const Route = createFileRoute("/_authed/tournaments/$id/predictions")({
|
||||
beforeLoad: async ({ context, params }) => {
|
||||
const { queryClient } = context;
|
||||
const tournament = await ensureServerQueryData(
|
||||
queryClient,
|
||||
tournamentQueries.details(params.id)
|
||||
);
|
||||
if (!tournament) throw redirect({ to: "/tournaments" });
|
||||
return {
|
||||
tournament,
|
||||
};
|
||||
},
|
||||
loader: () => ({
|
||||
header: {
|
||||
withBackButton: true,
|
||||
title: "Predictions",
|
||||
},
|
||||
}),
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const { id } = Route.useParams();
|
||||
const { data: tournament } = useTournament(id);
|
||||
|
||||
return (
|
||||
<Container size="md" px={0}>
|
||||
<PredictionLeaderboard tournament={tournament} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
import { useMemo } from "react";
|
||||
import { Box, Container, Group, Paper, Stack, Text } from "@mantine/core";
|
||||
import { tournamentQueries, useTournament } from "@/features/tournaments/queries";
|
||||
import { predictionQueries, usePlayerPrediction } from "@/features/predictions/queries";
|
||||
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
||||
import { BracketPending } from "@/features/bracket/components/bracket-pending";
|
||||
import { PredictionBracket } from "@/features/predictions/components/prediction-bracket";
|
||||
import { computePredictionScore } from "@/features/predictions/utils";
|
||||
import PlayerAvatar from "@/components/player-avatar";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authed/tournaments/$id/predictions_/$playerId"
|
||||
)({
|
||||
beforeLoad: async ({ context, params }) => {
|
||||
const { queryClient } = context;
|
||||
const tournament = await ensureServerQueryData(
|
||||
queryClient,
|
||||
tournamentQueries.details(params.id)
|
||||
);
|
||||
if (!tournament) throw redirect({ to: "/tournaments" });
|
||||
|
||||
const prediction = await ensureServerQueryData(
|
||||
queryClient,
|
||||
predictionQueries.player(params.id, params.playerId)
|
||||
);
|
||||
if (!prediction) {
|
||||
throw redirect({
|
||||
to: "/tournaments/$id/predictions",
|
||||
params: { id: params.id },
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
tournament,
|
||||
prediction,
|
||||
};
|
||||
},
|
||||
loader: ({ context }) => ({
|
||||
fullWidth: true,
|
||||
withPadding: false,
|
||||
header: {
|
||||
withBackButton: true,
|
||||
title: `${context.prediction.player.first_name}'s Bracket`,
|
||||
},
|
||||
}),
|
||||
component: RouteComponent,
|
||||
pendingComponent: BracketPending,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const { id, playerId } = Route.useParams();
|
||||
const { data: tournament } = useTournament(id);
|
||||
const { data: prediction } = usePlayerPrediction(id, playerId);
|
||||
|
||||
const matches = tournament.matches || [];
|
||||
const picks = prediction?.picks ?? {};
|
||||
|
||||
const score = useMemo(
|
||||
() => computePredictionScore(matches, picks),
|
||||
[matches, picks]
|
||||
);
|
||||
|
||||
return (
|
||||
<Container size="md" px={0}>
|
||||
<Box pos="relative">
|
||||
<PredictionBracket
|
||||
matches={matches}
|
||||
picks={picks}
|
||||
mode="view"
|
||||
perMatch={score.perMatch}
|
||||
/>
|
||||
|
||||
<Box
|
||||
pos="absolute"
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
p="md"
|
||||
style={{ zIndex: 2, pointerEvents: "none" }}
|
||||
>
|
||||
<Paper
|
||||
withBorder
|
||||
shadow="md"
|
||||
radius="lg"
|
||||
p="sm"
|
||||
style={{ pointerEvents: "auto" }}
|
||||
>
|
||||
<Group justify="space-between" align="center" wrap="nowrap">
|
||||
<Group gap="sm" align="center" wrap="nowrap">
|
||||
<PlayerAvatar
|
||||
name={`${prediction?.player.first_name} ${prediction?.player.last_name}`}
|
||||
size={32}
|
||||
disableFullscreen
|
||||
/>
|
||||
<Text size="sm" fw={600} lineClamp={1}>
|
||||
{prediction?.player.first_name} {prediction?.player.last_name}
|
||||
</Text>
|
||||
</Group>
|
||||
<Group gap="md" wrap="nowrap">
|
||||
<Stack gap={0} ta="center">
|
||||
<Text size="xs" c="dimmed" fw={700}>
|
||||
PTS
|
||||
</Text>
|
||||
<Text size="sm" fw={700}>
|
||||
{score.points}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack gap={0} ta="center">
|
||||
<Text size="xs" c="dimmed" fw={700}>
|
||||
PICKS
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{score.correct}/{score.total}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Group>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
import { tournamentQueries, useTournament } from "@/features/tournaments/queries";
|
||||
import { predictionQueries, useMyPrediction } from "@/features/predictions/queries";
|
||||
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
||||
import { Container } from "@mantine/core";
|
||||
import { BracketPending } from "@/features/bracket/components/bracket-pending";
|
||||
import { PredictionEditor } from "@/features/predictions/components/prediction-editor";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authed/tournaments/$id/predictions_/make"
|
||||
)({
|
||||
beforeLoad: async ({ context, params }) => {
|
||||
const { queryClient } = context;
|
||||
const tournament = await ensureServerQueryData(
|
||||
queryClient,
|
||||
tournamentQueries.details(params.id)
|
||||
);
|
||||
if (!tournament) throw redirect({ to: "/tournaments" });
|
||||
|
||||
const myPrediction = await ensureServerQueryData(
|
||||
queryClient,
|
||||
predictionQueries.mine(params.id)
|
||||
);
|
||||
if (!myPrediction.eligible || myPrediction.locked) {
|
||||
throw redirect({
|
||||
to: "/tournaments/$id/predictions",
|
||||
params: { id: params.id },
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
tournament,
|
||||
};
|
||||
},
|
||||
loader: ({ context }) => ({
|
||||
fullWidth: true,
|
||||
withPadding: false,
|
||||
header: {
|
||||
withBackButton: true,
|
||||
title: `${context.tournament.name}`,
|
||||
},
|
||||
}),
|
||||
component: RouteComponent,
|
||||
pendingComponent: BracketPending,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const { id } = Route.useParams();
|
||||
const { data: tournament } = useTournament(id);
|
||||
const { data: myPrediction } = useMyPrediction(id);
|
||||
|
||||
return (
|
||||
<Container size="md" px={0}>
|
||||
<PredictionEditor
|
||||
tournament={tournament}
|
||||
initialPicks={myPrediction.prediction?.picks ?? {}}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user