From 0d20b6317d791dfd09b5bbe213daac70a96d7c3d Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Tue, 15 Jul 2025 19:22:33 +0200 Subject: [PATCH] feat: #115 Hide match result battle plans --- convex/_generated/api.d.ts | 2 + .../fowV4/calculateFowV4MatchResultScore.ts | 3 +- .../fowV4/extractFowV4MatchResultBaseStats.ts | 3 +- .../checkMatchResultBattlePlanVisibility.ts | 55 +++++++++++++++ .../_helpers/deepenMatchResult.ts | 11 ++- .../matchResults/queries/getMatchResults.ts | 2 +- .../queries/getMatchResultsByTournament.ts | 1 + .../getMatchResultsByTournamentPairing.ts | 1 + .../getMatchResultsByTournamentRound.ts | 1 + .../FowV4MatchResultDetails.utils.ts | 4 +- .../components/MatchResultDetails.module.scss | 65 ----------------- .../components/MatchResultDetails.tsx | 69 ------------------- .../components/MatchResultDetails.utils.ts | 19 ----- 13 files changed, 77 insertions(+), 159 deletions(-) create mode 100644 convex/_model/matchResults/_helpers/checkMatchResultBattlePlanVisibility.ts delete mode 100644 src/pages/MatchResultDetailPage/components/MatchResultDetails.module.scss delete mode 100644 src/pages/MatchResultDetailPage/components/MatchResultDetails.tsx delete mode 100644 src/pages/MatchResultDetailPage/components/MatchResultDetails.utils.ts diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index fe317253..0e141492 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -53,6 +53,7 @@ import type * as _model_matchResultLikes_queries_getMatchResultLike from "../_mo import type * as _model_matchResultLikes_queries_getMatchResultLikesByMatchResult from "../_model/matchResultLikes/queries/getMatchResultLikesByMatchResult.js"; import type * as _model_matchResultLikes_queries_getMatchResultLikesByUser from "../_model/matchResultLikes/queries/getMatchResultLikesByUser.js"; import type * as _model_matchResults__helpers_checkMatchResultAuth from "../_model/matchResults/_helpers/checkMatchResultAuth.js"; +import type * as _model_matchResults__helpers_checkMatchResultBattlePlanVisibility from "../_model/matchResults/_helpers/checkMatchResultBattlePlanVisibility.js"; import type * as _model_matchResults__helpers_deepenMatchResult from "../_model/matchResults/_helpers/deepenMatchResult.js"; import type * as _model_matchResults__helpers_getShallowMatchResult from "../_model/matchResults/_helpers/getShallowMatchResult.js"; import type * as _model_matchResults_fields from "../_model/matchResults/fields.js"; @@ -240,6 +241,7 @@ declare const fullApi: ApiFromModules<{ "_model/matchResultLikes/queries/getMatchResultLikesByMatchResult": typeof _model_matchResultLikes_queries_getMatchResultLikesByMatchResult; "_model/matchResultLikes/queries/getMatchResultLikesByUser": typeof _model_matchResultLikes_queries_getMatchResultLikesByUser; "_model/matchResults/_helpers/checkMatchResultAuth": typeof _model_matchResults__helpers_checkMatchResultAuth; + "_model/matchResults/_helpers/checkMatchResultBattlePlanVisibility": typeof _model_matchResults__helpers_checkMatchResultBattlePlanVisibility; "_model/matchResults/_helpers/deepenMatchResult": typeof _model_matchResults__helpers_deepenMatchResult; "_model/matchResults/_helpers/getShallowMatchResult": typeof _model_matchResults__helpers_getShallowMatchResult; "_model/matchResults/fields": typeof _model_matchResults_fields; diff --git a/convex/_model/fowV4/calculateFowV4MatchResultScore.ts b/convex/_model/fowV4/calculateFowV4MatchResultScore.ts index 74c2517c..21d695ba 100644 --- a/convex/_model/fowV4/calculateFowV4MatchResultScore.ts +++ b/convex/_model/fowV4/calculateFowV4MatchResultScore.ts @@ -1,4 +1,5 @@ import { Doc } from '../../_generated/dataModel'; +import { DeepMatchResult } from '../matchResults'; /** * Calculate the Victory Points (i.e. score) for a given match result. @@ -9,7 +10,7 @@ import { Doc } from '../../_generated/dataModel'; * @param matchResult - The match result to score * @returns - A tuple with the scores for player 0 and 1 respectively */ -export const calculateFowV4MatchResultScore = (matchResult: Doc<'matchResults'>): [number, number] => { +export const calculateFowV4MatchResultScore = (matchResult: Doc<'matchResults'> | DeepMatchResult): [number, number] => { // TODO: Add some guards in case matchResult is not FowV4 diff --git a/convex/_model/fowV4/extractFowV4MatchResultBaseStats.ts b/convex/_model/fowV4/extractFowV4MatchResultBaseStats.ts index 7b89c6cb..23e6f08a 100644 --- a/convex/_model/fowV4/extractFowV4MatchResultBaseStats.ts +++ b/convex/_model/fowV4/extractFowV4MatchResultBaseStats.ts @@ -1,4 +1,5 @@ import { Doc } from '../../_generated/dataModel'; +import { DeepMatchResult } from '../matchResults'; import { calculateFowV4MatchResultScore } from './calculateFowV4MatchResultScore'; import { FowV4BaseStats } from './types'; @@ -9,7 +10,7 @@ import { FowV4BaseStats } from './types'; * @returns */ -export const extractFowV4MatchResultBaseStats = (matchResult: Doc<'matchResults'>): [FowV4BaseStats, FowV4BaseStats] => { +export const extractFowV4MatchResultBaseStats = (matchResult: Doc<'matchResults'> | DeepMatchResult): [FowV4BaseStats, FowV4BaseStats] => { const score = calculateFowV4MatchResultScore(matchResult); return [ { diff --git a/convex/_model/matchResults/_helpers/checkMatchResultBattlePlanVisibility.ts b/convex/_model/matchResults/_helpers/checkMatchResultBattlePlanVisibility.ts new file mode 100644 index 00000000..3b31c6b8 --- /dev/null +++ b/convex/_model/matchResults/_helpers/checkMatchResultBattlePlanVisibility.ts @@ -0,0 +1,55 @@ +import { getAuthUserId } from '@convex-dev/auth/server'; + +import { Doc } from '../../../_generated/dataModel'; +import { QueryCtx } from '../../../_generated/server'; +import { getTournamentShallow } from '../../../_model/tournaments'; +import { deepenTournamentPairing } from '../../tournamentPairings'; + +/** + * Checks if a match result's battle plans should be visible or not. + * + * @param ctx - Convex query context + * @param matchResult - Raw match result document + * @returns True if the battle plans should be visible, false if not + */ +export const checkMatchResultBattlePlanVisibility = async ( + ctx: QueryCtx, + matchResult: Doc<'matchResults'>, +): Promise => { + const userId = await getAuthUserId(ctx); + + // If the match result doesn't belong to a tournament pairing, battle plans should be visible: + if (!matchResult?.tournamentPairingId) { + return true; + } + + const tournamentPairing = await ctx.db.get(matchResult.tournamentPairingId); + + // If the match result's pairing has gone missing, treat it the same as a single match: + if (!tournamentPairing) { + return true; + } + const deepTournamentPairing = await deepenTournamentPairing(ctx, tournamentPairing); + const tournament = await getTournamentShallow(ctx, deepTournamentPairing.tournamentId); + + // If the match result is not from an on-going tournament, battle plans should be visible: + if (tournament?.status !== 'active') { + return true; + } + + if (userId) { + + // If the requesting user is an organizer, battle plans should be visible: + if (tournament.organizerUserIds.includes(userId)) { + return true; + } + + // If the requesting user is a player within that pairing, battle plans should be visible: + if (deepTournamentPairing.playerUserIds.includes(userId)) { + return true; + } + } + + // Hide battle plans in all other cases: + return false; +}; diff --git a/convex/_model/matchResults/_helpers/deepenMatchResult.ts b/convex/_model/matchResults/_helpers/deepenMatchResult.ts index 1ef3a542..55cd026f 100644 --- a/convex/_model/matchResults/_helpers/deepenMatchResult.ts +++ b/convex/_model/matchResults/_helpers/deepenMatchResult.ts @@ -2,6 +2,7 @@ import { Doc } from '../../../_generated/dataModel'; import { QueryCtx } from '../../../_generated/server'; import { getMission } from '../../fowV4/getMission'; import { getUser } from '../../users/queries/getUser'; +import { checkMatchResultBattlePlanVisibility } from './checkMatchResultBattlePlanVisibility'; /* eslint-disable @typescript-eslint/explicit-function-return-type */ /** @@ -24,19 +25,27 @@ export const deepenMatchResult = async ( const player1User = matchResult?.player1UserId ? await getUser(ctx, { id: matchResult.player1UserId, }) : null; - const mission = getMission(matchResult.details.missionId); + + // Social const comments = await ctx.db.query('matchResultComments') .withIndex('by_match_result_id',((q) => q.eq('matchResultId', matchResult._id))) .collect(); const likes = await ctx.db.query('matchResultLikes') .withIndex('by_match_result_id',((q) => q.eq('matchResultId', matchResult._id))) .collect(); + + // Details + const mission = getMission(matchResult.details.missionId); + const battlePlansVisible = await checkMatchResultBattlePlanVisibility(ctx, matchResult); + return { ...matchResult, ...(player0User ? { player0User } : {}), ...(player1User ? { player1User } : {}), details: { ...matchResult.details, + player0BattlePlan: battlePlansVisible ? matchResult.details.player0BattlePlan : undefined, + player1BattlePlan: battlePlansVisible ? matchResult.details.player1BattlePlan : undefined, missionName: mission?.displayName, }, likedByUserIds: likes.map((like) => like.userId), diff --git a/convex/_model/matchResults/queries/getMatchResults.ts b/convex/_model/matchResults/queries/getMatchResults.ts index 8bbb3e30..6381ab63 100644 --- a/convex/_model/matchResults/queries/getMatchResults.ts +++ b/convex/_model/matchResults/queries/getMatchResults.ts @@ -4,7 +4,7 @@ import { deepenMatchResult, DeepMatchResult } from '../_helpers/deepenMatchResul export const getMatchResults = async ( ctx: QueryCtx, ): Promise => { - const matchResults = await ctx.db.query('matchResults').collect(); + const matchResults = await ctx.db.query('matchResults').order('desc').collect(); return await Promise.all(matchResults.map( async (item) => await deepenMatchResult(ctx, item), )); diff --git a/convex/_model/matchResults/queries/getMatchResultsByTournament.ts b/convex/_model/matchResults/queries/getMatchResultsByTournament.ts index 2a70339a..c4231215 100644 --- a/convex/_model/matchResults/queries/getMatchResultsByTournament.ts +++ b/convex/_model/matchResults/queries/getMatchResultsByTournament.ts @@ -13,6 +13,7 @@ export const getMatchResultsByTournament = async ( ): Promise => { const matchResults = await ctx.db.query('matchResults') .withIndex('by_tournament_id', (q) => q.eq('tournamentId', args.tournamentId)) + .order('desc') .collect(); return await Promise.all(matchResults.map( async (item) => await deepenMatchResult(ctx, item), diff --git a/convex/_model/matchResults/queries/getMatchResultsByTournamentPairing.ts b/convex/_model/matchResults/queries/getMatchResultsByTournamentPairing.ts index add689e3..2e0feaf9 100644 --- a/convex/_model/matchResults/queries/getMatchResultsByTournamentPairing.ts +++ b/convex/_model/matchResults/queries/getMatchResultsByTournamentPairing.ts @@ -13,6 +13,7 @@ export const getMatchResultsByTournamentPairing = async ( ): Promise => { const matchResults = await ctx.db.query('matchResults') .withIndex('by_tournament_pairing_id', (q) => q.eq('tournamentPairingId', args.tournamentPairingId)) + .order('desc') .collect(); return await Promise.all(matchResults.map( async (item) => await deepenMatchResult(ctx, item), diff --git a/convex/_model/matchResults/queries/getMatchResultsByTournamentRound.ts b/convex/_model/matchResults/queries/getMatchResultsByTournamentRound.ts index 73b7e073..c3b5b33e 100644 --- a/convex/_model/matchResults/queries/getMatchResultsByTournamentRound.ts +++ b/convex/_model/matchResults/queries/getMatchResultsByTournamentRound.ts @@ -18,6 +18,7 @@ export const getMatchResultsByTournamentRound = async ( .collect(); const matchResults = await ctx.db.query('matchResults') .withIndex('by_tournament_id', (q) => q.eq('tournamentId', args.tournamentId)) + .order('desc') .collect(); const filteredMatchResults = matchResults.filter((result) => ( !!tournamentPairings.find((item) => item._id === result.tournamentPairingId) diff --git a/src/components/FowV4MatchResultDetails/FowV4MatchResultDetails.utils.ts b/src/components/FowV4MatchResultDetails/FowV4MatchResultDetails.utils.ts index 67c299fc..6116551a 100644 --- a/src/components/FowV4MatchResultDetails/FowV4MatchResultDetails.utils.ts +++ b/src/components/FowV4MatchResultDetails/FowV4MatchResultDetails.utils.ts @@ -12,10 +12,10 @@ export const formatOutcome = ( } if (details.winner !== -1 && details.outcomeType === 'force_broken') { if (details.winner === 0) { - return `${playerNames[0]} broke ${playerNames[1]}\u{2019}s formation(s).`; + return `${playerNames[0]} broke ${playerNames[1]}\u{2019}s formation.`; } if (details.winner === 1) { - return `${playerNames[1]} broke ${playerNames[0]}\u{2019}s formation(s).`; + return `${playerNames[1]} broke ${playerNames[0]}\u{2019}s formation.`; } } return 'Draw / Time Out'; diff --git a/src/pages/MatchResultDetailPage/components/MatchResultDetails.module.scss b/src/pages/MatchResultDetailPage/components/MatchResultDetails.module.scss deleted file mode 100644 index 9ab3d391..00000000 --- a/src/pages/MatchResultDetailPage/components/MatchResultDetails.module.scss +++ /dev/null @@ -1,65 +0,0 @@ -@use "/src/style/text"; -@use "/src/style/flex"; - -.Root { - @include text.ui; - - gap: 1rem; - - &[data-orientation="horizontal"] { - display: grid; - grid-template-areas: "meta meta" "set-up outcome"; - grid-template-columns: 1fr 1fr; - grid-template-rows: auto auto; - - .MetaSection { - @include flex.row; - } - } - - &[data-orientation="vertical"] { - @include flex.column; - - column-gap: 1rem; - } -} - -.MetaSection { - grid-area: meta; -} - -.SetUpSection { - display: grid; - grid-area: set-up; - grid-template-columns: auto 1fr; - row-gap: 0.25rem; - column-gap: 1rem; - - h3 { - grid-column: 1/3; - } -} - -.OutcomeSection { - display: grid; - grid-area: outcome; - grid-template-columns: auto 1fr; - row-gap: 0.25rem; - column-gap: 1rem; - - h3 { - grid-column: 1/3; - } -} - -.DetailLabel { - @include text.ui($muted: true); - - display: inline-block; -} - -.DetailValue { - @include text.ui; - - display: inline-block; -} diff --git a/src/pages/MatchResultDetailPage/components/MatchResultDetails.tsx b/src/pages/MatchResultDetailPage/components/MatchResultDetails.tsx deleted file mode 100644 index 19521b3c..00000000 --- a/src/pages/MatchResultDetailPage/components/MatchResultDetails.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import clsx from 'clsx'; - -import { fowV4BattlePlanOptions } from '~/api'; -import { useMatchResult } from '~/components/MatchResultProvider'; -import { useElementSize } from '~/hooks/useElementSize'; -import { formatOutcome } from '~/pages/MatchResultDetailPage/components/MatchResultDetails.utils'; - -import styles from './MatchResultDetails.module.scss'; - -export interface MatchResultDetailsProps { - className?: string; -} - -export const MatchResultDetails = ({ - className, -}: MatchResultDetailsProps): JSX.Element => { - const matchResult = useMatchResult(); - const [ref, width] = useElementSize(); - const orientation = Math.ceil(width) < 640 ? 'vertical' : 'horizontal'; // 2 x 320 + 1rem - 2x border - - const playerNames: [string, string] = [ - matchResult.player0User?.givenName || matchResult.player0User?.username || matchResult.player0Placeholder || 'Unknown Player', - matchResult.player1User?.givenName || matchResult.player1User?.username || matchResult.player1Placeholder || 'Unknown Player', - ]; - - return ( -
-
-

Meta

-
- Ruleset: - {matchResult.gameSystemId} (More Info) -
-
- Points: - {matchResult.gameSystemConfig.points} -
-
- Era: - {matchResult.gameSystemConfig.eraId} -
-
-
-

Game Set-Up

- {`${playerNames[0]}\u{2019}s Battle Plan:`} - {fowV4BattlePlanOptions.find((option) => option.value === matchResult.details.player0BattlePlan)?.label} - {`${playerNames[1]}\u{2019}s Battle Plan:`} - {fowV4BattlePlanOptions.find((option) => option.value === matchResult.details.player1BattlePlan)?.label} - Mission: - {matchResult.details.missionName} - Attacker: - {playerNames[matchResult.details.attacker]} - First Turn: - {playerNames[matchResult.details.firstTurn]} -
-
-

Outcome

- Turns Played: - {matchResult.details.turnsPlayed} - {`${playerNames[0]}\u{2019}s Units Lost:`} - {matchResult.details.player0UnitsLost} - {`${playerNames[1]}\u{2019}s Units Lost:`} - {matchResult.details.player1UnitsLost} - Outcome: - {formatOutcome(matchResult.details, playerNames)} -
-
- ); -}; diff --git a/src/pages/MatchResultDetailPage/components/MatchResultDetails.utils.ts b/src/pages/MatchResultDetailPage/components/MatchResultDetails.utils.ts deleted file mode 100644 index 5b0a2f5c..00000000 --- a/src/pages/MatchResultDetailPage/components/MatchResultDetails.utils.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { MatchResult } from '~/api'; - -export const formatOutcome = (details: MatchResult['details'], playerNames: [string, string]): string => { - if (details.winner !== -1 && details.outcomeType === 'attack_repelled') { - return `${playerNames[details.winner]} repelled the attack`; - } - if (details.winner !== -1 && details.outcomeType === 'objective_taken') { - return `${playerNames[details.winner]} took the objective`; - } - if (details.winner !== -1 && details.outcomeType === 'force_broken') { - if (details.winner === 0) { - return `${playerNames[0]} broke ${playerNames[1]}\u{2019}s formation(s)`; - } - if (details.winner === 1) { - return `${playerNames[1]} broke ${playerNames[0]}\u{2019}s formation(s)`; - } - } - return 'Draw / Time Out'; -};