Skip to content
Merged
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
3 changes: 1 addition & 2 deletions convex/_model/fowV4/calculateFowV4MatchResultScore.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Doc } from '../../_generated/dataModel';
import { DeepMatchResult } from '../matchResults';

/**
* Calculate the Victory Points (i.e. score) for a given match result.
Expand All @@ -10,7 +9,7 @@ import { DeepMatchResult } from '../matchResults';
* @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'> | DeepMatchResult): [number, number] => {
export const calculateFowV4MatchResultScore = (matchResult: Doc<'matchResults'>): [number, number] => {

// TODO: Add some guards in case matchResult is not FowV4

Expand Down
6 changes: 6 additions & 0 deletions convex/_model/matchResults/_helpers/deepenMatchResult.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Doc } from '../../../_generated/dataModel';
import { QueryCtx } from '../../../_generated/server';
import { calculateFowV4MatchResultScore } from '../../fowV4/calculateFowV4MatchResultScore';
import { getMission } from '../../fowV4/getMission';
import { getUser } from '../../users/queries/getUser';
import { checkMatchResultBattlePlanVisibility } from './checkMatchResultBattlePlanVisibility';
Expand Down Expand Up @@ -38,6 +39,9 @@ export const deepenMatchResult = async (
const mission = getMission(matchResult.details.missionId);
const battlePlansVisible = await checkMatchResultBattlePlanVisibility(ctx, matchResult);

// TODO: This is FowV4 specific, needs to be made generic!
const [player0Score, player1Score] = calculateFowV4MatchResultScore(matchResult);

return {
...matchResult,
...(player0User ? { player0User } : {}),
Expand All @@ -47,6 +51,8 @@ export const deepenMatchResult = async (
player0BattlePlan: battlePlansVisible ? matchResult.details.player0BattlePlan : undefined,
player1BattlePlan: battlePlansVisible ? matchResult.details.player1BattlePlan : undefined,
missionName: mission?.displayName,
player0Score,
player1Score,
},
likedByUserIds: likes.map((like) => like.userId),
commentCount: comments.length,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { getTournamentShallow } from '../../tournaments';
* This method's return type is, by nature, the definition of a deep TournamentPairing.
*
* @param ctx - Convex query context
* @param tournament - Raw TournamentPairing document
* @param tournamentPairing - Raw TournamentPairing document
* @returns A deep TournamentPairing
*/
export const deepenTournamentPairing = async (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { MatchResult, User } from '~/api';
export type FowV4MatchResultDetailsData = Pick<MatchResult, 'player0User' | 'player0UserId' | 'player0Placeholder' | 'player1User' | 'player1UserId' | 'player1Placeholder'> & {
player0User?: User;
player1User?: User;
details: Omit<MatchResult['details'], 'missionName'> & {
details: Omit<MatchResult['details'], 'missionName' | 'player0Score' | 'player1Score'> & {
missionName?: string;
}
};
Expand Down
28 changes: 10 additions & 18 deletions src/components/FowV4MatchResultForm/FowV4MatchResultForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
import clsx from 'clsx';

import {
MatchResultId,
MatchResult,
TournamentPairingId,
UserId,
} from '~/api';
Expand All @@ -16,11 +16,7 @@ import { SelectValue } from '~/components/generic/InputSelect/InputSelect.types'
import { Label } from '~/components/generic/Label';
import { Separator } from '~/components/generic/Separator';
import { useAsyncState } from '~/hooks/useAsyncState';
import {
useCreateMatchResult,
useGetMatchResult,
useUpdateMatchResult,
} from '~/services/matchResults';
import { useCreateMatchResult, useUpdateMatchResult } from '~/services/matchResults';
import { useGetActiveTournamentPairingsByUser } from '~/services/tournamentPairings';
import { getTournamentPairingDisplayName } from '~/utils/common/getTournamentPairingDisplayName';
import { CommonFields } from './components/CommonFields';
Expand All @@ -40,25 +36,20 @@ const confirmMatchResultDialogId = 'confirm-match-result';
export interface FowV4MatchResultFormProps {
id: string;
className?: string;
matchResultId?: MatchResultId;
matchResult?: MatchResult;
tournamentPairingId?: TournamentPairingId;
onSuccess?: () => void;
}

export const FowV4MatchResultForm = ({
id,
className,
matchResultId,
matchResult,
tournamentPairingId: forcedTournamentPairingId,
onSuccess,
}: FowV4MatchResultFormProps): JSX.Element => {
const user = useAuth();

const {
data: matchResult,
loading: matchResultLoading,
} = useGetMatchResult(matchResultId ? { id: matchResultId } : 'skip');

const [
tournamentPairingId,
setTournamentPairingId,
Expand Down Expand Up @@ -89,9 +80,10 @@ export const FowV4MatchResultForm = ({

const form = useForm<FowV4MatchResultFormData>({
resolver: zodResolver(fowV4MatchResultFormSchema),
defaultValues,
// React-Hook-Form is stupid and doesn't allow applying a partial record to the form values
values: { ...matchResult as FowV4MatchResultFormData },
defaultValues: {
...defaultValues,
...(matchResult ? fowV4MatchResultFormSchema.parse(matchResult) : {}),
},
mode: 'onSubmit',
});

Expand Down Expand Up @@ -143,13 +135,13 @@ export const FowV4MatchResultForm = ({

const disableSubmit = createMatchResultLoading || updateMatchResultLoading;

if (tournamentPairingsLoading || matchResultLoading) {
if (tournamentPairingsLoading) {
return <div>Loading...</div>;
}

return (
<Form id={id} form={form} onSubmit={onSubmit} className={clsx(styles.FowV4MatchResultForm, className)}>
{!matchResultId && (
{!matchResult && !forcedTournamentPairingId && (
<>
<div className={styles.FowV4MatchResultForm_ResultForSection}>
<Label>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ export const useMissionOptions = () => {
const {
missionMatrixId,
missionPackId,
useExperimentalMissions,
} = gameSystemConfig;
return useMemo(() => {
const missionPack = getMissionPack(missionPackId);
Expand All @@ -78,15 +77,12 @@ export const useMissionOptions = () => {

const matrixEntryMissionIds = matrixEntry.missions.reduce((acc: string[], item) => {
if (Array.isArray(item)) {
if (item[1] && useExperimentalMissions) {
return [...acc, item[1]];
}
return [ ...acc, ...item];
}
return [ ...acc,item];
return [ ...acc, item];
}, []);
return missionsOptions.filter((option) => matrixEntryMissionIds.includes(option.value));
}, [player0BattlePlan, player1BattlePlan, missionPackId, missionMatrixId, useExperimentalMissions]);
}, [player0BattlePlan, player1BattlePlan, missionPackId, missionMatrixId ]);
};

export const useOutcomeTypeOptions = () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useMemo } from 'react';
import { useEffect } from 'react';
import { useFormContext } from 'react-hook-form';

import { fowV4BattlePlanOptions, TournamentPairingId } from '~/api';
Expand Down Expand Up @@ -27,12 +27,8 @@ export const TournamentPlayersFields = ({
const player0Label = `${getTournamentCompetitorDisplayName(selectedPairing?.tournamentCompetitor0)} Player`;
const player1Label = `${getTournamentCompetitorDisplayName(selectedPairing?.tournamentCompetitor1)} Player`;

const player0Options = useMemo(() => (
getCompetitorPlayerOptions(selectedPairing?.tournamentCompetitor0)
), [selectedPairing]);
const player1Options = useMemo(() => (
getCompetitorPlayerOptions(selectedPairing?.tournamentCompetitor1)
), [selectedPairing]);
const player0Options = getCompetitorPlayerOptions(selectedPairing?.tournamentCompetitor0);
const player1Options = getCompetitorPlayerOptions(selectedPairing?.tournamentCompetitor1);

// Automatically set "Player 1" if possible
useEffect(() => {
Expand All @@ -48,6 +44,10 @@ export const TournamentPlayersFields = ({
}
}, [player1Options, player1UserId, setValue]);

if (!selectedPairing) {
return <div>Loading...</div>;
}

return (
<div className={styles.Root}>
<div className={styles.Player0Section}>
Expand Down
5 changes: 4 additions & 1 deletion src/components/IdentityBadge/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
export type { IdentityBadgeProps } from './IdentityBadge';
export { IdentityBadge } from './IdentityBadge';
export type { Identity } from './IdentityBadge.types';
export type {
Identity,
IdentityBadgePlaceholder,
} from './IdentityBadge.types';
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { PopoverMenu } from '~/components/generic/PopoverMenu';
import { MatchResultDeleteDialog, useMatchResultDeleteDialog } from '~/components/MatchResultDeleteDialog';
import { MatchResultEditDialog, useMatchResultEditDialog } from '~/components/MatchResultEditDialog';
import { useMatchResult } from '~/components/MatchResultProvider';
import { useGetTournament } from '~/services/tournaments';

export interface MatchResultContextMenuProps {
size?: 'small' | 'normal' | 'large';
Expand All @@ -16,12 +17,18 @@ export const MatchResultContextMenu = ({
}: MatchResultContextMenuProps): JSX.Element | null => {
const user = useAuth();
const matchResult = useMatchResult();
const { data: tournament } = useGetTournament(matchResult.tournamentId ? {
id: matchResult.tournamentId,
} : 'skip');

const { open: openEditDialog } = useMatchResultEditDialog(matchResult._id);
const { open: openDeleteDialog } = useMatchResultDeleteDialog(matchResult._id);

// TODO: Make better check for showing context menu
const showContextMenu = user && !matchResult.tournamentPairingId && [matchResult.player0UserId, matchResult.player1UserId].includes(user._id);
const isOrganizer = user && tournament?.organizerUserIds.includes(user._id) && tournament?.status === 'active';
const isPlayer = user && [matchResult.player0UserId, matchResult.player1UserId].includes(user._id);

const showContextMenu = isOrganizer || (isPlayer && !matchResult.tournamentId); // Don't allow editing of tournament results
const contextMenuItems = [
{ label: 'Edit', onClick: openEditDialog },
{ label: 'Delete', onClick: openDeleteDialog },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { TournamentPairingId } from '~/api';
import { useModal } from '~/modals';

export const useMatchResultCreateDialog = () => useModal('match-result-create-dialog');
export const useMatchResultCreateDialog = () => useModal<{ tournamentPairingId: TournamentPairingId }>('match-result-create-dialog');
12 changes: 10 additions & 2 deletions src/components/MatchResultCreateDialog/MatchResultCreateDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { TournamentPairingId } from '~/api';
import { FowV4MatchResultForm } from '~/components/FowV4MatchResultForm';
import { Button } from '~/components/generic/Button';
import {
Expand All @@ -11,15 +12,22 @@ import { useMatchResultCreateDialog } from './MatchResultCreateDialog.hooks';

import styles from './MatchResultCreateDialog.module.scss';

export const MatchResultCreateDialog = (): JSX.Element => {
const { id, close } = useMatchResultCreateDialog();
export interface MatchResultCreateDialogProps {
tournamentPairingId?: TournamentPairingId;
}

export const MatchResultCreateDialog = ({
tournamentPairingId,
}: MatchResultCreateDialogProps): JSX.Element => {
const { id, close, data } = useMatchResultCreateDialog();
return (
<ControlledDialog id={id} className={styles.MatchResultCreateDialog}>
<DialogHeader title="Create Match Result" onCancel={close} />
<Separator />
<ScrollArea type="scroll" indicatorBorders={['top', 'bottom']}>
<FowV4MatchResultForm
id="fow-v4-match-result-form"
tournamentPairingId={tournamentPairingId ?? data?.tournamentPairingId}
className={styles.Form}
onSuccess={close}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const MatchResultEditDialog = (): JSX.Element => {
<ScrollArea type="scroll" indicatorBorders={['top', 'bottom']}>
<FowV4MatchResultForm
id="fow-v4-match-result-form"
matchResultId={matchResult._id}
matchResult={matchResult}
className={styles.Form}
onSuccess={close}
/>
Expand Down
2 changes: 1 addition & 1 deletion src/pages/DashboardPage/DashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const DashboardPage = (): JSX.Element => {
stats: <LineChart />,
};
return (
<PageWrapper title="Dashboard" fitToWindow footer={!isDesktop ? (
<PageWrapper title="Dashboard" fitToWindow={isDesktop} footer={!isDesktop ? (
<div className={styles.DashboardPage_Tabs}>
{Object.entries(tabs).map(([key, icon]) => (
<div key={key} className={styles.DashboardPage_TabTrigger}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@
&_OpponentSection {
display: grid;
grid-template-areas:
"opponentLabel tableLabel ."
"opponent table checkInButton";
grid-template-columns: 1fr auto auto;
"opponentLabel tableLabel"
"opponent table"
"checkInButton checkInButton";
grid-template-columns: 1fr auto;
grid-template-rows: auto auto;
row-gap: 0.5rem;
column-gap: 1rem;
Expand Down Expand Up @@ -56,6 +57,7 @@

&_CheckInButton {
grid-area: checkInButton;
margin-top: 1rem;
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { generatePath, useNavigate } from 'react-router-dom';
import clsx from 'clsx';
import { ChevronRight, Plus } from 'lucide-react';
import { ChevronRight } from 'lucide-react';

import {
Tournament,
TournamentCompetitorRanked,
TournamentPairing,
} from '~/api';
import { useAuth } from '~/components/AuthProvider';
import { CheckInMatchDialog } from '~/components/CheckInMatchDialog';
import { Button } from '~/components/generic/Button';
import { Separator } from '~/components/generic/Separator';
import { IdentityBadge } from '~/components/IdentityBadge';
Expand Down Expand Up @@ -47,6 +46,12 @@ export const ActiveTournament = ({
const handleViewMore = (): void => {
navigate(generatePath(PATHS.tournamentDetails, { id: tournament._id }));
};
const handleViewMatchResults = (): void => {
if (!pairing) {
return;
}
navigate(generatePath(PATHS.tournamentPairingDetails, { id: pairing._id }));
};

const isOrganizer = user && tournament && tournament.organizerUserIds.includes(user._id);
const showTimer = tournament && tournament.currentRound !== undefined;
Expand All @@ -68,31 +73,32 @@ export const ActiveTournament = ({
</Header>
{showTimer && (
<>
<TournamentTimer className={styles.ActiveTournamentCard_Timer} />
<TournamentTimer className={styles.ActiveTournament_Timer} />
<Separator />
</>
)}
{showOpponent && (
<>
<div className={styles.ActiveTournamentCard_OpponentSection}>
<h3 className={styles.ActiveTournamentCard_OpponentSection_OpponentLabel}>
<div className={styles.ActiveTournament_OpponentSection}>
<h3 className={styles.ActiveTournament_OpponentSection_OpponentLabel}>
{tournament.currentRound !== undefined ? 'Current Opponent' : 'Next Opponent'}
</h3>
<IdentityBadge
className={styles.ActiveTournamentCard_OpponentSection_Opponent}
className={styles.ActiveTournament_OpponentSection_Opponent}
competitor={opponent}
/>
<h3 className={styles.ActiveTournamentCard_OpponentSection_TableLabel}>
<h3 className={styles.ActiveTournament_OpponentSection_TableLabel}>
Table
</h3>
<span className={styles.ActiveTournamentCard_OpponentSection_Table}>
<span className={styles.ActiveTournament_OpponentSection_Table}>
{(pairing?.table ?? 0) + 1}
</span>
<CheckInMatchDialog tournamentPairingId={pairing._id}>
<Button className={styles.ActiveTournamentCard_OpponentSection_CheckInButton}>
<Plus /> Add Match Result
</Button>
</CheckInMatchDialog>
<Button
className={styles.ActiveTournament_OpponentSection_CheckInButton}
onClick={handleViewMatchResults}
>
Match Results <ChevronRight />
</Button>
</div>
<Separator />
</>
Expand Down
Loading