diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index 11df2d20..35ab2830 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -126,6 +126,7 @@ import type * as _model_tournaments_queries_getTournamentRankings from "../_mode import type * as _model_tournaments_queries_getTournaments from "../_model/tournaments/queries/getTournaments.js"; import type * as _model_tournaments_queries_getTournamentsByStatus from "../_model/tournaments/queries/getTournamentsByStatus.js"; import type * as _model_users__helpers_checkUserAuth from "../_model/users/_helpers/checkUserAuth.js"; +import type * as _model_users__helpers_checkUserTournamentForcedName from "../_model/users/_helpers/checkUserTournamentForcedName.js"; import type * as _model_users__helpers_checkUserTournamentRelationship from "../_model/users/_helpers/checkUserTournamentRelationship.js"; import type * as _model_users__helpers_getShallowUser from "../_model/users/_helpers/getShallowUser.js"; import type * as _model_users__helpers_redactUser from "../_model/users/_helpers/redactUser.js"; @@ -308,6 +309,7 @@ declare const fullApi: ApiFromModules<{ "_model/tournaments/queries/getTournaments": typeof _model_tournaments_queries_getTournaments; "_model/tournaments/queries/getTournamentsByStatus": typeof _model_tournaments_queries_getTournamentsByStatus; "_model/users/_helpers/checkUserAuth": typeof _model_users__helpers_checkUserAuth; + "_model/users/_helpers/checkUserTournamentForcedName": typeof _model_users__helpers_checkUserTournamentForcedName; "_model/users/_helpers/checkUserTournamentRelationship": typeof _model_users__helpers_checkUserTournamentRelationship; "_model/users/_helpers/getShallowUser": typeof _model_users__helpers_getShallowUser; "_model/users/_helpers/redactUser": typeof _model_users__helpers_redactUser; diff --git a/convex/_model/fowV4/aggregateFowV4TournamentData.ts b/convex/_model/fowV4/aggregateFowV4TournamentData.ts index 1af355e7..b2e4c124 100644 --- a/convex/_model/fowV4/aggregateFowV4TournamentData.ts +++ b/convex/_model/fowV4/aggregateFowV4TournamentData.ts @@ -131,11 +131,11 @@ export const aggregateFowV4TournamentData = async ( } return { - players: flattenFowV4StatMap(playerStats).map(({ id, stats }) => ({ + players: flattenFowV4StatMap(playerStats).filter(({ gamesPlayed }) => gamesPlayed).map(({ id, stats }) => ({ id, stats, })), - competitors: flattenFowV4StatMap(competitorStats).map(({ id, stats }) => ({ + competitors: flattenFowV4StatMap(competitorStats).filter(({ gamesPlayed }) => gamesPlayed).map(({ id, stats }) => ({ id, stats, ...competitorMeta[id], diff --git a/convex/_model/fowV4/flattenFowV4StatMap.ts b/convex/_model/fowV4/flattenFowV4StatMap.ts index 818a3ed9..f546dbd6 100644 --- a/convex/_model/fowV4/flattenFowV4StatMap.ts +++ b/convex/_model/fowV4/flattenFowV4StatMap.ts @@ -13,7 +13,7 @@ import { export const flattenFowV4StatMap = ( statMap: Record, -): { id: T; stats: FowV4TournamentFlatExtendedStats }[] => { +): { id: T; stats: FowV4TournamentFlatExtendedStats, gamesPlayed: number }[] => { const statList = Object.entries(statMap) as [T, FowV4TournamentExtendedStats][]; return statList.map(([key, stats]) => { const flatStats = {} as FowV4TournamentFlatExtendedStats; @@ -27,6 +27,7 @@ export const flattenFowV4StatMap = ( return { id: key, stats: flatStats, + gamesPlayed: stats.gamesPlayed ?? 0, }; }); }; diff --git a/convex/_model/tournamentCompetitors/queries/getTournamentCompetitors.ts b/convex/_model/tournamentCompetitors/queries/getTournamentCompetitors.ts index 141db622..479846f8 100644 --- a/convex/_model/tournamentCompetitors/queries/getTournamentCompetitors.ts +++ b/convex/_model/tournamentCompetitors/queries/getTournamentCompetitors.ts @@ -5,7 +5,22 @@ export const getTournamentCompetitors = async ( ctx: QueryCtx, ): Promise => { const tournamentCompetitors = await ctx.db.query('tournamentCompetitors').collect(); - return await Promise.all(tournamentCompetitors.map( + const deepTournamentCompetitors = await Promise.all(tournamentCompetitors.map( async (item) => await deepenTournamentCompetitor(ctx, item), )); + return deepTournamentCompetitors.sort((a, b) => { + const getSortValue = (competitor: DeepTournamentCompetitor): string => { + if (competitor.teamName) { + return competitor.teamName; + } + if (competitor.players[0].user.familyName) { + return competitor.players[0].user.familyName; + } + if (competitor.players[0].user.username) { + return competitor.players[0].user.username; + } + return ''; + }; + return getSortValue(a).localeCompare(getSortValue(b)); + }); }; diff --git a/convex/_model/tournamentCompetitors/queries/getTournamentCompetitorsByTournament.ts b/convex/_model/tournamentCompetitors/queries/getTournamentCompetitorsByTournament.ts index c94ace79..de9087c9 100644 --- a/convex/_model/tournamentCompetitors/queries/getTournamentCompetitorsByTournament.ts +++ b/convex/_model/tournamentCompetitors/queries/getTournamentCompetitorsByTournament.ts @@ -14,7 +14,22 @@ export const getTournamentCompetitorsByTournament = async ( const tournamentCompetitors = await ctx.db.query('tournamentCompetitors') .withIndex('by_tournament_id', (q) => q.eq('tournamentId', args.tournamentId)) .collect(); - return await Promise.all(tournamentCompetitors.map( + const deepTournamentCompetitors = await Promise.all(tournamentCompetitors.map( async (item) => await deepenTournamentCompetitor(ctx, item), )); + return deepTournamentCompetitors.sort((a, b) => { + const getSortValue = (competitor: DeepTournamentCompetitor): string => { + if (competitor.teamName) { + return competitor.teamName; + } + if (competitor.players[0]?.user.familyName) { + return competitor.players[0].user.familyName; + } + if (competitor.players[0]?.user.username) { + return competitor.players[0].user.username; + } + return ''; + }; + return getSortValue(a).localeCompare(getSortValue(b)); + }); }; diff --git a/convex/_model/tournaments/_helpers/deepenTournament.ts b/convex/_model/tournaments/_helpers/deepenTournament.ts index d9ff7f74..e58a7098 100644 --- a/convex/_model/tournaments/_helpers/deepenTournament.ts +++ b/convex/_model/tournaments/_helpers/deepenTournament.ts @@ -37,6 +37,7 @@ export const deepenTournament = async ( // Computed properties (easy to do, but used so frequently, it's nice to include them by default) const playerCount = playerUserIds.length; + const activePlayerCount = activePlayerUserIds.length; const maxPlayers = tournament.maxCompetitors * tournament.competitorSize; const useTeams = tournament.competitorSize > 1; @@ -45,6 +46,7 @@ export const deepenTournament = async ( logoUrl, bannerUrl, competitorCount, + activePlayerCount, playerCount, playerUserIds, activePlayerUserIds, diff --git a/convex/_model/users/_helpers/checkUserTournamentForcedName.ts b/convex/_model/users/_helpers/checkUserTournamentForcedName.ts new file mode 100644 index 00000000..423be595 --- /dev/null +++ b/convex/_model/users/_helpers/checkUserTournamentForcedName.ts @@ -0,0 +1,34 @@ +import { Id } from '../../../_generated/dataModel'; +import { QueryCtx } from '../../../_generated/server'; +import { getTournamentUserIds } from '../../../_model/tournaments'; + +export const checkUserTournamentForcedName = async ( + ctx: QueryCtx, + userIdA?: Id<'users'> | null, + userIdB?: Id<'users'> | null, +): Promise => { + if (!userIdA || !userIdB) { + return false; + } + + const tournaments = await ctx.db.query('tournaments').collect(); + + // Check each tournament for a relationship, return true if one is found + // Check each tournament for a relationship, return true if one is found + for (const { _id, organizerUserIds, requireRealNames } of tournaments) { + const playerUserIds = await getTournamentUserIds(ctx, _id); + + // Merge all organizer IDs and player IDs into one set + const allTournamentUserIds = new Set([ + ...organizerUserIds, + ...playerUserIds, + ]); + + // If the set contains both user IDs, they were at the same tournament + if (allTournamentUserIds.has(userIdA) && allTournamentUserIds.has(userIdB) && requireRealNames) { + return true; + } + } + + return false; +}; diff --git a/convex/_model/users/_helpers/redactUser.ts b/convex/_model/users/_helpers/redactUser.ts index d3e001ef..961820b9 100644 --- a/convex/_model/users/_helpers/redactUser.ts +++ b/convex/_model/users/_helpers/redactUser.ts @@ -3,6 +3,7 @@ import { getAuthUserId } from '@convex-dev/auth/server'; import { Doc } from '../../../_generated/dataModel'; import { QueryCtx } from '../../../_generated/server'; import { getStorageUrl } from '../../common/_helpers/getStorageUrl'; +import { checkUserTournamentForcedName } from './checkUserTournamentForcedName'; import { checkUserTournamentRelationship } from './checkUserTournamentRelationship'; /** @@ -51,11 +52,15 @@ export const redactUser = async ( // If user is querying someone they are in a tournament with const hasTournamentRelationship = await checkUserTournamentRelationship(ctx, userId, user._id); + // If user is querying someone they are in a tournament with which requires real names + const requiredByTournament = await checkUserTournamentForcedName(ctx, userId, user._id); + // Add name information if allowed if ( (user?.nameVisibility === 'public') || (user?.nameVisibility === 'friends' && hasFriendRelationship) || - (user?.nameVisibility === 'tournaments' && (hasFriendRelationship || hasTournamentRelationship)) + (user?.nameVisibility === 'tournaments' && (hasFriendRelationship || hasTournamentRelationship)) || + requiredByTournament ) { limitedUser.givenName = user.givenName; limitedUser.familyName = user.familyName; diff --git a/src/components/AccountMenu/AccountMenu.module.scss b/src/components/AccountMenu/AccountMenu.module.scss index ed3c6f2e..dbe7af09 100644 --- a/src/components/AccountMenu/AccountMenu.module.scss +++ b/src/components/AccountMenu/AccountMenu.module.scss @@ -16,14 +16,11 @@ .Content { @include flex.column($gap: 0.25rem); + @include variants.card($elevated: true); @include animate.duration-quick; - @include corners.normal; - @include shadows.elevated; - @include variants.card; margin: 0.25rem 0; padding: 0.25rem; - background-color: var(--card-bg); @include animate.style-pop; // Must list last because it contains nested declarations } diff --git a/src/components/AvatarEditable/AvatarEditable.module.scss b/src/components/AvatarEditable/AvatarEditable.module.scss index 58bf7841..c05ad7ac 100644 --- a/src/components/AvatarEditable/AvatarEditable.module.scss +++ b/src/components/AvatarEditable/AvatarEditable.module.scss @@ -25,14 +25,11 @@ .ActionsMenu { @include flex.column($gap: 0.25rem); @include animate.duration-quick; - @include corners.normal; - @include shadows.elevated; - @include variants.card; + @include variants.card($elevated: true); z-index: 10000; margin: 0.25rem 0; padding: 0.25rem; - background-color: var(--card-bg); @include animate.style-pop; // Must list last because it contains nested declarations } diff --git a/src/components/FowV4MatchResultForm/FowV4MatchResultForm.tsx b/src/components/FowV4MatchResultForm/FowV4MatchResultForm.tsx index 28fc20ed..8bffffbc 100644 --- a/src/components/FowV4MatchResultForm/FowV4MatchResultForm.tsx +++ b/src/components/FowV4MatchResultForm/FowV4MatchResultForm.tsx @@ -2,7 +2,11 @@ import { SubmitHandler, useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import clsx from 'clsx'; -import { MatchResultId, TournamentPairingId } from '~/api'; +import { + MatchResultId, + TournamentPairingId, + UserId, +} from '~/api'; import { useAuth } from '~/components/AuthProvider'; import { ConfirmationDialog, useConfirmationDialog } from '~/components/ConfirmationDialog'; import { FowV4MatchResultDetails } from '~/components/FowV4MatchResultDetails'; @@ -119,7 +123,7 @@ export const FowV4MatchResultForm = ({ const resultForOptions = [ { value: 'single', label: 'Single Match' }, - ...(tournamentPairings || []).map((pairing) => ({ + ...(tournamentPairings || []).filter((pairing) => pairing.matchResultsProgress.submitted < pairing.matchResultsProgress.required).map((pairing) => ({ value: pairing._id, label: getTournamentPairingDisplayName(pairing), })), @@ -128,6 +132,10 @@ export const FowV4MatchResultForm = ({ const handleChangeResultFor = (value?: SelectValue): void => { if (value) { setTournamentPairingId(value as TournamentPairingId); + if (user && value === 'single') { + form.setValue('player0UserId', user._id); + form.setValue('player1UserId', '' as UserId); + } } }; diff --git a/src/components/MatchResultCard/MatchResultCard.module.scss b/src/components/MatchResultCard/MatchResultCard.module.scss index 93b2a24e..44f51d22 100644 --- a/src/components/MatchResultCard/MatchResultCard.module.scss +++ b/src/components/MatchResultCard/MatchResultCard.module.scss @@ -9,10 +9,7 @@ .MatchResultCard { @include flex.column($gap: 0); - @include variants.card; - @include shadows.surface; - @include corners.normal; position: relative; overflow: hidden; diff --git a/src/components/MatchResultPlayers/MatchResultPlayers.module.scss b/src/components/MatchResultPlayers/MatchResultPlayers.module.scss index f2268fae..7e32cacf 100644 --- a/src/components/MatchResultPlayers/MatchResultPlayers.module.scss +++ b/src/components/MatchResultPlayers/MatchResultPlayers.module.scss @@ -109,13 +109,10 @@ } .FactionDetails { - @include variants.card; - @include shadows.elevated; + @include variants.card($elevated: true); @include animate.duration-quick; @include animate.style-pop; @include flex.column($gap: 0); margin: 0.25rem 0; - background-color: var(--card-bg); - border-radius: variables.$corner-radius; } diff --git a/src/components/ToastProvider/ToastProvider.module.scss b/src/components/ToastProvider/ToastProvider.module.scss index 515e06a8..838f09b3 100644 --- a/src/components/ToastProvider/ToastProvider.module.scss +++ b/src/components/ToastProvider/ToastProvider.module.scss @@ -26,8 +26,7 @@ .Root { --icon-size: 1.25rem; - @include corners.wide; - @include variants.card; + @include variants.card($elevated: true); @include text.small; display: flex; diff --git a/src/components/TournamentCard/TournamentCard.module.scss b/src/components/TournamentCard/TournamentCard.module.scss index 154e3174..ef3f2706 100644 --- a/src/components/TournamentCard/TournamentCard.module.scss +++ b/src/components/TournamentCard/TournamentCard.module.scss @@ -12,8 +12,6 @@ --banner-wide-size: 14rem; @include variants.card; - @include shadows.surface; - @include corners.normal; @include text.ui; position: relative; diff --git a/src/components/TournamentCompetitorEditDialog/TournamentCompetitorEditDialog.tsx b/src/components/TournamentCompetitorEditDialog/TournamentCompetitorEditDialog.tsx index fd36e82a..e7870637 100644 --- a/src/components/TournamentCompetitorEditDialog/TournamentCompetitorEditDialog.tsx +++ b/src/components/TournamentCompetitorEditDialog/TournamentCompetitorEditDialog.tsx @@ -4,6 +4,7 @@ import { DialogActions, DialogHeader, } from '~/components/generic/Dialog'; +import { ScrollArea } from '~/components/generic/ScrollArea'; import { TournamentCompetitorForm, TournamentCompetitorSubmitData } from '~/components/TournamentCompetitorForm'; import { useUpdateTournamentCompetitor } from '~/services/tournamentCompetitors'; import { useTournamentCompetitorEditDialog } from './TournamentCompetitorEditDialog.hooks'; @@ -25,22 +26,27 @@ export const TournamentCompetitorEditDialog = (): JSX.Element => { if (!data) { return; } + + const { players, ...restData } = formData; updateTournamentCompetitor({ id: data?.tournamentCompetitor._id, - ...formData, + ...restData, + players: players.filter((player) => player.userId), }); }; return ( - + + + diff --git a/src/components/TournamentCompetitorForm/TournamentCompetitorForm.module.scss b/src/components/TournamentCompetitorForm/TournamentCompetitorForm.module.scss index c5348d3d..f35e4bff 100644 --- a/src/components/TournamentCompetitorForm/TournamentCompetitorForm.module.scss +++ b/src/components/TournamentCompetitorForm/TournamentCompetitorForm.module.scss @@ -11,6 +11,10 @@ row-gap: 0.5rem; column-gap: 1rem; align-items: center; + + &_AddButton { + grid-column: 1/-1; + } } &_SinglePlayer { diff --git a/src/components/TournamentCompetitorForm/TournamentCompetitorForm.schema.ts b/src/components/TournamentCompetitorForm/TournamentCompetitorForm.schema.ts index 299dc51d..31563df4 100644 --- a/src/components/TournamentCompetitorForm/TournamentCompetitorForm.schema.ts +++ b/src/components/TournamentCompetitorForm/TournamentCompetitorForm.schema.ts @@ -17,21 +17,19 @@ export const createSchema = ( ), }).superRefine((data, ctx) => { const activeCount = data.players.filter((player) => player.active).length; - if (status === 'active') { - if (activeCount < competitorSize) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `At least ${competitorSize} player(s) must be active.`, - path: ['players'], - }); - } - if (activeCount > competitorSize) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Only ${competitorSize} player(s) may be active.`, - path: ['players'], - }); - } + if (activeCount < competitorSize && status === 'active') { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `At least ${competitorSize} players must be active.`, + path: ['players'], + }); + } + if (activeCount > competitorSize) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Only ${competitorSize} players may be active.`, + path: ['players'], + }); } if (otherCompetitors.find((c) => c.teamName?.toLowerCase() === data.teamName.trim().toLowerCase())) { ctx.addIssue({ @@ -49,17 +47,10 @@ export const defaultValues: DeepPartial = { players: [], }; -export const getDefaultValues = (competitorSize: number, existingCompetitor?: TournamentCompetitor): DeepPartial => { - const emptyPlayers = Array.from({ length: competitorSize }, () => ({ - active: true, - userId: '', - })); - const existingPlayers = (existingCompetitor?.players || []).map(({ active, user }) => ({ +export const getDefaultValues = (competitorSize: number, existingCompetitor?: TournamentCompetitor): DeepPartial => ({ + teamName: existingCompetitor?.teamName ?? '', + players: (existingCompetitor?.players || []).map(({ active, user }) => ({ active, userId: user._id, - })); - return { - teamName: existingCompetitor?.teamName ?? '', - players: existingPlayers.length ? existingPlayers : emptyPlayers, - }; -}; + })), +}); diff --git a/src/components/TournamentCompetitorForm/TournamentCompetitorForm.tsx b/src/components/TournamentCompetitorForm/TournamentCompetitorForm.tsx index f28cc6b8..5506b273 100644 --- a/src/components/TournamentCompetitorForm/TournamentCompetitorForm.tsx +++ b/src/components/TournamentCompetitorForm/TournamentCompetitorForm.tsx @@ -1,3 +1,4 @@ +import { MouseEvent } from 'react'; import { Fragment } from 'react/jsx-runtime'; import { SubmitHandler, @@ -7,6 +8,7 @@ import { } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import clsx from 'clsx'; +import { Plus } from 'lucide-react'; import { TournamentCompetitor, @@ -14,6 +16,7 @@ import { UserId, } from '~/api'; import { useAuth } from '~/components/AuthProvider'; +import { Button } from '~/components/generic/Button'; import { Form, FormField } from '~/components/generic/Form'; import { InputSelect } from '~/components/generic/InputSelect'; import { InputText } from '~/components/generic/InputText'; @@ -70,7 +73,7 @@ export const TournamentCompetitorForm = ({ defaultValues: getDefaultValues(competitorSize, tournamentCompetitor), mode: 'onSubmit', }); - const { fields } = useFieldArray({ + const { fields, append } = useFieldArray({ control: form.control, name: 'players', rules: { @@ -91,11 +94,25 @@ export const TournamentCompetitorForm = ({ form.setValue(`players.${i}.userId`, userId); } }; + const handleChangePlayerActive = (i: number, active: boolean): void => { form.setValue(`players.${i}.active`, active); }; - const handleSubmit: SubmitHandler = async (formData): Promise => { - onSubmit({ tournamentId, ...formData }); + + const handleAddPlayer = (e: MouseEvent): void => { + e.preventDefault(); + append({ + active: players.filter((p) => p.active).length < competitorSize, + userId: '' as UserId, + }); + }; + + const handleSubmit: SubmitHandler = async ({ players, ...formData }): Promise => { + onSubmit({ + tournamentId, + players: players.filter((p) => p.userId.length), + ...formData, + }); }; const excludedUserIds = [ @@ -105,6 +122,8 @@ export const TournamentCompetitorForm = ({ const isOrganizer = user && organizerUserIds.includes(user._id); + const emptyPlayerSlots = players.filter((player) => !player.userId.length).length; + return (
{useTeams && ( @@ -124,20 +143,29 @@ export const TournamentCompetitorForm = ({ handleChangePlayerActive(i, value)} disabled={loading} /> handleChangeUser(i, value)} excludedUserIds={excludedUserIds} - disabled={loading} + disabled={loading || (!!players[i]?.userId && status !== 'published')} allowPlaceholder={false} /> ))} +
+ +
) : ( <> diff --git a/src/components/TournamentInfoBlock/TournamentInfoBlock.tsx b/src/components/TournamentInfoBlock/TournamentInfoBlock.tsx index 404b4be2..f1d34603 100644 --- a/src/components/TournamentInfoBlock/TournamentInfoBlock.tsx +++ b/src/components/TournamentInfoBlock/TournamentInfoBlock.tsx @@ -53,7 +53,7 @@ export const TournamentInfoBlock = ({
- {`${tournament.playerCount} / ${tournament.maxPlayers}`} + {`${tournament.activePlayerCount} / ${tournament.maxPlayers}`} {tournament.useTeams && ( {`(${tournament.competitorCount} / ${tournament.maxCompetitors} teams)`} )} diff --git a/src/components/TournamentPairingsGrid/PairingsGridRow/PairingsGridRow.tsx b/src/components/TournamentPairingsGrid/PairingsGridRow/PairingsGridRow.tsx index 0f3909c5..59f375bd 100644 --- a/src/components/TournamentPairingsGrid/PairingsGridRow/PairingsGridRow.tsx +++ b/src/components/TournamentPairingsGrid/PairingsGridRow/PairingsGridRow.tsx @@ -1,10 +1,11 @@ import { AnimatePresence, motion } from 'framer-motion'; import { CircleCheck, CircleX } from 'lucide-react'; -import { DraftTournamentPairing, TournamentCompetitorRanked } from '~/api'; +import { TournamentCompetitorRanked } from '~/api'; import { Draggable } from '../Draggable'; import { Droppable } from '../Droppable/Droppable'; import { PairableCompetitorCard } from '../PairableCompetitorCard'; +import { DraftTournamentPairing } from '../TournamentPairingsGrid.types'; import { checkDraftPairingIsValid } from './PairingsGridRow.utils'; import styles from './PairingsGridRow.module.scss'; diff --git a/src/components/TournamentPairingsGrid/PairingsGridRow/PairingsGridRow.utils.ts b/src/components/TournamentPairingsGrid/PairingsGridRow/PairingsGridRow.utils.ts index cd377314..a13691d4 100644 --- a/src/components/TournamentPairingsGrid/PairingsGridRow/PairingsGridRow.utils.ts +++ b/src/components/TournamentPairingsGrid/PairingsGridRow/PairingsGridRow.utils.ts @@ -1,4 +1,4 @@ -import { DraftTournamentPairing } from '~/api'; +import { DraftTournamentPairing } from '../TournamentPairingsGrid.types'; /** * Checks if a DraftPairing is valid. diff --git a/src/components/TournamentPairingsGrid/TournamentPairingsGrid.tsx b/src/components/TournamentPairingsGrid/TournamentPairingsGrid.tsx index 92e6588e..6d8b840b 100644 --- a/src/components/TournamentPairingsGrid/TournamentPairingsGrid.tsx +++ b/src/components/TournamentPairingsGrid/TournamentPairingsGrid.tsx @@ -1,5 +1,7 @@ import { + forwardRef, useEffect, + useImperativeHandle, useMemo, useState, } from 'react'; @@ -11,20 +13,22 @@ import { rectIntersection, } from '@dnd-kit/core'; import { restrictToWindowEdges } from '@dnd-kit/modifiers'; +import isEqual from 'fast-deep-equal'; import { AnimatePresence } from 'framer-motion'; -import { DraftTournamentPairing, TournamentCompetitorId } from '~/api'; +import { TournamentCompetitorId, TournamentCompetitorRanked } from '~/api'; import { Label } from '~/components/generic/Label'; -import { - buildGridState, - buildPairingResult, - convertPairingResultToCompetitorList, -} from '~/components/TournamentPairingsGrid/TournamentPairingsGrid.utils'; import { useTournament } from '~/components/TournamentProvider'; import { Draggable } from './Draggable'; import { Droppable } from './Droppable'; import { PairableCompetitorCard } from './PairableCompetitorCard'; import { PairingsGridRow } from './PairingsGridRow'; +import { DraftTournamentPairing, PairingsGridState } from './TournamentPairingsGrid.types'; +import { + buildGridState, + buildPairingResult, + convertPairingResultToCompetitorList, +} from './TournamentPairingsGrid.utils'; import styles from './TournamentPairingsGrid.module.scss'; @@ -45,22 +49,50 @@ const grabAnimationProps = { }; export interface TournamentPairingsGridProps { - value?: DraftTournamentPairing[]; + defaultValue?: DraftTournamentPairing[]; onChange: (value: DraftTournamentPairing[]) => void; } -export const TournamentPairingsGrid = ({ - value, +export interface TournamentPairingsGridHandle { + reset: (pairings: DraftTournamentPairing[]) => void; + isDirty: boolean; +} + +export const TournamentPairingsGrid = forwardRef(({ + defaultValue, onChange, -}: TournamentPairingsGridProps): JSX.Element => { +}: TournamentPairingsGridProps, ref): JSX.Element => { const tournament = useTournament(); const pairingIndexes = Array.from({ length: Math.ceil(tournament.maxCompetitors / 2) }, (_, i) => i); - // Store competitors with their opponentIds so we can check pairing validity - const competitors = useMemo(() => convertPairingResultToCompetitorList(value), [value]); - const state = useMemo(() => buildGridState(value), [value]); + // Store competitors with their opponentIds so we can check pairing validity: + const competitors = useMemo(() => convertPairingResultToCompetitorList(defaultValue), [defaultValue]); + // State: const [activeCompetitorId, setActiveCompetitorId] = useState(null); + const [gridState, setGridState] = useState(null); + + // Set internal state from parent: + useEffect(() => { + if (defaultValue && !gridState) { + setGridState(buildGridState(defaultValue)); + } + }, [defaultValue, gridState]); + + const pairingResult = useMemo(() => buildPairingResult(competitors, gridState), [competitors, gridState]); + const isDirty = !isEqual(defaultValue, pairingResult); + + // Emit change to parent components: + useEffect(() => { + onChange(pairingResult); + }, [pairingResult, onChange]); + + // Allow parent to reset and track dirty state: + useImperativeHandle(ref, () => ({ + reset: (pairings: DraftTournamentPairing[]): void => setGridState(buildGridState(pairings)), + pairingResult, + isDirty, + })); useEffect(() => { document.body.style.cursor = activeCompetitorId ? 'grabbing' : 'default'; @@ -76,28 +108,36 @@ export const TournamentPairingsGrid = ({ }; const handleDragEnd = ({ active, over }: DragEndEvent) => { - if (!over) { + if (!over || !gridState) { return; } setActiveCompetitorId(null); - const updatedInternalState = Object.entries(state).map(([pairingCompetitorId, slotId]) => { - if (pairingCompetitorId === active.id) { - return [pairingCompetitorId, over.id]; + setGridState(Object.entries(gridState).map(([competitorId, slotId]) => { + + // If this ID is the active one, we're dragging it. Set it's slotID to 'over': + if (competitorId === active.id) { + return [competitorId, over.id]; } + + // If this slot is the target, move its competitor to 'unpaired': if (slotId === over.id) { - return [pairingCompetitorId, 'unpaired']; + return [competitorId, 'unpaired']; } - return [pairingCompetitorId, slotId]; + + // Otherwise do nothing: + return [competitorId, slotId]; }).reduce((acc, [pairingCompetitorId, slotId]) => ({ ...acc, [pairingCompetitorId as TournamentCompetitorId]: slotId, - }), {}); - onChange(buildPairingResult(competitors, updatedInternalState)); + }), {})); }; - const fullPairings = (value || []).filter((pairing) => pairing[0] && pairing[1]); - const unpairedCompetitors = (value || []).filter((pairing) => !!pairing[0] && pairing[1] === null).map((pairing) => pairing[0]); + const unpairedCompetitors = competitors.filter((c) => gridState && gridState[c.id] === 'unpaired'); const activeCompetitor = competitors.find((c) => c.id === activeCompetitorId); + const gridStatePivoted = Object.entries(gridState ?? {}).reduce((acc, [competitorId, slotId]) => ({ + ...acc, + [slotId]: competitors.find((c) => c.id === competitorId), + }), {} as Record); return (
- {pairingIndexes.map((i) => ( - - ))} + {pairingIndexes.map((i) => { + const pairing: DraftTournamentPairing = [ + gridStatePivoted[`${i}_0`] ?? null, + gridStatePivoted[`${i}_1`] ?? null, + ]; + return ( + + ); + })}
@@ -148,4 +194,4 @@ export const TournamentPairingsGrid = ({ ); -}; +}); diff --git a/src/components/TournamentPairingsGrid/TournamentPairingsGrid.types.ts b/src/components/TournamentPairingsGrid/TournamentPairingsGrid.types.ts index 3e519e91..daf214a1 100644 --- a/src/components/TournamentPairingsGrid/TournamentPairingsGrid.types.ts +++ b/src/components/TournamentPairingsGrid/TournamentPairingsGrid.types.ts @@ -1,3 +1,5 @@ -import { TournamentCompetitorId } from '~/api'; +import { TournamentCompetitorId, TournamentCompetitorRanked } from '~/api'; export type PairingsGridState = Record; + +export type DraftTournamentPairing = [TournamentCompetitorRanked | null, TournamentCompetitorRanked | null]; diff --git a/src/components/TournamentPairingsGrid/TournamentPairingsGrid.utils.ts b/src/components/TournamentPairingsGrid/TournamentPairingsGrid.utils.ts index 2920f630..4baa4169 100644 --- a/src/components/TournamentPairingsGrid/TournamentPairingsGrid.utils.ts +++ b/src/components/TournamentPairingsGrid/TournamentPairingsGrid.utils.ts @@ -1,8 +1,5 @@ -import { - DraftTournamentPairing, - TournamentCompetitorId, - TournamentCompetitorRanked, -} from '~/api'; +import { TournamentCompetitorId, TournamentCompetitorRanked } from '~/api'; +import { DraftTournamentPairing, PairingsGridState } from './TournamentPairingsGrid.types'; export const convertPairingResultToCompetitorList = (draftPairings?: DraftTournamentPairing[]): TournamentCompetitorRanked[] => { if (!draftPairings) { @@ -25,22 +22,31 @@ export const buildGridState = (draftPairings?: DraftTournamentPairing[]): Record return {}; } return draftPairings.reduce((acc, pairing, i) => { - if (!pairing[1]) { + if (pairing[0] && !pairing[1]) { return { ...acc, [pairing[0].id]: 'unpaired', }; } - return { - ...acc, - [pairing[0].id]: `${i}_0`, - [pairing[1].id]: `${i}_1`, - }; + if (!pairing[0] && pairing[1]) { + return { + ...acc, + [pairing[1].id]: 'unpaired', + }; + } + if (pairing[0] && pairing[1]) { + return { + ...acc, + [pairing[0].id]: `${i}_0`, + [pairing[1].id]: `${i}_1`, + }; + } + return acc; }, {} as Record); }; -export const buildPairingResult = (competitors: TournamentCompetitorRanked[], state: Record): DraftTournamentPairing[] => { - if (!competitors?.length || !Object.keys(state).length) { +export const buildPairingResult = (competitors: TournamentCompetitorRanked[], state: PairingsGridState | null): DraftTournamentPairing[] => { + if (!competitors?.length || !state || !Object.keys(state).length) { return []; } const statefulCompetitors = competitors.map((competitor) => ({ diff --git a/src/components/TournamentPairingsGrid/index.ts b/src/components/TournamentPairingsGrid/index.ts index 954c5aee..0a628319 100644 --- a/src/components/TournamentPairingsGrid/index.ts +++ b/src/components/TournamentPairingsGrid/index.ts @@ -1 +1,6 @@ -export { TournamentPairingsGrid } from './TournamentPairingsGrid'; +export { + TournamentPairingsGrid, + type TournamentPairingsGridHandle, + type TournamentPairingsGridProps, +} from './TournamentPairingsGrid'; +export type { DraftTournamentPairing } from './TournamentPairingsGrid.types'; diff --git a/src/components/TournamentRoster/TournamentRoster.module.scss b/src/components/TournamentRoster/TournamentRoster.module.scss index fc2038af..548343ef 100644 --- a/src/components/TournamentRoster/TournamentRoster.module.scss +++ b/src/components/TournamentRoster/TournamentRoster.module.scss @@ -8,14 +8,25 @@ .TournamentRoster { &_Header { - @include flex.row; - @include flex.stretchy; + display: grid; + grid-template-areas: "Identity PlayerCount Actions"; + grid-template-columns: 1fr auto auto; + grid-template-rows: 2.5rem; + gap: 1rem; - &_Actions { - @include flex.row; + width: 100%; + } + + &_Identity { + grid-area: Identity; + } + + &_PlayerCount { + grid-area: PlayerCount; + } - margin-left: auto; - } + &_Actions { + grid-area: Actions; } &_Content { diff --git a/src/components/TournamentRoster/TournamentRoster.tsx b/src/components/TournamentRoster/TournamentRoster.tsx index e6156db9..5f2256c3 100644 --- a/src/components/TournamentRoster/TournamentRoster.tsx +++ b/src/components/TournamentRoster/TournamentRoster.tsx @@ -4,6 +4,7 @@ import { TournamentCompetitorEditDialog } from '~/components/TournamentCompetito import { useTournamentCompetitors } from '~/components/TournamentCompetitorsProvider'; import { useTournament } from '~/components/TournamentProvider'; import { CompetitorActions } from '~/components/TournamentRoster/components/CompetitorActions'; +import { PlayerCount } from '~/components/TournamentRoster/components/PlayerCount'; import styles from './TournamentRoster.module.scss'; @@ -14,7 +15,7 @@ export interface TournamentRosterProps { export const TournamentRoster = ({ className, }: TournamentRosterProps): JSX.Element => { - const { useTeams } = useTournament(); + const { useTeams, competitorSize } = useTournament(); const competitors = useTournamentCompetitors(); return ( <> @@ -22,8 +23,11 @@ export const TournamentRoster = ({ {(competitors || []).map((competitor) => (
- - + + {useTeams && ( + + )} +
{competitor.players.filter((player) => player.active).map((player) => ( diff --git a/src/components/TournamentRoster/components/CompetitorActions/CompetitorActions.module.scss b/src/components/TournamentRoster/components/CompetitorActions/CompetitorActions.module.scss index 09e4aabc..4efbf490 100644 --- a/src/components/TournamentRoster/components/CompetitorActions/CompetitorActions.module.scss +++ b/src/components/TournamentRoster/components/CompetitorActions/CompetitorActions.module.scss @@ -9,5 +9,5 @@ .CompetitorActions { @include flex.row; - margin-left: auto; + flex-shrink: 0; } diff --git a/src/components/TournamentRoster/components/CompetitorActions/CompetitorActions.tsx b/src/components/TournamentRoster/components/CompetitorActions/CompetitorActions.tsx index 2bb51ebe..15028b42 100644 --- a/src/components/TournamentRoster/components/CompetitorActions/CompetitorActions.tsx +++ b/src/components/TournamentRoster/components/CompetitorActions/CompetitorActions.tsx @@ -1,4 +1,5 @@ import { MouseEvent } from 'react'; +import clsx from 'clsx'; import { Ellipsis, UserPlus } from 'lucide-react'; import { TournamentCompetitor } from '~/api'; @@ -20,10 +21,12 @@ import { import styles from './CompetitorActions.module.scss'; export interface CompetitorActionsProps { + className?: string; competitor: TournamentCompetitor; } export const CompetitorActions = ({ + className, competitor, }: CompetitorActionsProps): JSX.Element => { const user = useAuth(); @@ -76,17 +79,17 @@ export const CompetitorActions = ({ playerUserId: user!._id, tournamentCompetitorId: competitor._id, }), - visible: user && isPlayer && tournament.status !== 'archived', + visible: isPlayer && !['active', 'archived'].includes(tournament.status), }, { label: 'Edit', onClick: () => openEditDialog({ tournamentCompetitor: competitor }), - visible: user && (isOrganizer || (isPlayer && tournament.useTeams)) && tournament.status !== 'archived' && tournament.currentRound === undefined, + visible: (isOrganizer || (isPlayer && tournament.useTeams)) && tournament.status !== 'archived' && tournament.currentRound === undefined, }, { label: 'Delete', onClick: () => openConfirmDeleteDialog(), - visible: user && isOrganizer && tournament.status === 'published', + visible: isOrganizer && tournament.status === 'published', }, ]; @@ -94,7 +97,7 @@ export const CompetitorActions = ({ return ( <> -
+
{showCheckInToggle && ( <> diff --git a/src/components/TournamentRoster/components/PlayerCount/PlayerCount.module.scss b/src/components/TournamentRoster/components/PlayerCount/PlayerCount.module.scss new file mode 100644 index 00000000..e6c4b647 --- /dev/null +++ b/src/components/TournamentRoster/components/PlayerCount/PlayerCount.module.scss @@ -0,0 +1,12 @@ +@use "/src/style/flex"; +@use "/src/style/text"; + +.PlayerCount { + @include flex.row($gap: 0.25rem); + @include text.ui($muted: true); + + svg { + width: 1rem; + height: 1rem; + } +} diff --git a/src/components/TournamentRoster/components/PlayerCount/PlayerCount.tsx b/src/components/TournamentRoster/components/PlayerCount/PlayerCount.tsx new file mode 100644 index 00000000..c5258d6c --- /dev/null +++ b/src/components/TournamentRoster/components/PlayerCount/PlayerCount.tsx @@ -0,0 +1,26 @@ +import clsx from 'clsx'; +import { Users } from 'lucide-react'; + +import { TournamentCompetitor } from '~/api'; + +import styles from './PlayerCount.module.scss'; + +export interface PlayerCountProps { + className?: string; + competitor: TournamentCompetitor; + competitorSize: number; +} + +export const PlayerCount = ({ + className, + competitor, + competitorSize, +}: PlayerCountProps): JSX.Element => { + const playerCount = (competitor.players ?? []).filter((p) => p.active).length; + return ( +
+ + {`${playerCount}/${competitorSize}`} +
+ ); +}; diff --git a/src/components/TournamentRoster/components/PlayerCount/index.ts b/src/components/TournamentRoster/components/PlayerCount/index.ts new file mode 100644 index 00000000..6e47b7b8 --- /dev/null +++ b/src/components/TournamentRoster/components/PlayerCount/index.ts @@ -0,0 +1,2 @@ +export type { PlayerCountProps } from './PlayerCount'; +export { PlayerCount } from './PlayerCount'; diff --git a/src/components/generic/Accordion/AccordionItem.module.scss b/src/components/generic/Accordion/AccordionItem.module.scss index 973e6062..10256714 100644 --- a/src/components/generic/Accordion/AccordionItem.module.scss +++ b/src/components/generic/Accordion/AccordionItem.module.scss @@ -13,17 +13,21 @@ overflow: hidden; &_Header { - @include flex.row($gap: 1rem); - @include variants.ghost; - - cursor: pointer; padding: calc(1rem - var(--border-width)); + &[data-enabled="true"] { + @include variants.ghost; + + cursor: pointer; + } + &_Chevron { place-self: center; width: 1.5rem; height: 1.5rem; } + + @include flex.row($gap: 1rem); } &_Content { diff --git a/src/components/generic/Accordion/AccordionItem.tsx b/src/components/generic/Accordion/AccordionItem.tsx index 5b874093..01ea648a 100644 --- a/src/components/generic/Accordion/AccordionItem.tsx +++ b/src/components/generic/Accordion/AccordionItem.tsx @@ -41,15 +41,18 @@ export const AccordionItem = ({
- - - + {!disabled && ( + + + + )} {header}
diff --git a/src/components/generic/Card/Card.module.scss b/src/components/generic/Card/Card.module.scss index 06b28ac2..a38e3dd4 100644 --- a/src/components/generic/Card/Card.module.scss +++ b/src/components/generic/Card/Card.module.scss @@ -8,11 +8,8 @@ .Card { @include flex.column($gap: 0); @include variants.card; - @include shadows.surface; - @include corners.normal; @include flex.stretchy; min-height: 0; max-height: 100%; - background-color: var(--card-bg); } diff --git a/src/components/generic/DataTable/DataTable.module.scss b/src/components/generic/DataTable/DataTable.module.scss index 88c21621..bfd4bbf8 100644 --- a/src/components/generic/DataTable/DataTable.module.scss +++ b/src/components/generic/DataTable/DataTable.module.scss @@ -27,14 +27,11 @@ $row-height: 3rem; } .PopoverContent { - @include shadows.elevated; @include animate.duration-quick; - @include variants.card; + @include variants.card($elevated: true); margin: 0.25rem 0; padding: 1rem; - background-color: var(--card-bg); - border-radius: variables.$corner-radius; @include animate.style-pop; // Must list last because it contains nested declarations } diff --git a/src/components/generic/InputDateTime/InputDateTime.module.scss b/src/components/generic/InputDateTime/InputDateTime.module.scss index e8c25663..3d4c364e 100644 --- a/src/components/generic/InputDateTime/InputDateTime.module.scss +++ b/src/components/generic/InputDateTime/InputDateTime.module.scss @@ -20,8 +20,7 @@ } .Content { - @include variants.card; - @include shadows.elevated; + @include variants.card($elevated: true); @include animate.duration-quick; @include animate.style-pop; @include flex.column($gap: 0); diff --git a/src/components/generic/InputLocation/InputLocation.module.scss b/src/components/generic/InputLocation/InputLocation.module.scss index a88ed4cb..ef0e349b 100644 --- a/src/components/generic/InputLocation/InputLocation.module.scss +++ b/src/components/generic/InputLocation/InputLocation.module.scss @@ -6,8 +6,7 @@ @use "/src/style/flex"; .InputDatePopoverContent { - @include variants.card; - @include shadows.elevated; + @include variants.card($elevated: true); @include animate.duration-quick; @include animate.style-pop; diff --git a/src/components/generic/PopoverMenu/PopoverMenu.module.scss b/src/components/generic/PopoverMenu/PopoverMenu.module.scss index 7ecad3cd..862c84d1 100644 --- a/src/components/generic/PopoverMenu/PopoverMenu.module.scss +++ b/src/components/generic/PopoverMenu/PopoverMenu.module.scss @@ -10,9 +10,7 @@ .PopoverContent { @include flex.column($gap: 0.25rem); @include animate.duration-quick; - @include corners.normal; - @include shadows.elevated; - @include variants.card; + @include variants.card($elevated: true); z-index: 1; margin: 0.25rem 0; diff --git a/src/pages/AuthPage/components/SignInForm/SignInForm.schema.ts b/src/pages/AuthPage/components/SignInForm/SignInForm.schema.ts index a80f008c..f43ba2a2 100644 --- a/src/pages/AuthPage/components/SignInForm/SignInForm.schema.ts +++ b/src/pages/AuthPage/components/SignInForm/SignInForm.schema.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; export const signInFormSchema = z.object({ - email: z.string().min(1, 'Please enter your email.').transform((val) => val.trim().toLowerCase()), + email: z.string().email('Please enter your email.').transform((val) => val.trim().toLowerCase()), password: z.string().min(1, 'Please enter your password.').transform((val) => val.trim()), }); diff --git a/src/pages/TournamentAdvanceRoundPage/components/ConfirmPairingsDialog/ConfirmPairingsDialog.tsx b/src/pages/TournamentAdvanceRoundPage/components/ConfirmPairingsDialog/ConfirmPairingsDialog.tsx index 705d68cd..e2b93281 100644 --- a/src/pages/TournamentAdvanceRoundPage/components/ConfirmPairingsDialog/ConfirmPairingsDialog.tsx +++ b/src/pages/TournamentAdvanceRoundPage/components/ConfirmPairingsDialog/ConfirmPairingsDialog.tsx @@ -1,6 +1,7 @@ -import { DraftTournamentPairing, UnassignedTournamentPairing } from '~/api'; +import { UnassignedTournamentPairing } from '~/api'; import { ConfirmationDialog } from '~/components/ConfirmationDialog'; import { TournamentPairingRow } from '~/components/TournamentPairingRow'; +import { DraftTournamentPairing } from '~/components/TournamentPairingsGrid'; import { convertDraftPairingsToUnassignedPairings } from '../PairingsStep/PairingsStep.utils'; import styles from './ConfirmPairingsDialog.module.scss'; diff --git a/src/pages/TournamentAdvanceRoundPage/components/PairingsStep/PairingsStep.tsx b/src/pages/TournamentAdvanceRoundPage/components/PairingsStep/PairingsStep.tsx index a63bbfa5..3e02f25b 100644 --- a/src/pages/TournamentAdvanceRoundPage/components/PairingsStep/PairingsStep.tsx +++ b/src/pages/TournamentAdvanceRoundPage/components/PairingsStep/PairingsStep.tsx @@ -2,12 +2,11 @@ import { forwardRef, useEffect, useImperativeHandle, + useRef, useState, } from 'react'; -import isEqual from 'fast-deep-equal'; import { - DraftTournamentPairing, TournamentPairingMethod, tournamentPairingMethodOptions, UnassignedTournamentPairing, @@ -17,7 +16,11 @@ import { Button } from '~/components/generic/Button'; import { InputSelect } from '~/components/generic/InputSelect'; import { Label } from '~/components/generic/Label'; import { Separator } from '~/components/generic/Separator'; -import { TournamentPairingsGrid } from '~/components/TournamentPairingsGrid'; +import { + DraftTournamentPairing, + TournamentPairingsGrid, + TournamentPairingsGridHandle, +} from '~/components/TournamentPairingsGrid'; import { useTournament } from '~/components/TournamentProvider'; import { useGetDraftTournamentPairings } from '~/services/tournamentPairings'; import { ConfirmPairingsDialog, confirmPairingsDialogId } from '../ConfirmPairingsDialog'; @@ -39,7 +42,7 @@ export interface PairingsStepHandle { export const PairingsStep = forwardRef(({ nextRound, onConfirm, -}: PairingsStepProps, ref) => { +}: PairingsStepProps, ref): JSX.Element => { const tournament = useTournament(); // Pairing state @@ -51,14 +54,15 @@ export const PairingsStep = forwardRef(({ round: nextRound, method: pairingMethod, }); - const [manualPairings, setManualPairings] = useState(draftPairingResults); + const [manualPairings, setManualPairings] = useState(); useEffect(() => { if (draftPairingResults) { setManualPairings(draftPairingResults); } }, [draftPairingResults]); - const isDirty = manualPairings && !isEqual(manualPairings, draftPairingResults); + const pairingsGridRef = useRef(null); + const isDirty = pairingsGridRef.current?.isDirty ?? false; const { open: openChangePairingMethodConfirmDialog } = useConfirmationDialog(changePairingMethodConfirmDialogId); const { open: openResetPairingsConfirmDialog } = useConfirmationDialog(resetPairingsConfirmDialogId); @@ -78,7 +82,7 @@ export const PairingsStep = forwardRef(({ if (draftPairingResults) { if (isDirty) { openResetPairingsConfirmDialog({ - onConfirm: () => setManualPairings(draftPairingResults), + onConfirm: () => pairingsGridRef.current?.reset(draftPairingResults), }); } else { setManualPairings(draftPairingResults); @@ -102,12 +106,12 @@ export const PairingsStep = forwardRef(({ value={pairingMethod} disabled={isFirstRound} /> -
- + [ - ...draftTournamentPairings.map((tournamentPairing) => ({ - tournamentCompetitor0Id: tournamentPairing[0].id, - tournamentCompetitor1Id: tournamentPairing[1]?.id ?? null, - playedTables: Array.from(new Set([ - ...tournamentPairing[0].playedTables, - ...(tournamentPairing[1]?.playedTables || []), - ])), - })), -]; +): UnassignedTournamentPairing[] => draftTournamentPairings.filter(([a, b]) => a || b).map(([a, b]) => { + const playedTables = Array.from( + new Set([ + ...(a?.playedTables ?? []), + ...(b?.playedTables ?? []), + ]), + ); + if (a && !b) { + return { + tournamentCompetitor0Id: a.id, + tournamentCompetitor1Id: null, + playedTables, + }; + } + if (!a && b) { + return { + tournamentCompetitor0Id: b.id, + tournamentCompetitor1Id: null, + playedTables, + }; + } + // We've filtered out pairings with no competitors, and handled the one-sided ones above: + return { + tournamentCompetitor0Id: a!.id, + tournamentCompetitor1Id: b!.id, + playedTables, + }; +}); diff --git a/src/pages/TournamentsPage/TournamentsPage.module.scss b/src/pages/TournamentsPage/TournamentsPage.module.scss index 2c656ae7..64fe4150 100644 --- a/src/pages/TournamentsPage/TournamentsPage.module.scss +++ b/src/pages/TournamentsPage/TournamentsPage.module.scss @@ -11,15 +11,12 @@ } .FilterPopover { - @include variants.card; - @include shadows.elevated; + @include variants.card($elevated: true); @include animate.duration-quick; @include animate.style-pop; margin: 0.25rem 0; padding: 1rem; - background-color: var(--card-bg); - border-radius: variables.$corner-radius; } .List { diff --git a/src/services/auth/useSignIn.ts b/src/services/auth/useSignIn.ts index becc3372..335ac107 100644 --- a/src/services/auth/useSignIn.ts +++ b/src/services/auth/useSignIn.ts @@ -14,6 +14,19 @@ type SignInInput = { password: string; }; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function mapConvexAuthError(error: any): string { + if (!error?.message) { + return 'An unexpected error occurred. Please try again.'; + } + + if (error.message.includes('InvalidAccountId')) { + return 'Your email or password is incorrect.'; + } + + return 'Sign-in failed. Please check your details and try again.'; +} + export const useSignIn = () => { const { signIn } = useAuthActions(); const navigate = useNavigate(); @@ -47,8 +60,9 @@ export const useSignIn = () => { flow: 'signIn', }).catch((error) => { setLoading(false); + const description = mapConvexAuthError(error); console.error(error); - toast.error('Error', { description: error.message }); + toast.error('Error', { description: description }); }); }, loading, diff --git a/src/style/_variants.scss b/src/style/_variants.scss index df383775..f840d409 100644 --- a/src/style/_variants.scss +++ b/src/style/_variants.scss @@ -1,5 +1,7 @@ @use "variables"; @use "/src/style/borders"; +@use "/src/style/corners"; +@use "/src/style/shadows"; // TODO: Move to utils @mixin reset { @@ -190,9 +192,14 @@ } } -@mixin card { +@mixin card($elevated: false) { @include borders.normal; + @include corners.normal; + @include shadows.surface; background-color: var(--card-bg); - border-color: var(--border-color-default); + + @if $elevated { + @include shadows.elevated; + } }