predictions
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
import { createServerFn } from "@tanstack/react-start";
|
||||
import { z } from "zod";
|
||||
import { pbAdmin } from "@/lib/pocketbase/client";
|
||||
import { logger } from "@/lib/logger";
|
||||
import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result";
|
||||
import { superTokensFunctionMiddleware } from "@/utils/supertokens";
|
||||
import { serverFnLoggingMiddleware } from "@/utils/activities";
|
||||
import { Tournament } from "@/features/tournaments/types";
|
||||
import {
|
||||
MyPrediction,
|
||||
Prediction,
|
||||
PredictionLeaderboardEntry,
|
||||
PredictionsLeaderboard,
|
||||
} from "./types";
|
||||
import {
|
||||
computePredictionScore,
|
||||
getPickableMatches,
|
||||
isPredictionComplete,
|
||||
isPredictionLocked,
|
||||
isTournamentPredictable,
|
||||
} from "./utils";
|
||||
|
||||
const getTournamentOrThrow = async (tournamentId: string): Promise<Tournament> => {
|
||||
const tournament = await pbAdmin.getTournament(tournamentId);
|
||||
if (!tournament) {
|
||||
throw new Error("Tournament not found");
|
||||
}
|
||||
return tournament;
|
||||
};
|
||||
|
||||
export const getMyPrediction = createServerFn()
|
||||
.validator(z.string())
|
||||
.middleware([superTokensFunctionMiddleware])
|
||||
.handler(async ({ data: tournamentId, context }) =>
|
||||
toServerResult(async (): Promise<MyPrediction> => {
|
||||
const tournament = await getTournamentOrThrow(tournamentId);
|
||||
const matches = tournament.matches || [];
|
||||
|
||||
const player = await pbAdmin.getPlayerByAuthId(context.userAuthId);
|
||||
const prediction = player
|
||||
? await pbAdmin.getPrediction(tournamentId, player.id)
|
||||
: null;
|
||||
|
||||
return {
|
||||
prediction,
|
||||
locked: isPredictionLocked(matches),
|
||||
eligible: isTournamentPredictable(tournament),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const submitPredictionSchema = z.object({
|
||||
tournamentId: z.string(),
|
||||
picks: z.record(z.string(), z.string()),
|
||||
});
|
||||
|
||||
export const submitPrediction = createServerFn()
|
||||
.validator(submitPredictionSchema)
|
||||
.middleware([superTokensFunctionMiddleware, serverFnLoggingMiddleware])
|
||||
.handler(async ({ data: { tournamentId, picks }, context }) =>
|
||||
toServerResult(async (): Promise<Prediction> => {
|
||||
const player = await pbAdmin.getPlayerByAuthId(context.userAuthId);
|
||||
if (!player) {
|
||||
throw new Error("Player not found");
|
||||
}
|
||||
|
||||
const tournament = await getTournamentOrThrow(tournamentId);
|
||||
const matches = tournament.matches || [];
|
||||
|
||||
if (!isTournamentPredictable(tournament)) {
|
||||
throw new Error("Predictions are not available for this tournament");
|
||||
}
|
||||
|
||||
if (isPredictionLocked(matches)) {
|
||||
throw new Error("Predictions are locked — the tournament has started");
|
||||
}
|
||||
|
||||
const pickableLids = new Set(
|
||||
getPickableMatches(matches, picks).map((match) => String(match.lid))
|
||||
);
|
||||
const pickKeys = Object.keys(picks);
|
||||
if (
|
||||
pickKeys.length !== pickableLids.size ||
|
||||
pickKeys.some((lid) => !pickableLids.has(lid))
|
||||
) {
|
||||
throw new Error("Prediction must include a pick for every match");
|
||||
}
|
||||
|
||||
if (!isPredictionComplete(matches, picks)) {
|
||||
throw new Error("Prediction contains invalid picks");
|
||||
}
|
||||
|
||||
const prediction = await pbAdmin.upsertPrediction(
|
||||
tournamentId,
|
||||
player.id,
|
||||
picks
|
||||
);
|
||||
|
||||
logger.info("Prediction submitted", {
|
||||
tournamentId,
|
||||
playerId: player.id,
|
||||
pickCount: pickKeys.length,
|
||||
});
|
||||
|
||||
return prediction;
|
||||
})
|
||||
);
|
||||
|
||||
export const getPredictionsLeaderboard = createServerFn()
|
||||
.validator(z.string())
|
||||
.middleware([superTokensFunctionMiddleware])
|
||||
.handler(async ({ data: tournamentId }) =>
|
||||
toServerResult(async (): Promise<PredictionsLeaderboard> => {
|
||||
const tournament = await getTournamentOrThrow(tournamentId);
|
||||
const matches = tournament.matches || [];
|
||||
const locked = isPredictionLocked(matches);
|
||||
|
||||
const predictions = await pbAdmin.getPredictionsForTournament(tournamentId);
|
||||
const submitters = predictions.map((prediction) => prediction.player);
|
||||
|
||||
if (!locked) {
|
||||
return { locked, count: predictions.length, entries: [], submitters };
|
||||
}
|
||||
|
||||
const entries: PredictionLeaderboardEntry[] = predictions.map(
|
||||
(prediction) => {
|
||||
const score = computePredictionScore(matches, prediction.picks);
|
||||
const championPick = tournament.teams?.find(
|
||||
(team) => team.id === score.predictedChampionId
|
||||
);
|
||||
|
||||
return {
|
||||
player: prediction.player,
|
||||
points: score.points,
|
||||
correct: score.correct,
|
||||
total: score.total,
|
||||
championPick,
|
||||
championCorrect:
|
||||
!!score.predictedChampionId &&
|
||||
score.predictedChampionId === tournament.first_place?.id,
|
||||
updated: prediction.updated,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
entries.sort(
|
||||
(a, b) =>
|
||||
b.points - a.points ||
|
||||
Number(b.championCorrect) - Number(a.championCorrect) ||
|
||||
a.updated.localeCompare(b.updated) ||
|
||||
(a.player.first_name ?? "").localeCompare(b.player.first_name ?? "")
|
||||
);
|
||||
|
||||
return { locked, count: entries.length, entries, submitters };
|
||||
})
|
||||
);
|
||||
|
||||
const playerPredictionSchema = z.object({
|
||||
tournamentId: z.string(),
|
||||
playerId: z.string(),
|
||||
});
|
||||
|
||||
export const getPlayerPrediction = createServerFn()
|
||||
.validator(playerPredictionSchema)
|
||||
.middleware([superTokensFunctionMiddleware])
|
||||
.handler(async ({ data: { tournamentId, playerId }, context }) =>
|
||||
toServerResult(async (): Promise<Prediction | null> => {
|
||||
const tournament = await getTournamentOrThrow(tournamentId);
|
||||
|
||||
if (!isPredictionLocked(tournament.matches || [])) {
|
||||
const me = await pbAdmin.getPlayerByAuthId(context.userAuthId);
|
||||
if (me?.id !== playerId) {
|
||||
throw new Error("Predictions are private until the tournament starts");
|
||||
}
|
||||
}
|
||||
|
||||
return pbAdmin.getPrediction(tournamentId, playerId);
|
||||
})
|
||||
);
|
||||
Reference in New Issue
Block a user