From 8a4db3e220562fcb377e2f4272bac4acce3d637a Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Sun, 21 Dec 2025 11:06:20 +0100 Subject: [PATCH 1/7] WIP --- .../createMockTournamentCompetitor.ts | 3 + convex/_generated/api.d.ts | 18 +- .../_model/common/_helpers/getCountryName.ts | 20 + convex/_model/common/errors.ts | 3 + .../_helpers/deepenTournamentCompetitor.ts | 10 +- .../_helpers/getAvailableActions.ts | 85 + .../_helpers/getDisplayName.ts | 38 + convex/_model/tournamentCompetitors/index.ts | 10 + .../queries/getTournamentCompetitor.ts | 3 +- convex/_model/tournamentCompetitors/table.ts | 8 +- .../_helpers/deepenTournamentRegistration.ts | 21 +- .../_helpers/getAvailableActions.ts | 86 + .../_model/tournamentRegistrations/index.ts | 26 +- .../mutations/createTournamentRegistration.ts | 75 +- .../mutations/deleteTournamentRegistration.ts | 44 +- .../mutations/toggleActive.ts | 24 +- ...tTournamentRegistrationByTournamentUser.ts | 23 + .../getTournamentRegistrationsByCompetitor.ts | 19 +- .../_model/tournamentRegistrations/types.ts | 5 + .../tournaments/_helpers/deepenTournament.ts | 8 +- .../_helpers/getAvailableActions.ts | 124 + .../tournaments/_helpers/getDisplayName.ts | 10 + convex/_model/tournaments/index.ts | 21 +- .../queries/getAvailableTournamentActions.ts | 102 - convex/_model/tournaments/table.ts | 6 + convex/_model/users/queries/getUsers.ts | 32 +- .../users/queries/internal/createIdFilter.ts | 30 + convex/_model/utils/createTestTournament.ts | 1 + convex/tournamentRegistrations.ts | 5 + convex/tournaments.ts | 5 - package-lock.json | 3224 +++++------------ package.json | 5 +- src/api.ts | 5 + src/components/App/App.hooks.ts | 20 +- src/components/App/App.tsx | 24 +- .../AvatarEditable/AvatarEditable.tsx | 2 +- src/components/ContextMenu/ContextMenu.tsx | 35 + .../ContextMenu/ContextMenu.types.ts | 21 + src/components/ContextMenu/index.ts | 7 + .../FloatingActionButton.tsx | 2 +- src/components/HeartToggle/HeartToggle.tsx | 2 +- .../IdentityBadge/IdentityBadge.hooks.tsx | 33 +- .../IdentityBadge/IdentityBadge.module.scss | 4 +- .../IdentityBadge/IdentityBadge.tsx | 23 +- .../TournamentCompetitorAvatar.tsx | 51 + src/components/IdentityBadge/index.ts | 3 + .../InputUser/InputUser.module.scss | 110 +- src/components/InputUser/InputUser.tsx | 249 +- src/components/InputUser/index.ts | 5 +- .../MatchResultCard/MatchResultCard.tsx | 52 +- .../MatchResultCreateDialog.tsx | 4 +- .../MatchResultEditDialog.tsx | 2 +- .../TournamentPlayerFields.tsx | 5 +- .../MatchResultPhotos/MatchResultPhotos.tsx | 2 +- .../PaginatedList/PaginatedList.module.scss | 5 + .../PaginatedList/PaginatedList.tsx | 49 + src/components/PaginatedList/index.ts | 4 + .../TournamentActionsProvider.context.ts | 12 - .../TournamentActionsProvider.hooks.tsx | 249 -- .../TournamentActionsProvider.tsx | 24 - .../TournamentActionsProvider/index.ts | 2 - .../TournamentBanner/TournamentBanner.tsx | 3 +- .../TournamentCard/TournamentCard.tsx | 64 +- .../TournamentCompetitorEditDialog.hooks.ts | 21 - .../TournamentCompetitorEditDialog.tsx | 114 - .../TournamentCompetitorEditDialog/index.ts | 2 - .../TournamentCompetitorForm.module.scss | 4 + .../TournamentCompetitorForm.schema.ts | 52 +- .../TournamentCompetitorForm.tsx | 166 +- .../ScoreAdjustmentFormItem.tsx | 5 +- .../ScoreAdjustmentFormItem.types.ts | 4 +- .../ScoreAdjustmentFormItem/index.ts | 1 + .../TournamentCompetitorForm/index.ts | 2 +- .../TournamentCompetitorActiveToggle.tsx | 23 + .../TournamentCompetitorContextMenu.tsx | 25 + .../TournamentCompetitorPlayerCount.tsx | 22 + .../TournamentCompetitorProvider.context.ts | 7 + .../TournamentCompetitorProvider.hooks.tsx | 33 + .../TournamentCompetitorProvider.tsx | 18 + .../actions/useAddPlayerAction.tsx | 47 + .../actions/useDeleteAction.tsx | 56 + .../actions/useEditAction.tsx | 48 + .../actions/useJoinAction.tsx | 36 + .../actions/useLeaveAction.tsx | 76 + .../actions/useToggleActiveAction.tsx | 22 + .../TournamentCompetitorProvider/index.ts | 24 + .../TournamentContextMenu.tsx | 32 - src/components/TournamentContextMenu/index.ts | 1 - .../TournamentForm/TournamentForm.schema.ts | 1 - .../components/GeneralFields.tsx | 4 +- .../components/RankingFactorFields.tsx | 4 +- .../TournamentPairingRow.tsx | 4 + .../TournamentContextMenu.tsx | 38 + .../TournamentProvider.context.ts | 4 +- .../TournamentProvider.hooks.ts | 11 - .../TournamentProvider.hooks.tsx | 47 + .../TournamentProvider/TournamentProvider.tsx | 6 +- .../actions/useAddPlayerAction.tsx | 44 + .../actions/useConfigureRoundAction.tsx | 61 + .../actions/useDeleteAction.tsx | 46 + .../actions/useEditAction.tsx | 22 + .../actions/useEndAction.tsx | 50 + .../actions/useEndRoundAction.tsx | 55 + .../actions/useJoinAction.tsx | 49 + .../actions/useLeaveAction.tsx | 75 + .../actions/usePublishAction.tsx | 25 + .../actions/useStartAction.tsx | 25 + .../actions/useStartRoundAction.tsx | 25 + .../actions/useSubmitMatchResultAction.tsx | 21 + .../actions/useUndoStartRoundAction.tsx | 53 + src/components/TournamentProvider/index.ts | 27 +- .../utils/validateConfigureRound.tsx | 12 +- .../TournamentRegistrationForm.module.scss} | 5 +- .../TournamentRegistrationForm.schema.ts | 39 + .../TournamentRegistrationForm.tsx | 170 + .../TournamentRegistrationForm/index.ts | 4 + .../TournamentRegistrationActiveToggle.tsx | 23 + .../TournamentRegistrationContextMenu.tsx | 21 + ...TournamentRegistrationProvider.context.tsx | 7 + .../TournamentRegistrationProvider.hooks.ts | 34 + .../TournamentRegistrationProvider.tsx | 18 + .../actions/useDeleteAction.tsx | 65 + .../actions/useToggleActiveAction.tsx | 27 + .../TournamentRegistrationProvider/index.ts | 19 + .../TournamentRoster.module.scss | 48 - .../TournamentRoster/TournamentRoster.tsx | 66 - .../CompetitorActions.module.scss | 13 - .../CompetitorActions/CompetitorActions.tsx | 138 - .../components/CompetitorActions/index.ts | 1 - .../PlayerCount/PlayerCount.module.scss | 12 - .../components/PlayerCount/PlayerCount.tsx | 26 - .../components/PlayerCount/index.ts | 2 - src/components/TournamentRoster/index.ts | 1 - src/components/generic/Avatar/Avatar.tsx | 22 +- .../generic/Button/Button.module.scss | 16 + src/components/generic/Button/Button.tsx | 9 +- .../generic/Carousel/CarouselNextButton.tsx | 2 +- .../Carousel/CarouselPreviousButton.tsx | 2 +- .../generic/InputCurrency/InputCurrency.tsx | 2 +- .../generic/InputSelect/InputSelect.tsx | 10 +- .../PopoverMenu/PopoverMenu.module.scss | 33 +- .../generic/PopoverMenu/PopoverMenu.tsx | 12 +- .../generic/ScrollArea/ScrollArea.hooks.tsx | 39 +- .../generic/ScrollArea/ScrollArea.tsx | 12 +- src/components/generic/Table/README.md | 11 - .../generic/Table/Table.module.scss | 78 - src/components/generic/Table/Table.tsx | 32 - src/components/generic/Table/Table.types.ts | 15 - src/components/generic/Table/TableCell.tsx | 43 - src/components/generic/Table/TableRow.tsx | 44 - src/components/generic/Table/index.ts | 3 - .../generic/Tabs/TabsList.module.scss | 17 +- src/components/generic/Tabs/TabsList.tsx | 43 +- src/components/generic/Tabs/TabsTrigger.scss | 5 +- src/components/generic/Tag/Tag.scss | 7 +- src/hooks/useDebouncedState.ts | 40 + src/hooks/useDialogInstance.ts | 29 + src/hooks/useFormDialog.tsx | 73 + src/hooks/useNavigateAway.ts | 15 + .../ActiveTournament.module.scss | 7 +- .../ActiveTournament/ActiveTournament.tsx | 7 +- .../ActiveTournament.utils.tsx | 8 +- .../LeagueRankingsCard.module.scss | 9 +- .../LeagueRankingsCard/LeagueRankingsCard.tsx | 4 +- .../LeagueRankingsCard.utils.tsx | 15 +- ...TournamentCompetitorDetailPage.module.scss | 156 + .../TournamentCompetitorDetailPage.tsx | 110 + .../components/Header/Header.module.scss | 77 + .../components/Header/Header.tsx | 70 + .../components/Header/index.ts | 4 + .../MatchResultsList.module.scss | 27 + .../MatchResultsList/MatchResultsList.tsx | 56 + .../components/MatchResultsList/index.ts | 4 + .../TournamentRegistrationsTable.module.scss | 16 + .../TournamentRegistrationsTable.tsx | 76 + .../TournamentRegistrationsTable/index.ts | 2 + .../TournamentCompetitorDetailPage/index.ts | 2 + .../TournamentDetailPage.tsx | 87 +- .../TournamentExportDataDialog.tsx | 3 +- .../TournamentPairingsCard.module.scss | 8 +- .../TournamentPairingsCard.tsx | 22 +- .../TournamentPairingsCard.utils.tsx | 20 +- .../TournamentRankingsCard.module.scss | 9 +- .../TournamentRankingsCard.tsx | 4 +- .../TournamentRankingsCard.utils.tsx | 14 +- .../TournamentRosterCard.module.scss | 24 +- .../TournamentRosterCard.tsx | 98 +- .../TournamentRosterCard.utils.tsx | 59 + .../TournamentPairingsPage.utils.tsx | 13 +- .../ConfirmPairingsDialog.module.scss | 17 +- .../ConfirmPairingsDialog.tsx | 5 +- .../ConfirmPairingsDialog.utils.tsx | 25 +- .../UserTournamentItem/UserTournamentItem.tsx | 6 +- .../UserTournamentsCard.tsx | 2 +- src/routes.tsx | 6 + src/services/tournamentRegistrations.ts | 1 + src/services/tournaments.ts | 1 - src/services/utils/createMutationHook.ts | 17 +- .../utils/createPaginatedQueryHook.ts | 33 +- src/settings.ts | 12 +- src/style/_borders.scss | 4 + src/style/_text.scss | 3 + src/style/_variants.scss | 1 + src/utils/common/getPathWithQuery.ts | 8 + .../getTournamentCompetitorDisplayName.ts | 31 - .../common/getTournamentPairingDisplayName.ts | 5 +- .../isUserTournamentCompetitorCaptain.ts | 11 + src/utils/validateForm.ts | 6 +- 208 files changed, 4975 insertions(+), 4300 deletions(-) create mode 100644 convex/_model/common/_helpers/getCountryName.ts create mode 100644 convex/_model/tournamentCompetitors/_helpers/getAvailableActions.ts create mode 100644 convex/_model/tournamentCompetitors/_helpers/getDisplayName.ts create mode 100644 convex/_model/tournamentRegistrations/_helpers/getAvailableActions.ts create mode 100644 convex/_model/tournamentRegistrations/queries/getTournamentRegistrationByTournamentUser.ts create mode 100644 convex/_model/tournaments/_helpers/getAvailableActions.ts create mode 100644 convex/_model/tournaments/_helpers/getDisplayName.ts delete mode 100644 convex/_model/tournaments/queries/getAvailableTournamentActions.ts create mode 100644 convex/_model/users/queries/internal/createIdFilter.ts create mode 100644 src/components/ContextMenu/ContextMenu.tsx create mode 100644 src/components/ContextMenu/ContextMenu.types.ts create mode 100644 src/components/ContextMenu/index.ts create mode 100644 src/components/IdentityBadge/TournamentCompetitorAvatar.tsx create mode 100644 src/components/PaginatedList/PaginatedList.module.scss create mode 100644 src/components/PaginatedList/PaginatedList.tsx create mode 100644 src/components/PaginatedList/index.ts delete mode 100644 src/components/TournamentActionsProvider/TournamentActionsProvider.context.ts delete mode 100644 src/components/TournamentActionsProvider/TournamentActionsProvider.hooks.tsx delete mode 100644 src/components/TournamentActionsProvider/TournamentActionsProvider.tsx delete mode 100644 src/components/TournamentActionsProvider/index.ts delete mode 100644 src/components/TournamentCompetitorEditDialog/TournamentCompetitorEditDialog.hooks.ts delete mode 100644 src/components/TournamentCompetitorEditDialog/TournamentCompetitorEditDialog.tsx delete mode 100644 src/components/TournamentCompetitorEditDialog/index.ts create mode 100644 src/components/TournamentCompetitorProvider/TournamentCompetitorActiveToggle.tsx create mode 100644 src/components/TournamentCompetitorProvider/TournamentCompetitorContextMenu.tsx create mode 100644 src/components/TournamentCompetitorProvider/TournamentCompetitorPlayerCount.tsx create mode 100644 src/components/TournamentCompetitorProvider/TournamentCompetitorProvider.context.ts create mode 100644 src/components/TournamentCompetitorProvider/TournamentCompetitorProvider.hooks.tsx create mode 100644 src/components/TournamentCompetitorProvider/TournamentCompetitorProvider.tsx create mode 100644 src/components/TournamentCompetitorProvider/actions/useAddPlayerAction.tsx create mode 100644 src/components/TournamentCompetitorProvider/actions/useDeleteAction.tsx create mode 100644 src/components/TournamentCompetitorProvider/actions/useEditAction.tsx create mode 100644 src/components/TournamentCompetitorProvider/actions/useJoinAction.tsx create mode 100644 src/components/TournamentCompetitorProvider/actions/useLeaveAction.tsx create mode 100644 src/components/TournamentCompetitorProvider/actions/useToggleActiveAction.tsx create mode 100644 src/components/TournamentCompetitorProvider/index.ts delete mode 100644 src/components/TournamentContextMenu/TournamentContextMenu.tsx delete mode 100644 src/components/TournamentContextMenu/index.ts create mode 100644 src/components/TournamentProvider/TournamentContextMenu.tsx delete mode 100644 src/components/TournamentProvider/TournamentProvider.hooks.ts create mode 100644 src/components/TournamentProvider/TournamentProvider.hooks.tsx create mode 100644 src/components/TournamentProvider/actions/useAddPlayerAction.tsx create mode 100644 src/components/TournamentProvider/actions/useConfigureRoundAction.tsx create mode 100644 src/components/TournamentProvider/actions/useDeleteAction.tsx create mode 100644 src/components/TournamentProvider/actions/useEditAction.tsx create mode 100644 src/components/TournamentProvider/actions/useEndAction.tsx create mode 100644 src/components/TournamentProvider/actions/useEndRoundAction.tsx create mode 100644 src/components/TournamentProvider/actions/useJoinAction.tsx create mode 100644 src/components/TournamentProvider/actions/useLeaveAction.tsx create mode 100644 src/components/TournamentProvider/actions/usePublishAction.tsx create mode 100644 src/components/TournamentProvider/actions/useStartAction.tsx create mode 100644 src/components/TournamentProvider/actions/useStartRoundAction.tsx create mode 100644 src/components/TournamentProvider/actions/useSubmitMatchResultAction.tsx create mode 100644 src/components/TournamentProvider/actions/useUndoStartRoundAction.tsx rename src/components/{TournamentActionsProvider => TournamentProvider}/utils/validateConfigureRound.tsx (83%) rename src/components/{TournamentCompetitorEditDialog/TournamentCompetitorEditDialog.module.scss => TournamentRegistrationForm/TournamentRegistrationForm.module.scss} (59%) create mode 100644 src/components/TournamentRegistrationForm/TournamentRegistrationForm.schema.ts create mode 100644 src/components/TournamentRegistrationForm/TournamentRegistrationForm.tsx create mode 100644 src/components/TournamentRegistrationForm/index.ts create mode 100644 src/components/TournamentRegistrationProvider/TournamentRegistrationActiveToggle.tsx create mode 100644 src/components/TournamentRegistrationProvider/TournamentRegistrationContextMenu.tsx create mode 100644 src/components/TournamentRegistrationProvider/TournamentRegistrationProvider.context.tsx create mode 100644 src/components/TournamentRegistrationProvider/TournamentRegistrationProvider.hooks.ts create mode 100644 src/components/TournamentRegistrationProvider/TournamentRegistrationProvider.tsx create mode 100644 src/components/TournamentRegistrationProvider/actions/useDeleteAction.tsx create mode 100644 src/components/TournamentRegistrationProvider/actions/useToggleActiveAction.tsx create mode 100644 src/components/TournamentRegistrationProvider/index.ts delete mode 100644 src/components/TournamentRoster/TournamentRoster.module.scss delete mode 100644 src/components/TournamentRoster/TournamentRoster.tsx delete mode 100644 src/components/TournamentRoster/components/CompetitorActions/CompetitorActions.module.scss delete mode 100644 src/components/TournamentRoster/components/CompetitorActions/CompetitorActions.tsx delete mode 100644 src/components/TournamentRoster/components/CompetitorActions/index.ts delete mode 100644 src/components/TournamentRoster/components/PlayerCount/PlayerCount.module.scss delete mode 100644 src/components/TournamentRoster/components/PlayerCount/PlayerCount.tsx delete mode 100644 src/components/TournamentRoster/components/PlayerCount/index.ts delete mode 100644 src/components/TournamentRoster/index.ts delete mode 100644 src/components/generic/Table/README.md delete mode 100644 src/components/generic/Table/Table.module.scss delete mode 100644 src/components/generic/Table/Table.tsx delete mode 100644 src/components/generic/Table/Table.types.ts delete mode 100644 src/components/generic/Table/TableCell.tsx delete mode 100644 src/components/generic/Table/TableRow.tsx delete mode 100644 src/components/generic/Table/index.ts create mode 100644 src/hooks/useDebouncedState.ts create mode 100644 src/hooks/useDialogInstance.ts create mode 100644 src/hooks/useFormDialog.tsx create mode 100644 src/hooks/useNavigateAway.ts create mode 100644 src/pages/TournamentCompetitorDetailPage/TournamentCompetitorDetailPage.module.scss create mode 100644 src/pages/TournamentCompetitorDetailPage/TournamentCompetitorDetailPage.tsx create mode 100644 src/pages/TournamentCompetitorDetailPage/components/Header/Header.module.scss create mode 100644 src/pages/TournamentCompetitorDetailPage/components/Header/Header.tsx create mode 100644 src/pages/TournamentCompetitorDetailPage/components/Header/index.ts create mode 100644 src/pages/TournamentCompetitorDetailPage/components/MatchResultsList/MatchResultsList.module.scss create mode 100644 src/pages/TournamentCompetitorDetailPage/components/MatchResultsList/MatchResultsList.tsx create mode 100644 src/pages/TournamentCompetitorDetailPage/components/MatchResultsList/index.ts create mode 100644 src/pages/TournamentCompetitorDetailPage/components/TournamentRegistrationsTable/TournamentRegistrationsTable.module.scss create mode 100644 src/pages/TournamentCompetitorDetailPage/components/TournamentRegistrationsTable/TournamentRegistrationsTable.tsx create mode 100644 src/pages/TournamentCompetitorDetailPage/components/TournamentRegistrationsTable/index.ts create mode 100644 src/pages/TournamentCompetitorDetailPage/index.ts create mode 100644 src/pages/TournamentDetailPage/components/TournamentRosterCard/TournamentRosterCard.utils.tsx create mode 100644 src/utils/common/getPathWithQuery.ts delete mode 100644 src/utils/common/getTournamentCompetitorDisplayName.ts create mode 100644 src/utils/common/isUserTournamentCompetitorCaptain.ts diff --git a/convex/_fixtures/createMockTournamentCompetitor.ts b/convex/_fixtures/createMockTournamentCompetitor.ts index d32da471..ff841ef0 100644 --- a/convex/_fixtures/createMockTournamentCompetitor.ts +++ b/convex/_fixtures/createMockTournamentCompetitor.ts @@ -17,9 +17,12 @@ export const createMockTournamentCompetitor = ( rank: -1, rankingFactors: {} as RankingFactorValues, registrations: [], + displayName: 'Test Tournament', tournamentId: 'T0' as Id<'tournaments'>, ...overrides, _id: overrides.id as Id<'tournamentCompetitors'>, + activeRegistrationCount: 0, + availableActions: [], }); export const createMockTournamentCompetitors = ( diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index 4e3299d8..77d0c772 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -20,6 +20,7 @@ import type * as _model_common__helpers_gameSystem_computeRankingFactors from ". import type * as _model_common__helpers_gameSystem_createSortByRanking from "../_model/common/_helpers/gameSystem/createSortByRanking.js"; import type * as _model_common__helpers_gameSystem_divideBaseStats from "../_model/common/_helpers/gameSystem/divideBaseStats.js"; import type * as _model_common__helpers_gameSystem_sumBaseStats from "../_model/common/_helpers/gameSystem/sumBaseStats.js"; +import type * as _model_common__helpers_getCountryName from "../_model/common/_helpers/getCountryName.js"; import type * as _model_common__helpers_getEnvironment from "../_model/common/_helpers/getEnvironment.js"; import type * as _model_common__helpers_getRange from "../_model/common/_helpers/getRange.js"; import type * as _model_common__helpers_getStaticEnumConvexValidator from "../_model/common/_helpers/getStaticEnumConvexValidator.js"; @@ -119,6 +120,8 @@ import type * as _model_photos_mutations_createPhoto from "../_model/photos/muta import type * as _model_photos_queries_getPhoto from "../_model/photos/queries/getPhoto.js"; import type * as _model_photos_table from "../_model/photos/table.js"; import type * as _model_tournamentCompetitors__helpers_deepenTournamentCompetitor from "../_model/tournamentCompetitors/_helpers/deepenTournamentCompetitor.js"; +import type * as _model_tournamentCompetitors__helpers_getAvailableActions from "../_model/tournamentCompetitors/_helpers/getAvailableActions.js"; +import type * as _model_tournamentCompetitors__helpers_getDisplayName from "../_model/tournamentCompetitors/_helpers/getDisplayName.js"; import type * as _model_tournamentCompetitors__helpers_sortTournamentCompetitorsByName from "../_model/tournamentCompetitors/_helpers/sortTournamentCompetitorsByName.js"; import type * as _model_tournamentCompetitors_index from "../_model/tournamentCompetitors/index.js"; import type * as _model_tournamentCompetitors_mutations_createTournamentCompetitor from "../_model/tournamentCompetitors/mutations/createTournamentCompetitor.js"; @@ -160,10 +163,12 @@ import type * as _model_tournamentPairings_queries_getTournamentPairingsByTourna import type * as _model_tournamentPairings_table from "../_model/tournamentPairings/table.js"; import type * as _model_tournamentRegistrations__helpers_checkUserIsRegistered from "../_model/tournamentRegistrations/_helpers/checkUserIsRegistered.js"; import type * as _model_tournamentRegistrations__helpers_deepenTournamentRegistration from "../_model/tournamentRegistrations/_helpers/deepenTournamentRegistration.js"; +import type * as _model_tournamentRegistrations__helpers_getAvailableActions from "../_model/tournamentRegistrations/_helpers/getAvailableActions.js"; import type * as _model_tournamentRegistrations_index from "../_model/tournamentRegistrations/index.js"; import type * as _model_tournamentRegistrations_mutations_createTournamentRegistration from "../_model/tournamentRegistrations/mutations/createTournamentRegistration.js"; import type * as _model_tournamentRegistrations_mutations_deleteTournamentRegistration from "../_model/tournamentRegistrations/mutations/deleteTournamentRegistration.js"; import type * as _model_tournamentRegistrations_mutations_toggleActive from "../_model/tournamentRegistrations/mutations/toggleActive.js"; +import type * as _model_tournamentRegistrations_queries_getTournamentRegistrationByTournamentUser from "../_model/tournamentRegistrations/queries/getTournamentRegistrationByTournamentUser.js"; import type * as _model_tournamentRegistrations_queries_getTournamentRegistrationsByCompetitor from "../_model/tournamentRegistrations/queries/getTournamentRegistrationsByCompetitor.js"; import type * as _model_tournamentRegistrations_queries_getTournamentRegistrationsByTournament from "../_model/tournamentRegistrations/queries/getTournamentRegistrationsByTournament.js"; import type * as _model_tournamentRegistrations_queries_getTournamentRegistrationsByUser from "../_model/tournamentRegistrations/queries/getTournamentRegistrationsByUser.js"; @@ -196,6 +201,8 @@ import type * as _model_tournaments__helpers_checkTournamentAuth from "../_model import type * as _model_tournaments__helpers_checkTournamentVisibility from "../_model/tournaments/_helpers/checkTournamentVisibility.js"; import type * as _model_tournaments__helpers_deepenTournament from "../_model/tournaments/_helpers/deepenTournament.js"; import type * as _model_tournaments__helpers_extractSearchTokens from "../_model/tournaments/_helpers/extractSearchTokens.js"; +import type * as _model_tournaments__helpers_getAvailableActions from "../_model/tournaments/_helpers/getAvailableActions.js"; +import type * as _model_tournaments__helpers_getDisplayName from "../_model/tournaments/_helpers/getDisplayName.js"; import type * as _model_tournaments__helpers_getTournamentDeep from "../_model/tournaments/_helpers/getTournamentDeep.js"; import type * as _model_tournaments__helpers_getTournamentNextRound from "../_model/tournaments/_helpers/getTournamentNextRound.js"; import type * as _model_tournaments__helpers_getTournamentPlayerUserIds from "../_model/tournaments/_helpers/getTournamentPlayerUserIds.js"; @@ -210,7 +217,6 @@ import type * as _model_tournaments_mutations_publishTournament from "../_model/ import type * as _model_tournaments_mutations_startTournament from "../_model/tournaments/mutations/startTournament.js"; import type * as _model_tournaments_mutations_startTournamentRound from "../_model/tournaments/mutations/startTournamentRound.js"; import type * as _model_tournaments_mutations_updateTournament from "../_model/tournaments/mutations/updateTournament.js"; -import type * as _model_tournaments_queries_getAvailableTournamentActions from "../_model/tournaments/queries/getAvailableTournamentActions.js"; import type * as _model_tournaments_queries_getTournament from "../_model/tournaments/queries/getTournament.js"; import type * as _model_tournaments_queries_getTournamentByTournamentPairing from "../_model/tournaments/queries/getTournamentByTournamentPairing.js"; import type * as _model_tournaments_queries_getTournamentOpenRound from "../_model/tournaments/queries/getTournamentOpenRound.js"; @@ -243,6 +249,7 @@ import type * as _model_users_mutations_updateUserAvatarNoAuth from "../_model/u import type * as _model_users_queries_getCurrentUser from "../_model/users/queries/getCurrentUser.js"; import type * as _model_users_queries_getUser from "../_model/users/queries/getUser.js"; import type * as _model_users_queries_getUsers from "../_model/users/queries/getUsers.js"; +import type * as _model_users_queries_internal_createIdFilter from "../_model/users/queries/internal/createIdFilter.js"; import type * as _model_users_queries_internal_getUserByClaimToken from "../_model/users/queries/internal/getUserByClaimToken.js"; import type * as _model_users_queries_internal_getUserByEmail from "../_model/users/queries/internal/getUserByEmail.js"; import type * as _model_users_table from "../_model/users/table.js"; @@ -313,6 +320,7 @@ declare const fullApi: ApiFromModules<{ "_model/common/_helpers/gameSystem/createSortByRanking": typeof _model_common__helpers_gameSystem_createSortByRanking; "_model/common/_helpers/gameSystem/divideBaseStats": typeof _model_common__helpers_gameSystem_divideBaseStats; "_model/common/_helpers/gameSystem/sumBaseStats": typeof _model_common__helpers_gameSystem_sumBaseStats; + "_model/common/_helpers/getCountryName": typeof _model_common__helpers_getCountryName; "_model/common/_helpers/getEnvironment": typeof _model_common__helpers_getEnvironment; "_model/common/_helpers/getRange": typeof _model_common__helpers_getRange; "_model/common/_helpers/getStaticEnumConvexValidator": typeof _model_common__helpers_getStaticEnumConvexValidator; @@ -412,6 +420,8 @@ declare const fullApi: ApiFromModules<{ "_model/photos/queries/getPhoto": typeof _model_photos_queries_getPhoto; "_model/photos/table": typeof _model_photos_table; "_model/tournamentCompetitors/_helpers/deepenTournamentCompetitor": typeof _model_tournamentCompetitors__helpers_deepenTournamentCompetitor; + "_model/tournamentCompetitors/_helpers/getAvailableActions": typeof _model_tournamentCompetitors__helpers_getAvailableActions; + "_model/tournamentCompetitors/_helpers/getDisplayName": typeof _model_tournamentCompetitors__helpers_getDisplayName; "_model/tournamentCompetitors/_helpers/sortTournamentCompetitorsByName": typeof _model_tournamentCompetitors__helpers_sortTournamentCompetitorsByName; "_model/tournamentCompetitors/index": typeof _model_tournamentCompetitors_index; "_model/tournamentCompetitors/mutations/createTournamentCompetitor": typeof _model_tournamentCompetitors_mutations_createTournamentCompetitor; @@ -453,10 +463,12 @@ declare const fullApi: ApiFromModules<{ "_model/tournamentPairings/table": typeof _model_tournamentPairings_table; "_model/tournamentRegistrations/_helpers/checkUserIsRegistered": typeof _model_tournamentRegistrations__helpers_checkUserIsRegistered; "_model/tournamentRegistrations/_helpers/deepenTournamentRegistration": typeof _model_tournamentRegistrations__helpers_deepenTournamentRegistration; + "_model/tournamentRegistrations/_helpers/getAvailableActions": typeof _model_tournamentRegistrations__helpers_getAvailableActions; "_model/tournamentRegistrations/index": typeof _model_tournamentRegistrations_index; "_model/tournamentRegistrations/mutations/createTournamentRegistration": typeof _model_tournamentRegistrations_mutations_createTournamentRegistration; "_model/tournamentRegistrations/mutations/deleteTournamentRegistration": typeof _model_tournamentRegistrations_mutations_deleteTournamentRegistration; "_model/tournamentRegistrations/mutations/toggleActive": typeof _model_tournamentRegistrations_mutations_toggleActive; + "_model/tournamentRegistrations/queries/getTournamentRegistrationByTournamentUser": typeof _model_tournamentRegistrations_queries_getTournamentRegistrationByTournamentUser; "_model/tournamentRegistrations/queries/getTournamentRegistrationsByCompetitor": typeof _model_tournamentRegistrations_queries_getTournamentRegistrationsByCompetitor; "_model/tournamentRegistrations/queries/getTournamentRegistrationsByTournament": typeof _model_tournamentRegistrations_queries_getTournamentRegistrationsByTournament; "_model/tournamentRegistrations/queries/getTournamentRegistrationsByUser": typeof _model_tournamentRegistrations_queries_getTournamentRegistrationsByUser; @@ -489,6 +501,8 @@ declare const fullApi: ApiFromModules<{ "_model/tournaments/_helpers/checkTournamentVisibility": typeof _model_tournaments__helpers_checkTournamentVisibility; "_model/tournaments/_helpers/deepenTournament": typeof _model_tournaments__helpers_deepenTournament; "_model/tournaments/_helpers/extractSearchTokens": typeof _model_tournaments__helpers_extractSearchTokens; + "_model/tournaments/_helpers/getAvailableActions": typeof _model_tournaments__helpers_getAvailableActions; + "_model/tournaments/_helpers/getDisplayName": typeof _model_tournaments__helpers_getDisplayName; "_model/tournaments/_helpers/getTournamentDeep": typeof _model_tournaments__helpers_getTournamentDeep; "_model/tournaments/_helpers/getTournamentNextRound": typeof _model_tournaments__helpers_getTournamentNextRound; "_model/tournaments/_helpers/getTournamentPlayerUserIds": typeof _model_tournaments__helpers_getTournamentPlayerUserIds; @@ -503,7 +517,6 @@ declare const fullApi: ApiFromModules<{ "_model/tournaments/mutations/startTournament": typeof _model_tournaments_mutations_startTournament; "_model/tournaments/mutations/startTournamentRound": typeof _model_tournaments_mutations_startTournamentRound; "_model/tournaments/mutations/updateTournament": typeof _model_tournaments_mutations_updateTournament; - "_model/tournaments/queries/getAvailableTournamentActions": typeof _model_tournaments_queries_getAvailableTournamentActions; "_model/tournaments/queries/getTournament": typeof _model_tournaments_queries_getTournament; "_model/tournaments/queries/getTournamentByTournamentPairing": typeof _model_tournaments_queries_getTournamentByTournamentPairing; "_model/tournaments/queries/getTournamentOpenRound": typeof _model_tournaments_queries_getTournamentOpenRound; @@ -536,6 +549,7 @@ declare const fullApi: ApiFromModules<{ "_model/users/queries/getCurrentUser": typeof _model_users_queries_getCurrentUser; "_model/users/queries/getUser": typeof _model_users_queries_getUser; "_model/users/queries/getUsers": typeof _model_users_queries_getUsers; + "_model/users/queries/internal/createIdFilter": typeof _model_users_queries_internal_createIdFilter; "_model/users/queries/internal/getUserByClaimToken": typeof _model_users_queries_internal_getUserByClaimToken; "_model/users/queries/internal/getUserByEmail": typeof _model_users_queries_internal_getUserByEmail; "_model/users/table": typeof _model_users_table; diff --git a/convex/_model/common/_helpers/getCountryName.ts b/convex/_model/common/_helpers/getCountryName.ts new file mode 100644 index 00000000..7c8d67b9 --- /dev/null +++ b/convex/_model/common/_helpers/getCountryName.ts @@ -0,0 +1,20 @@ +import { country, subdivision } from 'iso-3166-2'; + +export const getCountryName = (code: string): string | undefined => { + if (code === 'xx-lkt') { + return 'Landsknechte'; + } + if (code === 'xx-mrc') { + return 'Mercenaries'; + } + if (code === 'xx-prt') { + return 'Pirates'; + } + if (code === 'un') { + return 'United Nations'; + } + if (code.includes('-')) { + return subdivision(code)?.name; + } + return country(code)?.name; +}; diff --git a/convex/_model/common/errors.ts b/convex/_model/common/errors.ts index 99b0aaf4..b4f625b4 100644 --- a/convex/_model/common/errors.ts +++ b/convex/_model/common/errors.ts @@ -18,6 +18,9 @@ export const errors = { CANNOT_REMOVE_LAST_ORGANIZER_FROM_TOURNAMENT: 'Cannot remove the last organizer from tournament.', CANNOT_REMOVE_LAST_OWNER_FROM_TOURNAMENT: 'Please appoint another organizer as owner before deleting this one.', COMPETITOR_ALREADY_HAS_MAX_PLAYERS: 'Team already has the maximum number of active players.', + CANNOT_CREATE_REGISTRATION_WITHOUT_COMPETITOR: 'Cannot create a registration without a competitor ID or name.', + CANNOT_CREATE_REGISTRATION_WITH_COMPETITOR_NAME_ID: 'Cannot create a registration with both a competitor ID and name.', + CANNOT_CREATE_REGISTRATION_WITHOUT_GROUP: 'Tournament requires a competitor group.', // Tournament Lifecycle CANNOT_CLOSE_ROUND_ON_ARCHIVED_TOURNAMENT: 'Cannot close a round on an archived tournament.', diff --git a/convex/_model/tournamentCompetitors/_helpers/deepenTournamentCompetitor.ts b/convex/_model/tournamentCompetitors/_helpers/deepenTournamentCompetitor.ts index a603a65a..025a5e05 100644 --- a/convex/_model/tournamentCompetitors/_helpers/deepenTournamentCompetitor.ts +++ b/convex/_model/tournamentCompetitors/_helpers/deepenTournamentCompetitor.ts @@ -1,7 +1,9 @@ import { Doc } from '../../../_generated/dataModel'; import { QueryCtx } from '../../../_generated/server'; import { getTournamentRegistrationsByCompetitor } from '../../tournamentRegistrations'; -import { getTournamentResultsByCompetitor } from '../../tournamentResults/queries/getTournamentResultsByCompetitor'; +import { getTournamentResultsByCompetitor } from '../../tournamentResults'; +import { getAvailableActions } from './getAvailableActions'; +import { getDisplayName } from './getDisplayName'; /* eslint-disable @typescript-eslint/explicit-function-return-type */ /** @@ -29,10 +31,14 @@ export const deepenTournamentCompetitor = async ( tournamentId: doc.tournamentId, round: round ?? tournament?.lastRound ?? 0, }); - + const availableActions = await getAvailableActions(ctx, doc); + const displayName = await getDisplayName(ctx, doc); return { ...doc, ...results, + activeRegistrationCount: registrations.filter((r) => r.active).length, + availableActions, + displayName, registrations, }; }; diff --git a/convex/_model/tournamentCompetitors/_helpers/getAvailableActions.ts b/convex/_model/tournamentCompetitors/_helpers/getAvailableActions.ts new file mode 100644 index 00000000..66f0997f --- /dev/null +++ b/convex/_model/tournamentCompetitors/_helpers/getAvailableActions.ts @@ -0,0 +1,85 @@ +import { getAuthUserId } from '@convex-dev/auth/server'; + +import { Doc } from '../../../_generated/dataModel'; +import { QueryCtx } from '../../../_generated/server'; +import { checkUserIsTournamentOrganizer } from '../../tournamentOrganizers'; +import { getTournamentRegistrationsByCompetitor } from '../../tournamentRegistrations'; +import { checkUserIsRegistered } from '../../tournamentRegistrations/_helpers/checkUserIsRegistered'; + +export enum TournamentCompetitorActionKey { + AddPlayer = 'addPlayer', // Create TournamentRegistration as TO + Delete = 'delete', + Edit = 'edit', + Join = 'join', // Create TournamentRegistration as player + Leave = 'leave', // Delete TournamentRegistration as player + // TransferPlayers = 'transferPlayers', + ToggleActive = 'toggleActive', +} + +/** + * Gets a list of tournament competitor actions which are available to a user. + * + * @param ctx - Convex query context + * @param args - Convex query args + * @param args.id - ID of the tournament competitor + * @returns An array of TournamentCompetitorActionKey(s) + */ +export const getAvailableActions = async ( + ctx: QueryCtx, + doc: Doc<'tournamentCompetitors'>, +): Promise => { + + const tournament = await ctx.db.get(doc.tournamentId); + if (!tournament) { + return []; + } + const tournamentRegistrations = await getTournamentRegistrationsByCompetitor(ctx, { + tournamentCompetitorId: doc._id, + }); + + // --- CHECK AUTH ---- + const userId = await getAuthUserId(ctx); + + const isOrganizer = await checkUserIsTournamentOrganizer(ctx, tournament._id, userId); + const isPlayer = await checkUserIsRegistered(ctx, tournament._id, userId); + const isCaptain = userId && doc.captainUserId === userId; + const isTeamTournament = tournament.competitorSize > 1; + const hasSparePlayers = tournamentRegistrations.length > tournament.competitorSize; + + // ---- PRIMARY ACTIONS ---- + const actions: TournamentCompetitorActionKey[] = []; + + if (tournament.status === 'archived') { + return actions; + } + + if (isOrganizer || (isCaptain && hasSparePlayers)) { + actions.push(TournamentCompetitorActionKey.Edit); + } + + if ((isOrganizer || (isCaptain && isTeamTournament)) && tournament.status !== 'active') { + actions.push(TournamentCompetitorActionKey.Delete); + } + + if (isOrganizer && isTeamTournament) { + actions.push(TournamentCompetitorActionKey.AddPlayer); + } + + // if (isOrganizer) { + // actions.push(TournamentCompetitorActionKey.TransferPlayers); + // } + + if (!isPlayer && tournament.status === 'published' && isTeamTournament) { + actions.push(TournamentCompetitorActionKey.Join); + } + + if (isPlayer && ['draft', 'published'].includes(tournament.status) && isTeamTournament) { + actions.push(TournamentCompetitorActionKey.Leave); + } + + if (isOrganizer && tournament.status === 'active' && tournament.currentRound === undefined) { + actions.push(TournamentCompetitorActionKey.ToggleActive); + } + + return actions; +}; diff --git a/convex/_model/tournamentCompetitors/_helpers/getDisplayName.ts b/convex/_model/tournamentCompetitors/_helpers/getDisplayName.ts new file mode 100644 index 00000000..46a61342 --- /dev/null +++ b/convex/_model/tournamentCompetitors/_helpers/getDisplayName.ts @@ -0,0 +1,38 @@ +import { ConvexError } from 'convex/values'; + +import { Doc } from '../../../_generated/dataModel'; +import { QueryCtx } from '../../../_generated/server'; +import { getCountryName } from '../../common/_helpers/getCountryName'; +import { getErrorMessage } from '../../common/errors'; +import { getTournamentRegistrationsByCompetitor } from '../../tournamentRegistrations'; + +export const getDisplayName = async ( + ctx: QueryCtx, + doc: Doc<'tournamentCompetitors'>, +): Promise => { + const fallBack = 'Unknown Competitor'; + + const activeRegistrations = await getTournamentRegistrationsByCompetitor(ctx, { + tournamentCompetitorId: doc._id, + activeOnly: true, + }); + + const tournament = await ctx.db.get(doc.tournamentId); + if (!tournament) { + throw new ConvexError(getErrorMessage('TOURNAMENT_NOT_FOUND')); + } + + // If competitor has only 1 player, just use the player's name: + if (tournament?.competitorSize === 1 && activeRegistrations[0].user) { + return activeRegistrations[0].user.displayName; + } + + // Use the country name if there is one, otherwise just use the team name: + if (doc.teamName) { + const countryName = getCountryName(doc.teamName); + return countryName ?? doc.teamName; + } + + // Fallback: + return fallBack; +}; diff --git a/convex/_model/tournamentCompetitors/index.ts b/convex/_model/tournamentCompetitors/index.ts index eeb755b0..5a110e90 100644 --- a/convex/_model/tournamentCompetitors/index.ts +++ b/convex/_model/tournamentCompetitors/index.ts @@ -1,12 +1,22 @@ +import { Infer } from 'convex/values'; + import { Id } from '../../_generated/dataModel'; +import { scoreAdjustment } from '../common/scoreAdjustment'; export type TournamentCompetitorId = Id<'tournamentCompetitors'>; +export type ScoreAdjustment = Infer; // Helpers export { deepenTournamentCompetitor, type DeepTournamentCompetitor, } from './_helpers/deepenTournamentCompetitor'; +export { + TournamentCompetitorActionKey, +} from './_helpers/getAvailableActions'; +export { + getDisplayName, +} from './_helpers/getDisplayName'; // Mutations export { diff --git a/convex/_model/tournamentCompetitors/queries/getTournamentCompetitor.ts b/convex/_model/tournamentCompetitors/queries/getTournamentCompetitor.ts index b251ab5d..1aa22cf2 100644 --- a/convex/_model/tournamentCompetitors/queries/getTournamentCompetitor.ts +++ b/convex/_model/tournamentCompetitors/queries/getTournamentCompetitor.ts @@ -5,6 +5,7 @@ import { deepenTournamentCompetitor,DeepTournamentCompetitor } from '../_helpers export const getTournamentCompetitorArgs = v.object({ id: v.id('tournamentCompetitors'), + includeRankings: v.optional(v.number()), }); /** @@ -23,5 +24,5 @@ export const getTournamentCompetitor = async ( if (!tournamentCompetitor) { return null; } - return await deepenTournamentCompetitor(ctx, tournamentCompetitor); + return await deepenTournamentCompetitor(ctx, tournamentCompetitor, args.includeRankings); }; diff --git a/convex/_model/tournamentCompetitors/table.ts b/convex/_model/tournamentCompetitors/table.ts index 4a6e7d5c..d401ccbc 100644 --- a/convex/_model/tournamentCompetitors/table.ts +++ b/convex/_model/tournamentCompetitors/table.ts @@ -4,10 +4,11 @@ import { v } from 'convex/values'; import { scoreAdjustment } from '../common/scoreAdjustment'; export const editableFields = { - tournamentId: v.id('tournaments'), - teamName: v.optional(v.string()), captainUserId: v.optional(v.id('users')), scoreAdjustments: v.optional(v.array(scoreAdjustment)), + teamName: v.optional(v.string()), + tournamentGroupId: v.optional(v.id('tournamentGroups')), + tournamentId: v.id('tournaments'), }; /** @@ -22,4 +23,5 @@ export default defineTable({ ...editableFields, ...computedFields, }) - .index('by_tournament_id', ['tournamentId']); + .index('by_tournament_id', ['tournamentId']) + .index('by_tournament_group', ['tournamentGroupId']); diff --git a/convex/_model/tournamentRegistrations/_helpers/deepenTournamentRegistration.ts b/convex/_model/tournamentRegistrations/_helpers/deepenTournamentRegistration.ts index 44c13410..85e1198c 100644 --- a/convex/_model/tournamentRegistrations/_helpers/deepenTournamentRegistration.ts +++ b/convex/_model/tournamentRegistrations/_helpers/deepenTournamentRegistration.ts @@ -1,22 +1,27 @@ +import { ConvexError } from 'convex/values'; + import { Doc } from '../../../_generated/dataModel'; import { QueryCtx } from '../../../_generated/server'; -import { getList } from '../../lists'; +import { getErrorMessage } from '../../common/errors'; import { getUser } from '../../users'; +import { getAvailableActions } from './getAvailableActions'; /* eslint-disable @typescript-eslint/explicit-function-return-type */ export const deepenTournamentRegistration = async ( ctx: QueryCtx, - tournamentRegistration: Doc<'tournamentRegistrations'>, + doc: Doc<'tournamentRegistrations'>, ) => { - const user = await getUser(ctx, { id: tournamentRegistration.userId }); - const list = tournamentRegistration.listId ? ( - await getList(ctx, { id: tournamentRegistration.listId }) - ) : null; + const user = await getUser(ctx, { id: doc.userId }); + if (!user) { + throw new ConvexError(getErrorMessage('USER_NOT_FOUND')); + } + const availableActions = await getAvailableActions(ctx, doc); return { - ...tournamentRegistration, + ...doc, + availableActions, user, - list, + displayName: user.displayName, }; }; diff --git a/convex/_model/tournamentRegistrations/_helpers/getAvailableActions.ts b/convex/_model/tournamentRegistrations/_helpers/getAvailableActions.ts new file mode 100644 index 00000000..5ebb4972 --- /dev/null +++ b/convex/_model/tournamentRegistrations/_helpers/getAvailableActions.ts @@ -0,0 +1,86 @@ +import { getAuthUserId } from '@convex-dev/auth/server'; + +import { Doc } from '../../../_generated/dataModel'; +import { QueryCtx } from '../../../_generated/server'; +import { checkUserIsTournamentOrganizer } from '../../tournamentOrganizers'; + +export enum TournamentRegistrationActionKey { + // ApproveList = 'approveList', + Delete = 'delete', + Leave = 'leave', + // RejectList = 'rejectList', + // SubmitList = 'submitList', + ToggleActive = 'toggleActive', + // Transfer = 'transfer', +} + +/** + * Gets a list of tournament registration actions which are available to a user. + * + * @param ctx - Convex query context + * @param args - Convex query args + * @param args.id - ID of the tournament registration + * @returns An array of TournamentRegistrationActionKey(s) + */ +export const getAvailableActions = async ( + ctx: QueryCtx, + doc: Doc<'tournamentRegistrations'>, +): Promise => { + const tournamentCompetitor = await ctx.db.get(doc.tournamentCompetitorId); + if (!tournamentCompetitor) { + return []; + } + const tournament = await ctx.db.get(doc.tournamentId); + if (!tournament) { + return []; + } + const tournamentRegistrations = await ctx.db.query('tournamentRegistrations') + .withIndex('by_tournament_competitor', (q) => q.eq('tournamentCompetitorId', tournamentCompetitor._id)) + .collect(); + + // --- CHECK AUTH ---- + const userId = await getAuthUserId(ctx); + + const isOrganizer = await checkUserIsTournamentOrganizer(ctx, tournament._id, userId); + const isSelf = userId && doc.userId === userId; + const isCaptain = userId && tournamentCompetitor.captainUserId === userId; + const hasSparePlayers = tournamentRegistrations.length > tournament.competitorSize; + // const isListSubmissionOpen = Date.now() < tournament.listSubmissionClosesAt; + + // ---- PRIMARY ACTIONS ---- + const actions: TournamentRegistrationActionKey[] = []; + + if (tournament.status === 'archived') { + return actions; + } + + // if (isOrganizer) { + // actions.push(TournamentRegistrationActionKey.ApproveList); + // } + + if ((isOrganizer || (isCaptain && !isSelf)) && tournament.status !== 'active') { + actions.push(TournamentRegistrationActionKey.Delete); + } + + if (isSelf && tournament.status !== 'active') { + actions.push(TournamentRegistrationActionKey.Leave); + } + + // if (isOrganizer) { + // actions.push(TournamentRegistrationActionKey.RejectList); + // } + + // if (isOrganizer || ((isCaptain || isPlayer) && isListSubmissionOpen)) { + // actions.push(TournamentRegistrationActionKey.SubmitList); + // } + + if ((isOrganizer || isCaptain) && hasSparePlayers) { + actions.push(TournamentRegistrationActionKey.ToggleActive); + } + + // if (isOrganizer) { + // actions.push(TournamentRegistrationActionKey.Transfer); + // } + + return actions; +}; diff --git a/convex/_model/tournamentRegistrations/index.ts b/convex/_model/tournamentRegistrations/index.ts index 939adc89..7faf4336 100644 --- a/convex/_model/tournamentRegistrations/index.ts +++ b/convex/_model/tournamentRegistrations/index.ts @@ -1,3 +1,19 @@ +// Types +export type { + TournamentRegistration, + TournamentRegistrationFormData, + TournamentRegistrationId, +} from './types'; + +// Helpers +export { + checkUserIsRegistered, +} from './_helpers/checkUserIsRegistered'; +export { + TournamentRegistrationActionKey, +} from './_helpers/getAvailableActions'; + +// Mutations export { createTournamentRegistration, createTournamentRegistrationArgs, @@ -10,6 +26,12 @@ export { toggleTournamentRegistrationActive, toggleTournamentRegistrationActiveArgs, } from './mutations/toggleActive'; + +// Queries +export { + getTournamentRegistrationByTournamentUser, + getTournamentRegistrationByTournamentUserArgs, +} from './queries/getTournamentRegistrationByTournamentUser'; export { getTournamentRegistrationsByCompetitor, getTournamentRegistrationsByCompetitorArgs, @@ -22,7 +44,3 @@ export { getTournamentRegistrationsByUser, getTournamentRegistrationsByUserArgs, } from './queries/getTournamentRegistrationsByUser'; -export type { - TournamentRegistration, - TournamentRegistrationId, -} from './types'; diff --git a/convex/_model/tournamentRegistrations/mutations/createTournamentRegistration.ts b/convex/_model/tournamentRegistrations/mutations/createTournamentRegistration.ts index 034f4d28..c372803b 100644 --- a/convex/_model/tournamentRegistrations/mutations/createTournamentRegistration.ts +++ b/convex/_model/tournamentRegistrations/mutations/createTournamentRegistration.ts @@ -4,25 +4,47 @@ import { v, } from 'convex/values'; -import { Id } from '../../../_generated/dataModel'; import { MutationCtx } from '../../../_generated/server'; import { checkAuth } from '../../common/_helpers/checkAuth'; import { getErrorMessage } from '../../common/errors'; import { VisibilityLevel } from '../../common/VisibilityLevel'; import { getTournamentOrganizersByTournament } from '../../tournamentOrganizers'; import { checkUserIsRegistered } from '../_helpers/checkUserIsRegistered'; +import { deepenTournamentRegistration } from '../_helpers/deepenTournamentRegistration'; import { editableFields } from '../table'; +import { TournamentRegistration } from '../types'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const { tournamentCompetitorId, ...restFields } = editableFields; export const createTournamentRegistrationArgs = v.object({ - ...editableFields, + ...restFields, + tournamentCompetitorId: v.optional(v.id('tournamentCompetitors')), + tournamentCompetitor: v.optional(v.object({ + teamName: v.optional(v.string()), + })), }); export const createTournamentRegistration = async ( ctx: MutationCtx, args: Infer, -): Promise> => { +): Promise => { // --- CHECK AUTH ---- - const userId = await checkAuth(ctx); + /* These user IDs can make changes to this tournament registration: + * - Tournament organizers; + * - The user themselves; + */ + const currentUserId = await checkAuth(ctx); + const tournamentOrganizers = await getTournamentOrganizersByTournament(ctx, { + tournamentId: args.tournamentId, + }); + const authorizedUserIds = [ + ...tournamentOrganizers.map((r) => r.userId), + args.userId, + ]; + if (!authorizedUserIds.includes(currentUserId)) { + throw new ConvexError(getErrorMessage('USER_DOES_NOT_HAVE_PERMISSION')); + } // ---- VALIDATE ---- const tournament = await ctx.db.get(args.tournamentId); @@ -39,33 +61,38 @@ export const createTournamentRegistration = async ( if (isAlreadyRegistered) { throw new ConvexError(getErrorMessage('USER_ALREADY_IN_TOURNAMENT')); } - - // ---- EXTENDED AUTH CHECK ---- - /* These user IDs can make changes to this tournament registration: - * - Tournament organizers; - * - The user themselves; - */ - const tournamentOrganizers = await getTournamentOrganizersByTournament(ctx, { - tournamentId: tournament._id, - }); - const authorizedUserIds = [ - ...tournamentOrganizers.map((r) => r.userId), - args.userId, - ]; - if (!authorizedUserIds.includes(userId)) { - throw new ConvexError(getErrorMessage('USER_DOES_NOT_HAVE_PERMISSION')); + if (!args.tournamentCompetitorId && !args.tournamentCompetitor?.teamName?.length && tournament.competitorSize > 1) { + throw new ConvexError(getErrorMessage('CANNOT_CREATE_REGISTRATION_WITHOUT_COMPETITOR')); + } + if (args.tournamentCompetitorId && args.tournamentCompetitor?.teamName?.length) { + throw new ConvexError(getErrorMessage('CANNOT_CREATE_REGISTRATION_WITH_COMPETITOR_NAME_ID')); } // ---- PRIMARY ACTIONS ---- + let tournamentCompetitorId = args.tournamentCompetitorId; + + // If creating a new competitor: + if (!tournamentCompetitorId) { + tournamentCompetitorId = await ctx.db.insert('tournamentCompetitors', { + captainUserId: args.userId, + teamName: args.tournamentCompetitor?.teamName, + // tournamentGroupId, + tournamentId: args.tournamentId, + }); + } + const teamTournamentRegistrations = await ctx.db.query('tournamentRegistrations') - .withIndex('by_tournament_competitor', (q) => q.eq('tournamentCompetitorId', args.tournamentCompetitorId)) + .withIndex('by_tournament_competitor', (q) => q.eq('tournamentCompetitorId', tournamentCompetitorId)) .collect(); const activePlayerCount = teamTournamentRegistrations.filter((reg) => reg.active).length; + const tournamentRegistrationId = await ctx.db.insert('tournamentRegistrations', { - ...args, active: activePlayerCount < tournament.competitorSize, + confirmed: args.userId === currentUserId, listApproved: false, - confirmed: args.userId === userId, + tournamentCompetitorId, + tournamentId: args.tournamentId, + userId: args.userId, }); // Force user's name visibility to match tournament requirement: @@ -74,12 +101,12 @@ export const createTournamentRegistration = async ( if (!user) { throw new ConvexError(getErrorMessage('USER_NOT_FOUND')); } - if (user.nameVisibility < VisibilityLevel.Tournaments && userId === args.userId) { + if (user.nameVisibility < VisibilityLevel.Tournaments && currentUserId === args.userId) { await ctx.db.patch(args.userId, { nameVisibility: VisibilityLevel.Tournaments, }); } } - return tournamentRegistrationId; + return await deepenTournamentRegistration(ctx, (await ctx.db.get(tournamentRegistrationId))!); }; diff --git a/convex/_model/tournamentRegistrations/mutations/deleteTournamentRegistration.ts b/convex/_model/tournamentRegistrations/mutations/deleteTournamentRegistration.ts index 549ac744..47b66f7a 100644 --- a/convex/_model/tournamentRegistrations/mutations/deleteTournamentRegistration.ts +++ b/convex/_model/tournamentRegistrations/mutations/deleteTournamentRegistration.ts @@ -11,42 +11,57 @@ import { getTournamentOrganizersByTournament } from '../../tournamentOrganizers' export const deleteTournamentRegistrationArgs = v.object({ id: v.id('tournamentRegistrations'), + // newCaptainUserId: v.optional(v.id('users')), // TODO }); export const deleteTournamentRegistration = async ( ctx: MutationCtx, args: Infer, -): Promise => { +): Promise<{ wasLast: boolean }> => { // --- CHECK AUTH ---- const userId = await checkAuth(ctx); + // ---- REQUIRED DATA ---- const registration = await ctx.db.get(args.id); if (!registration) { throw new ConvexError(getErrorMessage('TOURNAMENT_REGISTRATION_NOT_FOUND')); } - - // ---- VALIDATE ---- const tournament = await ctx.db.get(registration.tournamentId); if (!tournament) { throw new ConvexError(getErrorMessage('TOURNAMENT_NOT_FOUND')); } + const tournamentCompetitor = await ctx.db.get(registration.tournamentCompetitorId); + if (!tournamentCompetitor) { + throw new ConvexError(getErrorMessage('TOURNAMENT_COMPETITOR_NOT_FOUND')); + } + const tournamentRegistrations = await ctx.db.query('tournamentRegistrations') + .withIndex('by_tournament_competitor', (q) => q.eq('tournamentCompetitorId', registration.tournamentCompetitorId)) + .collect(); + const tournamentOrganizers = await getTournamentOrganizersByTournament(ctx, { + tournamentId: registration.tournamentId, + }); + const wasLast = tournamentRegistrations.length < 2; + const wasCaptain = tournamentCompetitor?.captainUserId === userId; + + // ---- VALIDATE ---- if (tournament.status === 'archived') { throw new ConvexError(getErrorMessage('CANNOT_MODIFY_ARCHIVED_TOURNAMENT')); } if (tournament.status === 'active') { throw new ConvexError(getErrorMessage('CANNOT_REMOVE_PLAYER_FROM_ACTIVE_TOURNAMENT')); } + // TODO: + // if (wasCaptain && !args.newCaptainUserId) { + + // } // ---- EXTENDED AUTH CHECK ---- /* These user IDs can make changes to this tournament registration: * - Tournament organizers; * - The user themselves; */ - const tournamentOrganizers = await getTournamentOrganizersByTournament(ctx, { - tournamentId: tournament._id, - }); const authorizedUserIds = [ - ...tournamentOrganizers.map((r) => r.userId === userId), + ...tournamentOrganizers.map((r) => r.userId), registration.userId, ]; if (!authorizedUserIds.includes(userId)) { @@ -58,10 +73,17 @@ export const deleteTournamentRegistration = async ( await ctx.db.delete(registration._id); // If this was the last player, also delete the corresponding competitor: - const teamTournamentRegistrations = await ctx.db.query('tournamentRegistrations') - .withIndex('by_tournament_competitor', (q) => q.eq('tournamentCompetitorId', registration.tournamentCompetitorId)) - .collect(); - if (!teamTournamentRegistrations.length) { + if (wasLast) { await ctx.db.delete(registration.tournamentCompetitorId); } + + // If this was the captain, set a new captain: + if (!wasLast && wasCaptain) { + await ctx.db.patch(registration.tournamentCompetitorId, { + captainUserId: tournamentRegistrations[0].userId, + }); + } + return { + wasLast, + }; }; diff --git a/convex/_model/tournamentRegistrations/mutations/toggleActive.ts b/convex/_model/tournamentRegistrations/mutations/toggleActive.ts index 50680e97..686fd2f3 100644 --- a/convex/_model/tournamentRegistrations/mutations/toggleActive.ts +++ b/convex/_model/tournamentRegistrations/mutations/toggleActive.ts @@ -5,9 +5,8 @@ import { } from 'convex/values'; import { MutationCtx } from '../../../_generated/server'; -import { checkAuth } from '../../common/_helpers/checkAuth'; import { getErrorMessage } from '../../common/errors'; -import { getTournamentOrganizersByTournament } from '../../tournamentOrganizers'; +import { getAvailableActions, TournamentRegistrationActionKey } from '../_helpers/getAvailableActions'; export const toggleTournamentRegistrationActiveArgs = v.object({ id: v.id('tournamentRegistrations'), @@ -16,10 +15,7 @@ export const toggleTournamentRegistrationActiveArgs = v.object({ export const toggleTournamentRegistrationActive = async ( ctx: MutationCtx, args: Infer, -): Promise => { - // --- CHECK AUTH ---- - const userId = await checkAuth(ctx); - +): Promise => { // ---- VALIDATE ---- const tournamentRegistration = await ctx.db.get(args.id); if (!tournamentRegistration) { @@ -41,19 +37,13 @@ export const toggleTournamentRegistrationActive = async ( } // ---- EXTENDED AUTH CHECK ---- - /* These user IDs can make changes to this tournament competitor: - * - Tournament organizers; - */ - const tournamentOrganizers = await getTournamentOrganizersByTournament(ctx, { - tournamentId: tournamentRegistration.tournamentId, - }); - const authorizedUserIds = tournamentOrganizers.map((r) => r.userId); - if (!authorizedUserIds.includes(userId)) { + const availableActions = await getAvailableActions(ctx, tournamentRegistration); + if (!availableActions.includes(TournamentRegistrationActionKey.ToggleActive)) { throw new ConvexError(getErrorMessage('USER_DOES_NOT_HAVE_PERMISSION')); } // ---- PRIMARY ACTIONS ---- - await ctx.db.patch(args.id, { - active: !tournamentRegistration.active, - }); + const active = !tournamentRegistration.active; + await ctx.db.patch(args.id, { active }); + return active; }; diff --git a/convex/_model/tournamentRegistrations/queries/getTournamentRegistrationByTournamentUser.ts b/convex/_model/tournamentRegistrations/queries/getTournamentRegistrationByTournamentUser.ts new file mode 100644 index 00000000..22d2a7ef --- /dev/null +++ b/convex/_model/tournamentRegistrations/queries/getTournamentRegistrationByTournamentUser.ts @@ -0,0 +1,23 @@ +import { Infer, v } from 'convex/values'; + +import { QueryCtx } from '../../../_generated/server'; +import { deepenTournamentRegistration } from '../_helpers/deepenTournamentRegistration'; +import { TournamentRegistration } from '../types'; + +export const getTournamentRegistrationByTournamentUserArgs = v.object({ + tournamentId: v.id('tournaments'), + userId: v.id('users'), +}); + +export const getTournamentRegistrationByTournamentUser = async ( + ctx: QueryCtx, + args: Infer, +): Promise => { + const doc = await ctx.db.query('tournamentRegistrations') + .withIndex('by_tournament_user', (q) => q.eq('tournamentId', args.tournamentId).eq('userId', args.userId)) + .unique(); + if (!doc) { + return null; + } + return await deepenTournamentRegistration(ctx, doc); +}; diff --git a/convex/_model/tournamentRegistrations/queries/getTournamentRegistrationsByCompetitor.ts b/convex/_model/tournamentRegistrations/queries/getTournamentRegistrationsByCompetitor.ts index afb81cec..8d86ccc1 100644 --- a/convex/_model/tournamentRegistrations/queries/getTournamentRegistrationsByCompetitor.ts +++ b/convex/_model/tournamentRegistrations/queries/getTournamentRegistrationsByCompetitor.ts @@ -1,12 +1,20 @@ import { Infer, v } from 'convex/values'; import { QueryCtx } from '../../../_generated/server'; +import { notNullOrUndefined } from '../../common/_helpers/notNullOrUndefined'; import { deepenTournamentRegistration, DeepTournamentRegistration } from '../_helpers/deepenTournamentRegistration'; export const getTournamentRegistrationsByCompetitorArgs = v.object({ tournamentCompetitorId: v.id('tournamentCompetitors'), + activeOnly: v.optional(v.boolean()), }); +/** + * Gets an array of deep TournamentRegistrations for the given TournamentCompetitor. + * + * @param ctx - Convex query context + * @returns An array of deep TournamentRegistrations + */ export const getTournamentRegistrationsByCompetitor = async ( ctx: QueryCtx, args: Infer, @@ -14,7 +22,14 @@ export const getTournamentRegistrationsByCompetitor = async ( const tournamentRegistrations = await ctx.db.query('tournamentRegistrations') .withIndex('by_tournament_competitor', (q) => q.eq('tournamentCompetitorId', args.tournamentCompetitorId)) .collect(); - return await Promise.all( - tournamentRegistrations.map(async (registration) => await deepenTournamentRegistration(ctx, registration)), + const deepTournamentRegistrations = await Promise.all( + tournamentRegistrations.map(async (registration) => { + if (args.activeOnly && !registration.active) { + return null; + } else { + return deepenTournamentRegistration(ctx, registration); + } + }), ); + return deepTournamentRegistrations.filter(notNullOrUndefined); }; diff --git a/convex/_model/tournamentRegistrations/types.ts b/convex/_model/tournamentRegistrations/types.ts index a3729534..f1951e5e 100644 --- a/convex/_model/tournamentRegistrations/types.ts +++ b/convex/_model/tournamentRegistrations/types.ts @@ -1,5 +1,10 @@ +import { Infer, v } from 'convex/values'; + import { deepenTournamentRegistration } from './_helpers/deepenTournamentRegistration'; import { Id } from '../../_generated/dataModel'; +import { editableFields } from './table'; export type TournamentRegistrationId = Id<'tournamentRegistrations'>; export type TournamentRegistration = Awaited>; +const formData = v.object(editableFields); +export type TournamentRegistrationFormData = Infer; diff --git a/convex/_model/tournaments/_helpers/deepenTournament.ts b/convex/_model/tournaments/_helpers/deepenTournament.ts index 015e5083..800ca17e 100644 --- a/convex/_model/tournaments/_helpers/deepenTournament.ts +++ b/convex/_model/tournaments/_helpers/deepenTournament.ts @@ -2,6 +2,8 @@ import { Doc } from '../../../_generated/dataModel'; import { QueryCtx } from '../../../_generated/server'; import { getStorageUrl } from '../../common/_helpers/getStorageUrl'; import { getTournamentOrganizersByTournament } from '../../tournamentOrganizers'; +import { getAvailableActions } from './getAvailableActions'; +import { getDisplayName } from './getDisplayName'; import { getTournamentNextRound } from './getTournamentNextRound'; /* eslint-disable @typescript-eslint/explicit-function-return-type */ @@ -21,7 +23,7 @@ export const deepenTournament = async ( ) => { const logoUrl = await getStorageUrl(ctx, tournament.logoStorageId); const bannerUrl = await getStorageUrl(ctx, tournament.bannerStorageId); - + const availableActions = await getAvailableActions(ctx, tournament); const tournamentOrganizers = await getTournamentOrganizersByTournament(ctx, { tournamentId: tournament._id, }); @@ -37,14 +39,16 @@ export const deepenTournament = async ( return { ...tournament, - organizers: tournamentOrganizers, activePlayerCount: activePlayerUserIds.length, activePlayerUserIds, + availableActions, bannerUrl, competitorCount: tournamentCompetitors.length, + displayName: getDisplayName(tournament), logoUrl, maxPlayers : tournament.maxCompetitors * tournament.competitorSize, nextRound: getTournamentNextRound(tournament), + organizers: tournamentOrganizers, playerCount: playerUserIds.length, playerUserIds, useTeams: tournament.competitorSize > 1, diff --git a/convex/_model/tournaments/_helpers/getAvailableActions.ts b/convex/_model/tournaments/_helpers/getAvailableActions.ts new file mode 100644 index 00000000..04accae5 --- /dev/null +++ b/convex/_model/tournaments/_helpers/getAvailableActions.ts @@ -0,0 +1,124 @@ +import { getAuthUserId } from '@convex-dev/auth/server'; + +import { Doc } from '../../../_generated/dataModel'; +import { QueryCtx } from '../../../_generated/server'; +import { getMatchResultsByTournamentRound } from '../../matchResults'; +import { checkUserIsTournamentOrganizer } from '../../tournamentOrganizers'; +import { checkUserIsRegistered } from '../../tournamentRegistrations'; +import { checkTournamentVisibility } from './checkTournamentVisibility'; +import { getTournamentNextRound } from './getTournamentNextRound'; + +export enum TournamentActionKey { + Cancel = 'cancel', + ConfigureRound = 'configureRound', + AddPlayer = 'addPlayer', // Create TournamentRegistration (+ TournamentCompetitor) as TO + Delete = 'delete', + Edit = 'edit', + End = 'end', + EndRound = 'endRound', + Join = 'join', // Create TournamentRegistration (+ TournamentCompetitor) as player + Leave = 'leave', // Delete TournamentRegistration (+ TournamentCompetitor) as player + Publish = 'publish', + ResetRound = 'resetRound', + Start = 'start', + StartRound = 'startRound', + SubmitMatchResult = 'submitMatchResult', + UndoEndRound = 'undoEndRound', + UndoStartRound = 'undoStartRound', +} + +/** + * Gets a list of tournament actions which are available to a user. + * + * @param ctx - Convex query context + * @param args - Convex query args + * @param args.id - ID of the tournament + * @returns An array of TournamentActionKey(s) + */ +export const getAvailableActions = async ( + ctx: QueryCtx, + doc: Doc<'tournaments'>, +): Promise => { + + // --- CHECK AUTH ---- + const userId = await getAuthUserId(ctx); + if (!(await checkTournamentVisibility(ctx, doc))) { + return []; + } + const isOrganizer = await checkUserIsTournamentOrganizer(ctx, doc._id, userId); + const isPlayer = await checkUserIsRegistered(ctx, doc._id, userId); + + // ---- GATHER DATA ---- + const hasCurrentRound = doc.currentRound !== undefined; + + const nextRound = getTournamentNextRound(doc); + const hasNextRound = nextRound !== undefined; + + const nextRoundPairings = await ctx.db.query('tournamentPairings') + .withIndex('by_tournament_round', (q) => q.eq('tournamentId', doc._id).eq('round', nextRound ?? -1)) + .collect(); + const nextRoundPairingCount = (nextRoundPairings ?? []).length; + + const currentRoundMatchResults = await getMatchResultsByTournamentRound(ctx, { + tournamentId: doc._id, + round: doc.currentRound ?? 0, + }); + const currentRoundMatchResultCount = (currentRoundMatchResults ?? []).length; + + // ---- PRIMARY ACTIONS ---- + const actions: TournamentActionKey[] = []; + + if (isOrganizer && ['draft', 'published'].includes(doc.status)) { + actions.push(TournamentActionKey.Edit); + } + + if (isOrganizer && ['draft', 'published'].includes(doc.status)) { + actions.push(TournamentActionKey.Delete); + } + + if (isOrganizer && doc.status === 'draft') { + actions.push(TournamentActionKey.Publish); + } + + if (isOrganizer && doc.status === 'published') { + actions.push(TournamentActionKey.AddPlayer); + } + + if (!isPlayer && doc.status === 'published') { + actions.push(TournamentActionKey.Join); + } + + if (isPlayer && doc.status === 'published') { + actions.push(TournamentActionKey.Leave); + } + + if (isOrganizer && doc.status === 'published') { // TODO: Check for at least 2 competitors + actions.push(TournamentActionKey.Start); + } + + if (isOrganizer && doc.status === 'active' && !hasCurrentRound && hasNextRound) { + actions.push(TournamentActionKey.ConfigureRound); + } + + if (isOrganizer && doc.status === 'active' && !hasCurrentRound && hasNextRound && nextRoundPairingCount > 0) { + actions.push(TournamentActionKey.StartRound); + } + + if (isOrganizer && doc.status === 'active' && hasCurrentRound && currentRoundMatchResultCount === 0) { + actions.push(TournamentActionKey.ResetRound); + } + + if ((isOrganizer || isPlayer) && hasCurrentRound) { // TODO: Don't show if all matches checked in + actions.push(TournamentActionKey.SubmitMatchResult); + } + + if (isOrganizer && hasCurrentRound) { + actions.push(TournamentActionKey.EndRound); + } + + if (isOrganizer && doc.status === 'active' && !hasCurrentRound) { + actions.push(TournamentActionKey.End); + } + + return actions; +}; diff --git a/convex/_model/tournaments/_helpers/getDisplayName.ts b/convex/_model/tournaments/_helpers/getDisplayName.ts new file mode 100644 index 00000000..4910a7f5 --- /dev/null +++ b/convex/_model/tournaments/_helpers/getDisplayName.ts @@ -0,0 +1,10 @@ +import { Doc } from '../../../_generated/dataModel'; + +export const getDisplayName = ( + doc: Doc<'tournaments'>, +): string => { + if (doc.editionYear) { + return `${doc.title} ${doc.editionYear}`; + } + return doc.title; +}; diff --git a/convex/_model/tournaments/index.ts b/convex/_model/tournaments/index.ts index afd926d8..679dc8a2 100644 --- a/convex/_model/tournaments/index.ts +++ b/convex/_model/tournaments/index.ts @@ -5,19 +5,6 @@ import { Id } from '../../_generated/dataModel'; import { editableFields } from './table'; export type TournamentId = Id<'tournaments'>; -export enum TournamentActionKey { - Edit = 'edit', - Delete = 'delete', - Publish = 'publish', - Cancel = 'cancel', - Start = 'start', - ConfigureRound = 'configureRound', - StartRound = 'startRound', - EndRound = 'endRound', - End = 'end', - SubmitMatchResult = 'submitMatchResult', - ResetRound = 'resetRound', -} const tournamentEditableFields = v.object(editableFields); export type TournamentEditableFields = Infer; @@ -26,6 +13,10 @@ export type TournamentFilterParams = Infer; // Helpers export { checkTournamentAuth } from './_helpers/checkTournamentAuth'; export { deepenTournament, type TournamentDeep } from './_helpers/deepenTournament'; +export { + TournamentActionKey, +} from './_helpers/getAvailableActions'; +export { getDisplayName } from './_helpers/getDisplayName'; export { getTournamentDeep } from './_helpers/getTournamentDeep'; export { getTournamentShallow } from './_helpers/getTournamentShallow'; @@ -64,10 +55,6 @@ export { } from './mutations/updateTournament'; // Queries -export { - getAvailableTournamentActions, - getAvailableTournamentActionsArgs, -} from './queries/getAvailableTournamentActions'; export { getTournament, getTournamentArgs, diff --git a/convex/_model/tournaments/queries/getAvailableTournamentActions.ts b/convex/_model/tournaments/queries/getAvailableTournamentActions.ts deleted file mode 100644 index 97a22419..00000000 --- a/convex/_model/tournaments/queries/getAvailableTournamentActions.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { getAuthUserId } from '@convex-dev/auth/server'; -import { Infer, v } from 'convex/values'; - -import { QueryCtx } from '../../../_generated/server'; -import { getMatchResultsByTournamentRound } from '../../matchResults'; -import { checkUserIsTournamentOrganizer } from '../../tournamentOrganizers'; -import { checkUserIsRegistered } from '../../tournamentRegistrations/_helpers/checkUserIsRegistered'; -import { TournamentActionKey } from '..'; -import { checkTournamentVisibility } from '../_helpers/checkTournamentVisibility'; -import { getTournamentNextRound } from '../_helpers/getTournamentNextRound'; - -export const getAvailableTournamentActionsArgs = v.object({ - id: v.id('tournaments'), -}); - -/** - * Gets a list of tournament actions which are available to a user. - * - * @param ctx - Convex query context - * @param args - Convex query args - * @param args.id - ID of the tournament - * @returns An array of TournamentActionKey(s) - */ -export const getAvailableTournamentActions = async ( - ctx: QueryCtx, - args: Infer, -): Promise => { - const tournament = await ctx.db.get(args.id); - if (!tournament) { - return []; - } - - // --- CHECK AUTH ---- - const userId = await getAuthUserId(ctx); - if (!(await checkTournamentVisibility(ctx, tournament))) { - return []; - } - const isOrganizer = await checkUserIsTournamentOrganizer(ctx, args.id, userId); - const isPlayer = await checkUserIsRegistered(ctx, args.id, userId); - - // ---- GATHER DATA ---- - const hasCurrentRound = tournament.currentRound !== undefined; - - const nextRound = getTournamentNextRound(tournament); - const hasNextRound = nextRound !== undefined; - - const nextRoundPairings = await ctx.db.query('tournamentPairings') - .withIndex('by_tournament_round', (q) => q.eq('tournamentId', args.id).eq('round', nextRound ?? -1)) - .collect(); - const nextRoundPairingCount = (nextRoundPairings ?? []).length; - - const currentRoundMatchResults = await getMatchResultsByTournamentRound(ctx, { - tournamentId: tournament._id, - round: tournament.currentRound ?? 0, - }); - const currentRoundMatchResultCount = (currentRoundMatchResults ?? []).length; - - // ---- PRIMARY ACTIONS ---- - const actions: TournamentActionKey[] = []; - - if (isOrganizer && ['draft', 'published'].includes(tournament.status)) { - actions.push(TournamentActionKey.Edit); - } - - if (isOrganizer && ['draft', 'published'].includes(tournament.status)) { - actions.push(TournamentActionKey.Delete); - } - - if (isOrganizer && tournament.status === 'draft') { - actions.push(TournamentActionKey.Publish); - } - - if (isOrganizer && tournament.status === 'published') { // TODO: Check for at least 2 competitors - actions.push(TournamentActionKey.Start); - } - - if (isOrganizer && tournament.status === 'active' && !hasCurrentRound && hasNextRound) { - actions.push(TournamentActionKey.ConfigureRound); - } - - if (isOrganizer && tournament.status === 'active' && !hasCurrentRound && hasNextRound && nextRoundPairingCount > 0) { - actions.push(TournamentActionKey.StartRound); - } - - if (isOrganizer && tournament.status === 'active' && hasCurrentRound && currentRoundMatchResultCount === 0) { - actions.push(TournamentActionKey.ResetRound); - } - - if ((isOrganizer || isPlayer) && hasCurrentRound) { // TODO: Don't show if all matches checked in - actions.push(TournamentActionKey.SubmitMatchResult); - } - - if (isOrganizer && hasCurrentRound) { - actions.push(TournamentActionKey.EndRound); - } - - if (isOrganizer && tournament.status === 'active' && !hasCurrentRound) { - actions.push(TournamentActionKey.End); - } - - return actions; -}; diff --git a/convex/_model/tournaments/table.ts b/convex/_model/tournaments/table.ts index a0a6af0e..f2c95522 100644 --- a/convex/_model/tournaments/table.ts +++ b/convex/_model/tournaments/table.ts @@ -55,6 +55,12 @@ export const editableFields = { currency: currencyCode, })), useNationalTeams: v.boolean(), + allowCompetitorGroupChoice: v.optional(v.boolean()), + groupBehavior: v.optional(v.union( + v.literal('hidden'), // Hidden from player + v.literal('optional'), // Player can select, otherwise auto assigned + v.literal('required'), // Player must select + )), // Format pairingMethod: tournamentPairingMethod, diff --git a/convex/_model/users/queries/getUsers.ts b/convex/_model/users/queries/getUsers.ts index c9780465..41aa5c83 100644 --- a/convex/_model/users/queries/getUsers.ts +++ b/convex/_model/users/queries/getUsers.ts @@ -1,18 +1,24 @@ -import { paginationOptsValidator, PaginationResult } from 'convex/server'; +import { + OrderedQuery, + paginationOptsValidator, + PaginationResult, + QueryInitializer, +} from 'convex/server'; import { Infer, v } from 'convex/values'; -import { Doc } from '../../../_generated/dataModel'; +import { DataModel } from '../../../_generated/dataModel'; import { QueryCtx } from '../../../_generated/server'; import { LimitedUser, redactUser } from '../_helpers/redactUser'; export const getUsersArgs = v.object({ search: v.optional(v.string()), + excludeIds: v.optional(v.array(v.id('users'))), paginationOpts: paginationOptsValidator, }); /** * Gets an array of limited users. - * + * * @param ctx - Convex query context * @param args - Convex query args * @param args.search - Search text @@ -22,15 +28,21 @@ export const getUsers = async ( ctx: QueryCtx, args: Infer, ): Promise> => { - let results: PaginationResult>; + const baseQuery: QueryInitializer = ctx.db.query('users'); + + let orderedQuery: OrderedQuery = baseQuery; if (args.search && args.search.trim() !== '') { - results = await ctx.db - .query('users') - .withSearchIndex('search', (q) => q.search('search', args.search!)) - .paginate(args.paginationOpts); - } else { - results = await ctx.db.query('users').paginate(args.paginationOpts); + orderedQuery = baseQuery.withSearchIndex('search', (q) => q.search('search', args.search!)); + } + + if (args.excludeIds && args.excludeIds.length > 0) { + orderedQuery = orderedQuery.filter((q) => q.not(q.or( + ...args.excludeIds!.map((id) => q.eq(q.field('_id'), id)), + ))); } + + const results = await orderedQuery.paginate(args.paginationOpts); + return { ...results, page: (await Promise.all(results.page.map( diff --git a/convex/_model/users/queries/internal/createIdFilter.ts b/convex/_model/users/queries/internal/createIdFilter.ts new file mode 100644 index 00000000..e2784522 --- /dev/null +++ b/convex/_model/users/queries/internal/createIdFilter.ts @@ -0,0 +1,30 @@ +import { Expression, FilterBuilder } from 'convex/server'; + +import { DataModel, Id } from '../../../../_generated/dataModel'; + +type CreateIdFilterArgs = { + limitToIds?: Id<'users'>[]; + excludeIds?: Id<'users'>[]; +}; + +export const createIdFilter = (args: CreateIdFilterArgs): ((q: FilterBuilder) => Expression) | undefined => { + // If no filtering is needed, return undefined to skip the filter + if ((!args.limitToIds || args.limitToIds.length === 0) && + (!args.excludeIds || args.excludeIds.length === 0)) { + return undefined; + } + + return (q: FilterBuilder): Expression => { + if (args.limitToIds && args.limitToIds.length > 0) { + const isInLimitList = q.or(...args.limitToIds.map((id) => q.eq(q.field('_id'), id))); + if (args.excludeIds && args.excludeIds.length > 0) { + const isNotExcluded = q.not(q.or(...args.excludeIds.map((id) => q.eq(q.field('_id'), id)))); + return q.and(isInLimitList, isNotExcluded); + } + return isInLimitList; + } + + // This branch is only reached when excludeIds is provided but limitToIds is not + return q.not(q.or(...args.excludeIds!.map((id) => q.eq(q.field('_id'), id)))); + }; +}; diff --git a/convex/_model/utils/createTestTournament.ts b/convex/_model/utils/createTestTournament.ts index a5c285f3..7d78d4b4 100644 --- a/convex/_model/utils/createTestTournament.ts +++ b/convex/_model/utils/createTestTournament.ts @@ -90,6 +90,7 @@ export const createTestTournament = async ( const tournamentCompetitorId = await ctx.db.insert('tournamentCompetitors', { teamName, tournamentId, + captainUserId: players[0].userId, active: status === 'active', }); diff --git a/convex/tournamentRegistrations.ts b/convex/tournamentRegistrations.ts index 1c7d60aa..0ad7e411 100644 --- a/convex/tournamentRegistrations.ts +++ b/convex/tournamentRegistrations.ts @@ -1,6 +1,11 @@ import { mutation, query } from './_generated/server'; import * as model from './_model/tournamentRegistrations'; +export const getTournamentRegistrationByTournamentUser = query({ + args: model.getTournamentRegistrationByTournamentUserArgs, + handler: model.getTournamentRegistrationByTournamentUser, +}); + export const getTournamentRegistrationsByCompetitor = query({ args: model.getTournamentRegistrationsByCompetitorArgs, handler: model.getTournamentRegistrationsByCompetitor, diff --git a/convex/tournaments.ts b/convex/tournaments.ts index ec48871e..2a216bf7 100644 --- a/convex/tournaments.ts +++ b/convex/tournaments.ts @@ -25,11 +25,6 @@ export const getTournamentOpenRound = query({ handler: model.getTournamentOpenRound, }); -export const getAvailableTournamentActions = query({ - args: model.getAvailableTournamentActionsArgs, - handler: model.getAvailableTournamentActions, -}); - export const createTournament = mutation({ args: model.createTournamentArgs, handler: model.createTournament, diff --git a/package-lock.json b/package-lock.json index 9badeb92..708e7139 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,9 @@ "": { "name": "combat-command", "version": "1.9.1", + "license": "UNLICENSED", "dependencies": { + "@base-ui/react": "^1.0.0", "@convex-dev/auth": "^0.0.80", "@convex-dev/migrations": "^0.2.9", "@dnd-kit/core": "^6.3.1", @@ -15,7 +17,7 @@ "@dnd-kit/sortable": "^10.0.0", "@fontsource/figtree": "^5.1.0", "@hookform/resolvers": "^3.9.0", - "@ianpaschal/combat-command-components": "^1.2.0", + "@ianpaschal/combat-command-components": "file:../combat-command-components/ianpaschal-combat-command-components-1.6.0.tgz", "@ianpaschal/combat-command-game-systems": "^1.1.4", "@mapbox/search-js-core": "^1.0.0-beta.25", "@radix-ui/colors": "^3.0.0", @@ -40,7 +42,7 @@ "image-blob-reduce": "^4.1.0", "iso-3166-2": "^1.0.0", "jest-environment-jsdom": "^29.7.0", - "lucide-react": "^0.438.0", + "lucide-react": "^0.553.0", "nanoid": "^5.1.5", "nuqs": "^2.7.3", "oslo": "^1.2.1", @@ -322,9 +324,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -355,13 +357,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", - "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.4" + "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -446,32 +448,32 @@ } }, "node_modules/@babel/types": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", - "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@base-ui-components/react": { - "version": "1.0.0-beta.4", - "resolved": "https://registry.npmjs.org/@base-ui-components/react/-/react-1.0.0-beta.4.tgz", - "integrity": "sha512-sPYKj26gbFHD2ZsrMYqQshXnMuomBodzPn+d0dDxWieTj232XCQ9QGt9fU9l5SDGC9hi8s24lDlg9FXPSI7T8A==", + "node_modules/@base-ui/react": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@base-ui/react/-/react-1.0.0.tgz", + "integrity": "sha512-4USBWz++DUSLTuIYpbYkSgy1F9ZmNG9S/lXvlUN6qMK0P0RlW+6eQmDUB4DgZ7HVvtXl4pvi4z5J2fv6Z3+9hg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.4", - "@base-ui-components/utils": "0.1.2", + "@base-ui/utils": "0.2.3", "@floating-ui/react-dom": "^2.1.6", "@floating-ui/utils": "^0.2.10", "reselect": "^5.1.1", - "tabbable": "^6.2.0", - "use-sync-external-store": "^1.5.0" + "tabbable": "^6.3.0", + "use-sync-external-store": "^1.6.0" }, "engines": { "node": ">=14.0.0" @@ -491,16 +493,16 @@ } } }, - "node_modules/@base-ui-components/utils": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@base-ui-components/utils/-/utils-0.1.2.tgz", - "integrity": "sha512-aEitDGpMsYO2qnSpYOwZNykn9Rzn2ioyEVk2fyDRH7t+TIHVKpp9CeV7SPTq43M9mMSDxQ+7UeZJVkrj2dCVIQ==", + "node_modules/@base-ui/utils": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@base-ui/utils/-/utils-0.2.3.tgz", + "integrity": "sha512-/CguQ2PDaOzeVOkllQR8nocJ0FFIDqsWIcURsVmm53QGo8NhFNpePjNlyPIB41luxfOqnG7PU0xicMEw3ls7XQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.4", "@floating-ui/utils": "^0.2.10", "reselect": "^5.1.1", - "use-sync-external-store": "^1.5.0" + "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", @@ -1484,16 +1486,19 @@ "license": "BSD-3-Clause" }, "node_modules/@ianpaschal/combat-command-components": { - "version": "1.2.0", - "resolved": "https://npm.pkg.github.com/download/@ianpaschal/combat-command-components/1.2.0/6d78d5059a78f4ccb5cbc7d3fe6620d89036fa6b", - "integrity": "sha512-oqHsQaCaHv4AbxxyeYeDsA68Oyt7KySGkKZAVYOhGzlo2U7X6MBu5o5eTxs6oPlqx0lKXXTFwdf6LmFTr6j+7A==", + "version": "1.6.0", + "resolved": "file:../combat-command-components/ianpaschal-combat-command-components-1.6.0.tgz", + "integrity": "sha512-/hyD74iYuBVfzoYA9IyCcEFxGLDkIkvxqTnuj9sr3X/qInswVoD5FXgQu4bEqpQCzEGYCO3xz46uctfVzmFYYA==", "license": "MIT", "dependencies": { - "@base-ui-components/react": "^1.0.0-beta.4", + "@base-ui/react": "^1.0.0", "@fontsource/figtree": "^5.2.10", "@radix-ui/colors": "^3.0.0", + "@tanstack/react-store": "^0.8.0", + "@tanstack/store": "^0.8.0", "clsx": "^2.1.1", - "lucide-react": "^0.548.0" + "lucide-react": "^0.548.0", + "radix-ui": "^1.4.3" }, "peerDependencies": { "react": ">=18.0.0", @@ -1501,6 +1506,34 @@ "react-router-dom": "^7.9.4" } }, + "node_modules/@ianpaschal/combat-command-components/node_modules/@tanstack/react-store": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.8.0.tgz", + "integrity": "sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow==", + "license": "MIT", + "dependencies": { + "@tanstack/store": "0.8.0", + "use-sync-external-store": "^1.6.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@ianpaschal/combat-command-components/node_modules/@tanstack/store": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.8.0.tgz", + "integrity": "sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@ianpaschal/combat-command-components/node_modules/lucide-react": { "version": "0.548.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.548.0.tgz", @@ -2866,9 +2899,9 @@ "license": "MIT" }, "node_modules/@radix-ui/primitive": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", - "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", "license": "MIT" }, "node_modules/@radix-ui/react-accessible-icon": { @@ -2895,13 +2928,13 @@ } }, "node_modules/@radix-ui/react-accordion": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.11.tgz", - "integrity": "sha512-l3W5D54emV2ues7jjeG1xcyN7S3jnK3zE2zHqgn0CmMsy9lNJwmgcrmaxS+7ipw15FAivzKNzH3d5EcGoFKw0A==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-collapsible": "1.1.11", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", @@ -2925,75 +2958,16 @@ } } }, - "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-alert-dialog": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.14.tgz", - "integrity": "sha512-IOZfZ3nPvN6lXpJTBCunFQPRSvK8MDgSc1FB85xnIpUKOw9en0dJj8JmCAxV7BiZdtYlUpmrQjoTFkVYtdoWzQ==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dialog": "1.1.14", + "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, @@ -3012,66 +2986,13 @@ } } }, - "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-dialog": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz", - "integrity": "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.10", - "@radix-ui/react-focus-guards": "1.1.2", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.4", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", - "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-focus-scope": { + "node_modules/@radix-ui/react-arrow": { "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", - "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1" + "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", @@ -3088,24 +3009,6 @@ } } }, - "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-aspect-ratio": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz", @@ -3157,15 +3060,15 @@ } }, "node_modules/@radix-ui/react-checkbox": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.2.tgz", - "integrity": "sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", @@ -3187,16 +3090,16 @@ } }, "node_modules/@radix-ui/react-collapsible": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.11.tgz", - "integrity": "sha512-2qrRsVGSCYasSz1RFOorXwl0H7g7J1frQtgpQgYrt+MOidtPAINHn9CPovQXb83r8ahapdx3Tu0fa/pdFFSdPg==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" @@ -3216,21 +3119,29 @@ } } }, - "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, @@ -3265,14 +3176,14 @@ } }, "node_modules/@radix-ui/react-context-menu": { - "version": "2.2.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.15.tgz", - "integrity": "sha512-UsQUMjcYTsBjTSXw0P3GO0werEQvUY2plgRQuKoCTtkNr45q1DiL51j4m7gxhABzZ0BadoXNsIbg7F3KwiUBbw==", + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", + "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-menu": "2.1.15", + "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" @@ -3292,13 +3203,26 @@ } } }, - "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-arrow": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", - "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.3" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", @@ -3315,16 +3239,32 @@ } } }, - "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", "license": "MIT", "dependencies": { + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -3341,25 +3281,39 @@ } } }, - "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", - "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -3371,7 +3325,7 @@ } } }, - "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-focus-scope": { + "node_modules/@radix-ui/react-focus-scope": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", @@ -3396,48 +3350,49 @@ } } }, - "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "node_modules/@radix-ui/react-form": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.8.tgz", + "integrity": "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-menu": { - "version": "2.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.15.tgz", - "integrity": "sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew==", + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", + "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-collection": "1.1.7", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.10", - "@radix-ui/react-focus-guards": "1.1.2", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.10", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" + "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", @@ -3454,22 +3409,31 @@ } } }, - "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-popper": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", - "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==", + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", "license": "MIT", "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", @@ -3486,21 +3450,30 @@ } } }, - "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz", - "integrity": "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==", + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", @@ -3517,41 +3490,22 @@ } } }, - "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-use-rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", - "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", - "license": "MIT", - "dependencies": { - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", - "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", - "license": "MIT" - }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz", - "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==", + "node_modules/@radix-ui/react-menubar": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", + "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", @@ -3568,18 +3522,26 @@ } } }, - "node_modules/@radix-ui/react-form": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.7.tgz", - "integrity": "sha512-IXLKFnaYvFg/KkeV5QfOX7tRnwHXp127koOFUjLWMTrRv5Rny3DQcAtIFFeA/Cli4HHM8DuJCXAUsgnFVJndlw==", + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", + "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-label": "2.1.7", - "@radix-ui/react-primitive": "2.1.3" + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -3596,39 +3558,54 @@ } } }, - "node_modules/@radix-ui/react-form/node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "node_modules/@radix-ui/react-one-time-password-field": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz", + "integrity": "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==", "license": "MIT", "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@radix-ui/react-hover-card": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.14.tgz", - "integrity": "sha512-CPYZ24Mhirm+g6D8jArmLzjYu4Eyg3TTUHswR26QgzXBHBe64BO/RHOJKzmF/Dxb4y4f9PKyJdwm/O/AhNkb+Q==", + "node_modules/@radix-ui/react-password-toggle-field": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz", + "integrity": "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.10", - "@radix-ui/react-popper": "1.2.7", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0" }, "peerDependencies": { "@types/react": "*", @@ -3645,13 +3622,27 @@ } } }, - "node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-arrow": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", - "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.3" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", @@ -3668,10 +3659,10 @@ } } }, - "node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-popper": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", - "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==", + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", "license": "MIT", "dependencies": { "@floating-ui/react-dom": "^2.0.0", @@ -3700,37 +3691,38 @@ } } }, - "node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-use-rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", - "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", "license": "MIT", "dependencies": { - "@radix-ui/rect": "1.1.1" + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", - "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", - "license": "MIT" - }, - "node_modules/@radix-ui/react-label": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", - "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.3" + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -3747,22 +3739,13 @@ } } }, - "node_modules/@radix-ui/react-menubar": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.15.tgz", - "integrity": "sha512-Z71C7LGD+YDYo3TV81paUs8f3Zbmkvg6VLRQpKYfzioOE6n7fOhA3ApK/V/2Odolxjoc4ENk8AYCjohCNayd5A==", + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-menu": "2.1.15", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.10", - "@radix-ui/react-use-controllable-state": "1.2.2" + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -3779,12 +3762,13 @@ } } }, - "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-arrow": { + "node_modules/@radix-ui/react-progress": { "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", - "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", + "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", "license": "MIT", "dependencies": { + "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { @@ -3802,16 +3786,22 @@ } } }, - "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", "license": "MIT", "dependencies": { + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -3828,45 +3818,21 @@ } } }, - "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", - "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", - "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", "license": "MIT", "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1" + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", @@ -3883,46 +3849,62 @@ } } }, - "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", "license": "MIT", "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-menu": { - "version": "2.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.15.tgz", - "integrity": "sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew==", + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.10", - "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.10", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, @@ -3941,22 +3923,13 @@ } } }, - "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-popper": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", - "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==", + "node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", "license": "MIT", "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" + "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", @@ -3973,21 +3946,23 @@ } } }, - "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz", - "integrity": "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==", + "node_modules/@radix-ui/react-slider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -4004,13 +3979,13 @@ } } }, - "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-use-rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", - "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", "license": "MIT", "dependencies": { - "@radix-ui/rect": "1.1.1" + "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", @@ -4022,32 +3997,19 @@ } } }, - "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", - "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", - "license": "MIT" - }, - "node_modules/@radix-ui/react-navigation-menu": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.13.tgz", - "integrity": "sha512-WG8wWfDiJlSF5hELjwfjSGOXcBR/ZMhBFCGYe8vERpC39CQYZeq1PQ2kaYHdye3V95d06H89KGMsVCIE4LWo3g==", + "node_modules/@radix-ui/react-switch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-collection": "1.1.7", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.10", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3" + "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -4064,16 +4026,20 @@ } } }, - "node_modules/@radix-ui/react-navigation-menu/node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", @@ -4090,57 +4056,24 @@ } } }, - "node_modules/@radix-ui/react-navigation-menu/node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-navigation-menu/node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-one-time-password-field": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.7.tgz", - "integrity": "sha512-w1vm7AGI8tNXVovOK7TYQHrAGpRF7qQL+ENpT1a743De5Zmay2RbWGKAiYDKIyIuqptns+znCKwNztE2xl1n0Q==", + "node_modules/@radix-ui/react-toast": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", + "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", "license": "MIT", "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-is-hydrated": "0.1.0", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -4157,16 +4090,15 @@ } } }, - "node_modules/@radix-ui/react-one-time-password-field/node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" + "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", @@ -4183,54 +4115,48 @@ } } }, - "node_modules/@radix-ui/react-one-time-password-field/node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-one-time-password-field/node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@radix-ui/react-one-time-password-field/node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz", - "integrity": "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==", + "node_modules/@radix-ui/react-toolbar": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz", + "integrity": "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-toggle-group": "1.1.11" }, "peerDependencies": { "@types/react": "*", @@ -4247,20 +4173,24 @@ } } }, - "node_modules/@radix-ui/react-password-toggle-field": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.2.tgz", - "integrity": "sha512-F90uYnlBsLPU1UbSLciLsWQmk8+hdWa6SFw4GXaIdNWxFxI5ITKVdAG64f+Twaa9ic6xE7pqxPyUmodrGjT4pQ==", + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-is-hydrated": "0.1.0" + "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -4277,14 +4207,11 @@ } } }, - "node_modules/@radix-ui/react-password-toggle-field/node_modules/@radix-ui/react-id": { + "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -4295,71 +4222,51 @@ } } }, - "node_modules/@radix-ui/react-popover": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.14.tgz", - "integrity": "sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw==", + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.10", - "@radix-ui/react-focus-guards": "1.1.2", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.7", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.4", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true - }, - "@types/react-dom": { - "optional": true } } }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-arrow": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", - "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.3" + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true - }, - "@types/react-dom": { - "optional": true } } }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", - "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -4370,39 +4277,29 @@ } } }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", - "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1" + "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true - }, - "@types/react-dom": { - "optional": true } } }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-id": { + "node_modules/@radix-ui/react-use-layout-effect": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -4413,39 +4310,22 @@ } } }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-popper": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", - "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==", + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" - }, "peerDependencies": { "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true - }, - "@types/react-dom": { - "optional": true } } }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-rect": { + "node_modules/@radix-ui/react-use-rect": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", @@ -4463,44 +4343,31 @@ } } }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/rect": { + "node_modules/@radix-ui/react-use-size": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", - "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", - "license": "MIT" - }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true - }, - "@types/react-dom": { - "optional": true } } }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", - "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", @@ -4517,1178 +4384,92 @@ } } }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" }, - "node_modules/@radix-ui/react-progress": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", - "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "node_modules/@react-email/render": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/@react-email/render/-/render-0.0.16.tgz", + "integrity": "sha512-wDaMy27xAq1cJHtSFptp0DTKPuV2GYhloqia95ub/DH9Dea1aWYsbdM918MOc/b/HvVS3w1z8DWzfAk13bGStQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "html-to-text": "9.0.5", + "js-beautify": "^1.14.11", + "react-promise-suspense": "0.3.4" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-radio-group": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.7.tgz", - "integrity": "sha512-9w5XhD0KPOrm92OTTE0SysH3sYzHsSTHNvZgUBo/VZ80VdYyB5RneDbc0dKpURS24IxkoFRu/hI0i4XyfFwY6g==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-presence": "1.1.4", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.10", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" + "engines": { + "node": ">=18.0.0" }, "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "react": "^18.2.0", + "react-dom": "^18.2.0" } }, - "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "node_modules/@react-hook/debounce": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@react-hook/debounce/-/debounce-3.0.0.tgz", + "integrity": "sha512-ir/kPrSfAzY12Gre0sOHkZ2rkEmM4fS5M5zFxCi4BnCeXh2nvx9Ujd+U4IGpKCuPA+EQD0pg1eK2NGLvfWejag==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" + "@react-hook/latest": "^1.0.2" }, "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "react": ">=16.8" } }, - "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "node_modules/@react-hook/event": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@react-hook/event/-/event-1.2.6.tgz", + "integrity": "sha512-JUL5IluaOdn5w5Afpe/puPa1rj8X6udMlQ9dt4hvMuKmTrBS1Ya6sb4sVgvfe2eU4yDuOfAhik8xhbcCekbg9Q==", "license": "MIT", "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "react": ">=16.8" } }, - "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "node_modules/@react-hook/latest": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@react-hook/latest/-/latest-1.0.3.tgz", + "integrity": "sha512-dy6duzl+JnAZcDbNTfmaP3xHiKtbXYOaz3G51MGVljh548Y8MWzTr+PHLOfvpypEVW9zwvl+VyKjbWKEVbV1Rg==", "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "react": ">=16.8" } }, - "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz", - "integrity": "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==", + "node_modules/@react-hook/throttle": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@react-hook/throttle/-/throttle-2.2.0.tgz", + "integrity": "sha512-LJ5eg+yMV8lXtqK3lR+OtOZ2WH/EfWvuiEEu0M3bhR7dZRfTyEJKxH1oK9uyBxiXPtWXiQggWbZirMCXam51tg==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" + "@react-hook/latest": "^1.0.2" }, "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "react": ">=16.8" } }, - "node_modules/@radix-ui/react-scroll-area": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.9.tgz", - "integrity": "sha512-YSjEfBXnhUELsO2VzjdtYYD4CfQjvao+lhhrX5XsHD7/cyUNzljF1FHEbgTPN7LH2MClfwRMIsYlqTYpKTTe2A==", + "node_modules/@react-hook/window-size": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@react-hook/window-size/-/window-size-3.1.1.tgz", + "integrity": "sha512-yWnVS5LKnOUIrEsI44oz3bIIUYqflamPL27n+k/PC//PsX/YeWBky09oPeAoc9As6jSH16Wgo8plI+ECZaHk3g==", "license": "MIT", "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-presence": "1.1.4", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@react-hook/debounce": "^3.0.0", + "@react-hook/event": "^1.2.1", + "@react-hook/throttle": "^2.2.0" }, "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "react": ">=16.8" } }, - "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-separator": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", - "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slider": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.5.tgz", - "integrity": "sha512-rkfe2pU2NBAYfGaxa3Mqosi7VZEWX5CxKaanRv0vZd4Zhl9fvQrg0VM93dv3xGLGfrHuoTRF3JXH8nb9g+B3fw==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-switch": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.5.tgz", - "integrity": "sha512-5ijLkak6ZMylXsaImpZ8u4Rlf5grRmoc0p0QeX9VJtlrM4f5m3nCTX8tWga/zOA8PZYIR/t0p2Mnvd7InrJ6yQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tabs": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.12.tgz", - "integrity": "sha512-GTVAlRVrQrSw3cEARM0nAx73ixrWDPNZAruETn3oHCNP6SbZ/hNxdxp+u7VkIEv3/sFoLq1PfcHrl7Pnp0CDpw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.4", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.10", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz", - "integrity": "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toast": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.14.tgz", - "integrity": "sha512-nAP5FBxBJGQ/YfUB+r+O6USFVkWq3gAInkxyEnmvEV5jtSbfDhfa4hwX8CraCnbjMLsE7XSf/K75l9xXY7joWg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.10", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.4", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.9.tgz", - "integrity": "sha512-ZoFkBBz9zv9GWer7wIjvdRxmh2wyc2oKWw6C6CseWd6/yq1DK/l5lJ+wnsmFwJZbBYqr02mrf8A2q/CVCuM3ZA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle-group": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.10.tgz", - "integrity": "sha512-kiU694Km3WFLTC75DdqgM/3Jauf3rD9wxeS9XtyWFKsBUeZA337lC+6uUazT7I1DhanZ5gyD5Stf8uf2dbQxOQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.10", - "@radix-ui/react-toggle": "1.1.9", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz", - "integrity": "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toolbar": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.10.tgz", - "integrity": "sha512-jiwQsduEL++M4YBIurjSa+voD86OIytCod0/dbIxFZDLD8NfO1//keXYMfsW8BPcfqwoNjt+y06XcJqAb4KR7A==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.10", - "@radix-ui/react-separator": "1.1.7", - "@radix-ui/react-toggle-group": "1.1.10" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz", - "integrity": "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.7.tgz", - "integrity": "sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.10", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.7", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.4", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-visually-hidden": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-arrow": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", - "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-popper": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", - "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", - "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", - "license": "MIT", - "dependencies": { - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", - "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", - "license": "MIT" - }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", - "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", - "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-is-hydrated": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", - "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", - "license": "MIT", - "dependencies": { - "use-sync-external-store": "^1.5.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-previous": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", - "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", - "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", - "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@react-email/render": { - "version": "0.0.16", - "resolved": "https://registry.npmjs.org/@react-email/render/-/render-0.0.16.tgz", - "integrity": "sha512-wDaMy27xAq1cJHtSFptp0DTKPuV2GYhloqia95ub/DH9Dea1aWYsbdM918MOc/b/HvVS3w1z8DWzfAk13bGStQ==", - "license": "MIT", - "dependencies": { - "html-to-text": "9.0.5", - "js-beautify": "^1.14.11", - "react-promise-suspense": "0.3.4" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "react": "^18.2.0", - "react-dom": "^18.2.0" - } - }, - "node_modules/@react-hook/debounce": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@react-hook/debounce/-/debounce-3.0.0.tgz", - "integrity": "sha512-ir/kPrSfAzY12Gre0sOHkZ2rkEmM4fS5M5zFxCi4BnCeXh2nvx9Ujd+U4IGpKCuPA+EQD0pg1eK2NGLvfWejag==", - "license": "MIT", - "dependencies": { - "@react-hook/latest": "^1.0.2" - }, - "peerDependencies": { - "react": ">=16.8" - } - }, - "node_modules/@react-hook/event": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@react-hook/event/-/event-1.2.6.tgz", - "integrity": "sha512-JUL5IluaOdn5w5Afpe/puPa1rj8X6udMlQ9dt4hvMuKmTrBS1Ya6sb4sVgvfe2eU4yDuOfAhik8xhbcCekbg9Q==", - "license": "MIT", - "peerDependencies": { - "react": ">=16.8" - } - }, - "node_modules/@react-hook/latest": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@react-hook/latest/-/latest-1.0.3.tgz", - "integrity": "sha512-dy6duzl+JnAZcDbNTfmaP3xHiKtbXYOaz3G51MGVljh548Y8MWzTr+PHLOfvpypEVW9zwvl+VyKjbWKEVbV1Rg==", - "license": "MIT", - "peerDependencies": { - "react": ">=16.8" - } - }, - "node_modules/@react-hook/throttle": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@react-hook/throttle/-/throttle-2.2.0.tgz", - "integrity": "sha512-LJ5eg+yMV8lXtqK3lR+OtOZ2WH/EfWvuiEEu0M3bhR7dZRfTyEJKxH1oK9uyBxiXPtWXiQggWbZirMCXam51tg==", - "license": "MIT", - "dependencies": { - "@react-hook/latest": "^1.0.2" - }, - "peerDependencies": { - "react": ">=16.8" - } - }, - "node_modules/@react-hook/window-size": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@react-hook/window-size/-/window-size-3.1.1.tgz", - "integrity": "sha512-yWnVS5LKnOUIrEsI44oz3bIIUYqflamPL27n+k/PC//PsX/YeWBky09oPeAoc9As6jSH16Wgo8plI+ECZaHk3g==", - "license": "MIT", - "dependencies": { - "@react-hook/debounce": "^3.0.0", - "@react-hook/event": "^1.2.1", - "@react-hook/throttle": "^2.2.0" - }, - "peerDependencies": { - "react": ">=16.8" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.38", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.38.tgz", - "integrity": "sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw==", - "dev": true, - "license": "MIT" + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.38", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.38.tgz", + "integrity": "sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw==", + "dev": true, + "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.52.4", @@ -7260,9 +6041,9 @@ "license": "Python-2.0" }, "node_modules/aria-hidden": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", - "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", "license": "MIT", "dependencies": { "tslib": "^2.0.0" @@ -7461,9 +6242,9 @@ } }, "node_modules/ast-v8-to-istanbul": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.7.tgz", - "integrity": "sha512-kr1Hy6YRZBkGQSb6puP+D6FQ59Cx4m0siYhAxygMCAgadiWQ6oxAxQXHOMvJx67SJ63jRoVIIg5eXzUbbct1ww==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.8.tgz", + "integrity": "sha512-szgSZqUxI5T8mLKvS7WTjF9is+MVbOeLADU73IseOcrqhxr/VAvy6wfoVE39KnKzA7JRhjF5eUagNlHwvZPlKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10896,9 +9677,9 @@ } }, "node_modules/istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -11461,12 +10242,12 @@ } }, "node_modules/lucide-react": { - "version": "0.438.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.438.0.tgz", - "integrity": "sha512-uq6yCB+IzVfgIPMK8ibkecXSWTTSOMs9UjUgZigfrDCVqgdwkpIgYg1fSYnf0XXF2AoSyCJZhoZXQwzoai7VGw==", + "version": "0.553.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.553.0.tgz", + "integrity": "sha512-BRgX5zrWmNy/lkVAe0dXBgd7XQdZ3HTf+Hwe3c9WK6dqgnj9h+hxV+MDncM88xDWlCq27+TKvHGE70ViODNILw==", "license": "ISC", "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/lunr": { @@ -12542,637 +11323,304 @@ } ], "license": "MIT", - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.4.29" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-sorting": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/postcss-sorting/-/postcss-sorting-8.0.2.tgz", - "integrity": "sha512-M9dkSrmU00t/jK7rF6BZSZauA5MAaBW4i5EnJXspMwt4iqTh/L9j6fgMnbElEOfyRyfLfVbIHj/R52zHzAPe1Q==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "postcss": "^8.4.20" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/postcss/node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/preact": { - "version": "10.24.3", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", - "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", - "license": "MIT", - "peer": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/preact" - } - }, - "node_modules/preact-render-to-string": { - "version": "6.5.11", - "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz", - "integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==", - "license": "MIT", - "peer": true, - "peerDependencies": { - "preact": ">=10" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, - "node_modules/proto-list": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", - "license": "ISC" - }, - "node_modules/prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/psl": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", - "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" + "engines": { + "node": ">=12.0" }, - "funding": { - "url": "https://github.com/sponsors/lupomontero" + "peerDependencies": { + "postcss": "^8.4.29" } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, "engines": { - "node": ">=6" + "node": ">=4" } }, - "node_modules/punycode.js": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", - "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "node_modules/postcss-sorting": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/postcss-sorting/-/postcss-sorting-8.0.2.tgz", + "integrity": "sha512-M9dkSrmU00t/jK7rF6BZSZauA5MAaBW4i5EnJXspMwt4iqTh/L9j6fgMnbElEOfyRyfLfVbIHj/R52zHzAPe1Q==", "dev": true, "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "postcss": "^8.4.20" } }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, "license": "MIT" }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" + "url": "https://github.com/sponsors/ai" } ], - "license": "MIT" - }, - "node_modules/radix-ui": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.2.tgz", - "integrity": "sha512-fT/3YFPJzf2WUpqDoQi005GS8EpCi+53VhcLaHUj5fwkPYiZAjk1mSxFvbMA8Uq71L03n+WysuYC+mlKkXxt/Q==", "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-accessible-icon": "1.1.7", - "@radix-ui/react-accordion": "1.2.11", - "@radix-ui/react-alert-dialog": "1.1.14", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-aspect-ratio": "1.1.7", - "@radix-ui/react-avatar": "1.1.10", - "@radix-ui/react-checkbox": "1.3.2", - "@radix-ui/react-collapsible": "1.1.11", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-context-menu": "2.2.15", - "@radix-ui/react-dialog": "1.1.14", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.10", - "@radix-ui/react-dropdown-menu": "2.1.15", - "@radix-ui/react-focus-guards": "1.1.2", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-form": "0.1.7", - "@radix-ui/react-hover-card": "1.1.14", - "@radix-ui/react-label": "2.1.7", - "@radix-ui/react-menu": "2.1.15", - "@radix-ui/react-menubar": "1.1.15", - "@radix-ui/react-navigation-menu": "1.2.13", - "@radix-ui/react-one-time-password-field": "0.1.7", - "@radix-ui/react-password-toggle-field": "0.1.2", - "@radix-ui/react-popover": "1.1.14", - "@radix-ui/react-popper": "1.2.7", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.4", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-progress": "1.1.7", - "@radix-ui/react-radio-group": "1.3.7", - "@radix-ui/react-roving-focus": "1.1.10", - "@radix-ui/react-scroll-area": "1.2.9", - "@radix-ui/react-select": "2.2.5", - "@radix-ui/react-separator": "1.1.7", - "@radix-ui/react-slider": "1.3.5", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-switch": "1.2.5", - "@radix-ui/react-tabs": "1.1.12", - "@radix-ui/react-toast": "1.2.14", - "@radix-ui/react-toggle": "1.1.9", - "@radix-ui/react-toggle-group": "1.1.10", - "@radix-ui/react-toolbar": "1.1.10", - "@radix-ui/react-tooltip": "1.2.7", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-escape-keydown": "1.1.1", - "@radix-ui/react-use-is-hydrated": "0.1.0", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "bin": { + "nanoid": "bin/nanoid.cjs" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/radix-ui/node_modules/@radix-ui/react-arrow": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", - "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "node_modules/preact": { + "version": "10.24.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", + "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" } }, - "node_modules/radix-ui/node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "node_modules/preact-render-to-string": { + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz", + "integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==", "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" - }, + "peer": true, "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "preact": ">=10" } }, - "node_modules/radix-ui/node_modules/@radix-ui/react-dialog": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz", - "integrity": "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==", + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.10", - "@radix-ui/react-focus-guards": "1.1.2", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.4", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": ">= 0.8.0" } }, - "node_modules/radix-ui/node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "bin": { + "prettier": "bin/prettier.cjs" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/radix-ui/node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.15.tgz", - "integrity": "sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ==", + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-menu": "2.1.15", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/radix-ui/node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", - "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "peer": true, + "engines": { + "node": ">=10" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/radix-ui/node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", - "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" } }, - "node_modules/radix-ui/node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "license": "ISC" + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } + "optional": true }, - "node_modules/radix-ui/node_modules/@radix-ui/react-menu": { - "version": "2.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.15.tgz", - "integrity": "sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew==", + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.10", - "@radix-ui/react-focus-guards": "1.1.2", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.7", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.4", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.10", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "punycode": "^2.3.1" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/lupomontero" } }, - "node_modules/radix-ui/node_modules/@radix-ui/react-popper": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", - "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==", + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": ">=6" } }, - "node_modules/radix-ui/node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz", - "integrity": "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==", + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" + "side-channel": "^1.1.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": ">=0.6" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" }, - "@types/react-dom": { - "optional": true + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" } - } + ], + "license": "MIT" }, - "node_modules/radix-ui/node_modules/@radix-ui/react-select": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.5.tgz", - "integrity": "sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA==", + "node_modules/radix-ui": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz", + "integrity": "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==", "license": "MIT", "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-accessible-icon": "1.1.7", + "@radix-ui/react-accordion": "1.2.12", + "@radix-ui/react-alert-dialog": "1.1.15", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-aspect-ratio": "1.1.7", + "@radix-ui/react-avatar": "1.1.10", + "@radix-ui/react-checkbox": "1.3.3", + "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-context-menu": "2.2.16", + "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.10", - "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-dropdown-menu": "2.1.16", + "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-form": "0.1.8", + "@radix-ui/react-hover-card": "1.1.15", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-menubar": "1.1.16", + "@radix-ui/react-navigation-menu": "1.2.14", + "@radix-ui/react-one-time-password-field": "0.1.8", + "@radix-ui/react-password-toggle-field": "0.1.3", + "@radix-ui/react-popover": "1.1.15", + "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-progress": "1.1.7", + "@radix-ui/react-radio-group": "1.3.8", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-scroll-area": "1.2.10", + "@radix-ui/react-select": "2.2.6", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-slider": "1.3.6", "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-switch": "1.2.6", + "@radix-ui/react-tabs": "1.1.13", + "@radix-ui/react-toast": "1.2.15", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-toggle-group": "1.1.11", + "@radix-ui/react-toolbar": "1.1.11", + "@radix-ui/react-tooltip": "1.2.8", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-escape-keydown": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -13189,30 +11637,6 @@ } } }, - "node_modules/radix-ui/node_modules/@radix-ui/react-use-rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", - "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", - "license": "MIT", - "dependencies": { - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/radix-ui/node_modules/@radix-ui/rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", - "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", - "license": "MIT" - }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -13331,9 +11755,9 @@ } }, "node_modules/react-remove-scroll": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz", - "integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", "license": "MIT", "dependencies": { "react-remove-scroll-bar": "^2.3.7", @@ -14048,9 +12472,9 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", - "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", "dev": true, "license": "MIT" }, @@ -15388,9 +13812,9 @@ } }, "node_modules/use-sync-external-store": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", - "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" diff --git a/package.json b/package.json index f66efbf6..2e5acb80 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "release:finish": "node scripts/finishRelease.js" }, "dependencies": { + "@base-ui/react": "^1.0.0", "@convex-dev/auth": "^0.0.80", "@convex-dev/migrations": "^0.2.9", "@dnd-kit/core": "^6.3.1", @@ -27,7 +28,7 @@ "@dnd-kit/sortable": "^10.0.0", "@fontsource/figtree": "^5.1.0", "@hookform/resolvers": "^3.9.0", - "@ianpaschal/combat-command-components": "^1.2.0", + "@ianpaschal/combat-command-components": "file:../combat-command-components/ianpaschal-combat-command-components-1.6.0.tgz", "@ianpaschal/combat-command-game-systems": "^1.1.4", "@mapbox/search-js-core": "^1.0.0-beta.25", "@radix-ui/colors": "^3.0.0", @@ -52,7 +53,7 @@ "image-blob-reduce": "^4.1.0", "iso-3166-2": "^1.0.0", "jest-environment-jsdom": "^29.7.0", - "lucide-react": "^0.438.0", + "lucide-react": "^0.553.0", "nanoid": "^5.1.5", "nuqs": "^2.7.3", "oslo": "^1.2.1", diff --git a/src/api.ts b/src/api.ts index 01c54f9f..6dbbc154 100644 --- a/src/api.ts +++ b/src/api.ts @@ -48,7 +48,9 @@ export type StorageId = Id<'_storage'>; // Tournament Competitors export { + type ScoreAdjustment, type DeepTournamentCompetitor as TournamentCompetitor, + TournamentCompetitorActionKey, type TournamentCompetitorId, } from '../convex/_model/tournamentCompetitors'; @@ -69,6 +71,8 @@ export { // Tournament Registrations export { type TournamentRegistration, + TournamentRegistrationActionKey, + type TournamentRegistrationFormData, type TournamentRegistrationId, } from '../convex/_model/tournamentRegistrations'; @@ -82,6 +86,7 @@ export { // Tournaments export { + getDisplayName as getTournamentDisplayName, type TournamentDeep as Tournament, TournamentActionKey, type TournamentEditableFields, diff --git a/src/components/App/App.hooks.ts b/src/components/App/App.hooks.ts index 0e7df130..818dca1b 100644 --- a/src/components/App/App.hooks.ts +++ b/src/components/App/App.hooks.ts @@ -1,18 +1,10 @@ import { useMemo } from 'react'; import { generatePath } from 'react-router-dom'; import { Route } from '@ianpaschal/combat-command-components'; -import qs from 'qs'; -import { TournamentFilterParams } from '~/api'; import { useAuth } from '~/components/AuthProvider'; import { PATHS } from '~/settings'; - -const getQueryString = (basePath: string, params: TournamentFilterParams): string => { - const queryString = qs.stringify(params, { - arrayFormat: 'comma', - }); - return `${basePath}?${queryString}`; -}; +import { getPathWithQuery } from '~/utils/common/getPathWithQuery'; export const usePrimaryAppRoutes = (): Route[] => { const user = useAuth(); @@ -23,25 +15,25 @@ export const usePrimaryAppRoutes = (): Route[] => { }] : []), { title: 'Tournaments', - path: getQueryString(PATHS.tournaments, { order: 'desc' }), + path: getPathWithQuery(PATHS.tournaments, { order: 'desc' }), children: [ ...(user ? [ { title: 'My Tournaments', - path: getQueryString(PATHS.tournaments, { userId: user._id, order: 'desc' }), + path: getPathWithQuery(PATHS.tournaments, { userId: user._id, order: 'desc' }), }, ] : []), { title: 'Ongoing', - path: getQueryString(PATHS.tournaments, { status: ['active'] }), + path: getPathWithQuery(PATHS.tournaments, { status: ['active'] }), }, { title: 'Upcoming', - path: getQueryString(PATHS.tournaments, { status: ['published'] }), + path: getPathWithQuery(PATHS.tournaments, { status: ['published'] }), }, { title: 'Past', - path: getQueryString(PATHS.tournaments, { status: ['archived'], order: 'desc' }), + path: getPathWithQuery(PATHS.tournaments, { status: ['archived'], order: 'desc' }), }, ], }, diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index 540c434e..67603ae3 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx @@ -1,5 +1,9 @@ import { Outlet, useLocation } from 'react-router-dom'; -import { AppNavigation, Route } from '@ianpaschal/combat-command-components'; +import { + AppNavigation, + DialogProvider, + Route, +} from '@ianpaschal/combat-command-components'; import { useWindowWidth } from '@react-hook/window-size/throttled'; import { Coffee } from 'lucide-react'; import { NuqsAdapter } from 'nuqs/adapters/react-router/v7'; @@ -24,16 +28,18 @@ export const App = (): JSX.Element => { ]; return (
- } - secondaryRoutes={secondaryRoutes} - /> + } + secondaryRoutes={secondaryRoutes} + /> - + + +
diff --git a/src/components/AvatarEditable/AvatarEditable.tsx b/src/components/AvatarEditable/AvatarEditable.tsx index 35838418..56841b97 100644 --- a/src/components/AvatarEditable/AvatarEditable.tsx +++ b/src/components/AvatarEditable/AvatarEditable.tsx @@ -37,7 +37,7 @@ export const AvatarEditable = (): JSX.Element => { - - + + + + +
+ + +
+ + {showLoading && ( +
+ Loading... +
+ )} + {showEmptyState && ( +
+ No users found. +
+ )} + {showResults && ( +
+ {(options ?? []).map((user) => ( +
handleSelect(user)} + > + +
+ ))} +
+ )} +
+
+
+
+ ); }); diff --git a/src/components/InputUser/index.ts b/src/components/InputUser/index.ts index 62bdbe65..05892e63 100644 --- a/src/components/InputUser/index.ts +++ b/src/components/InputUser/index.ts @@ -1 +1,4 @@ -export { InputUser } from './InputUser'; +export { + InputUser, + type InputUserProps, +} from './InputUser'; diff --git a/src/components/MatchResultCard/MatchResultCard.tsx b/src/components/MatchResultCard/MatchResultCard.tsx index 21edff37..09bd9a36 100644 --- a/src/components/MatchResultCard/MatchResultCard.tsx +++ b/src/components/MatchResultCard/MatchResultCard.tsx @@ -4,6 +4,7 @@ import { ChevronRight } from 'lucide-react'; import { MatchResult } from '~/api'; import { Button } from '~/components/generic/Button'; import { Separator } from '~/components/generic/Separator'; +import { Spinner } from '~/components/generic/Spinner'; import { Timestamp } from '~/components/generic/Timestamp'; import { MatchResultContextMenu } from '~/components/MatchResultContextMenu'; import { MatchResultPlayers } from '~/components/MatchResultPlayers'; @@ -15,7 +16,7 @@ import { MatchResultPhotos } from './MatchResultPhotos'; import styles from './MatchResultCard.module.scss'; export interface MatchResultCardProps { - matchResult: MatchResult; + matchResult?: MatchResult; } export const MatchResultCard = ({ @@ -24,29 +25,36 @@ export const MatchResultCard = ({ const navigate = useNavigate(); // TODO: Replace with global feature flags const usePhotos = false; - const detailsPath = generatePath(PATHS.matchResultDetails, { id: matchResult._id }); const handleClickDetails = (): void => { - navigate(detailsPath); + if (!matchResult) { + return; // TODO: Error + } + navigate(generatePath(PATHS.matchResultDetails, { id: matchResult._id })); }; + // TODO: Skeleton loading return ( - -
- {usePhotos && } - - - - -
- - -
-
-
+
+ {matchResult ? ( + + {usePhotos && } + + + + +
+ + +
+
+ ) : ( + + )} +
); }; diff --git a/src/components/MatchResultCreateDialog/MatchResultCreateDialog.tsx b/src/components/MatchResultCreateDialog/MatchResultCreateDialog.tsx index 92092280..39acf7d1 100644 --- a/src/components/MatchResultCreateDialog/MatchResultCreateDialog.tsx +++ b/src/components/MatchResultCreateDialog/MatchResultCreateDialog.tsx @@ -29,7 +29,7 @@ export const MatchResultCreateDialog = ({ - + ); }; diff --git a/src/components/MatchResultEditDialog/MatchResultEditDialog.tsx b/src/components/MatchResultEditDialog/MatchResultEditDialog.tsx index 9347280c..b598aa18 100644 --- a/src/components/MatchResultEditDialog/MatchResultEditDialog.tsx +++ b/src/components/MatchResultEditDialog/MatchResultEditDialog.tsx @@ -24,7 +24,7 @@ export const MatchResultEditDialog = (): JSX.Element => { } - round + rounded onClick={openUploadPhotoDialog} /> )} diff --git a/src/components/PaginatedList/PaginatedList.module.scss b/src/components/PaginatedList/PaginatedList.module.scss new file mode 100644 index 00000000..2529735e --- /dev/null +++ b/src/components/PaginatedList/PaginatedList.module.scss @@ -0,0 +1,5 @@ +@use "/src/style/flex"; + +.PaginatedList { + @include flex.column; +} diff --git a/src/components/PaginatedList/PaginatedList.tsx b/src/components/PaginatedList/PaginatedList.tsx new file mode 100644 index 00000000..fa1c8a8c --- /dev/null +++ b/src/components/PaginatedList/PaginatedList.tsx @@ -0,0 +1,49 @@ +import { cloneElement, ReactElement } from 'react'; +import clsx from 'clsx'; +import { PaginatedQueryItem } from 'convex/react'; + +import { Button } from '~/components/generic/Button'; +import { PaginatedQueryHookResult, QueryFn } from '~/services/utils/createPaginatedQueryHook'; +import { DEFAULT_PAGE_SIZE } from '~/settings'; + +import styles from './PaginatedList.module.scss'; + +export interface PaginatedListProps { + className?: string; + query: PaginatedQueryHookResult; + render: (result?: PaginatedQueryItem) => ReactElement; + loadMoreSize?: number; + emptyState?: ReactElement; +} + +export const PaginatedList = ({ + className, + query, + render, + emptyState, +}: PaginatedListProps): JSX.Element => { + const results = query.status === 'LoadingFirstPage' ? Array.from({ + length: DEFAULT_PAGE_SIZE, + }).map(() => undefined) : (query.data ?? []); + return ( +
+ {results.map((r, i) => { + const element = render(r); + return cloneElement(element, { + key: i, + className: clsx(element.props.className, styles.PaginatedList_Item), + }); + })} + {query.status === 'Exhausted' && !query.data?.length && ( + emptyState + )} + {query.status && ['CanLoadMore', 'LoadingMore'].includes(query.status) && ( +
+ ); +}; diff --git a/src/components/PaginatedList/index.ts b/src/components/PaginatedList/index.ts new file mode 100644 index 00000000..9c4fc246 --- /dev/null +++ b/src/components/PaginatedList/index.ts @@ -0,0 +1,4 @@ +export { + PaginatedList, + type PaginatedListProps, +} from './PaginatedList'; diff --git a/src/components/TournamentActionsProvider/TournamentActionsProvider.context.ts b/src/components/TournamentActionsProvider/TournamentActionsProvider.context.ts deleted file mode 100644 index 123e1194..00000000 --- a/src/components/TournamentActionsProvider/TournamentActionsProvider.context.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { createContext, MouseEvent } from 'react'; - -import { TournamentActionKey } from '~/api'; - -export type Action = { - handler: (e: MouseEvent) => void; - label: string; -}; - -export type TournamentActions = Partial>; - -export const TournamentActionsContext = createContext(null); diff --git a/src/components/TournamentActionsProvider/TournamentActionsProvider.hooks.tsx b/src/components/TournamentActionsProvider/TournamentActionsProvider.hooks.tsx deleted file mode 100644 index 7461b027..00000000 --- a/src/components/TournamentActionsProvider/TournamentActionsProvider.hooks.tsx +++ /dev/null @@ -1,249 +0,0 @@ -import { useContext } from 'react'; -import { generatePath, useNavigate } from 'react-router-dom'; - -import { TournamentActionKey } from '~/api'; -import { ConfirmationDialogData } from '~/components/ConfirmationDialog'; -import { Warning } from '~/components/generic/Warning'; -import { useMatchResultCreateDialog } from '~/components/MatchResultCreateDialog'; -import { toast } from '~/components/ToastProvider'; -import { useTournament } from '~/components/TournamentProvider'; -import { useGetTournamentCompetitorsByTournament } from '~/services/tournamentCompetitors'; -import { - useDeleteTournament, - useEndTournament, - useEndTournamentRound, - useGetAvailableTournamentActions, - useGetTournamentOpenRound, - usePublishTournament, - useStartTournament, - useStartTournamentRound, -} from '~/services/tournaments'; -import { PATHS } from '~/settings'; -import { validateConfigureRound } from './utils/validateConfigureRound'; -import { - Action, - TournamentActions, - TournamentActionsContext, -} from './TournamentActionsProvider.context'; - -export const useTournamentActions = () => { - const context = useContext(TournamentActionsContext); - if (!context) { - throw Error('useTournamentActions must be used within a !'); - } - return context; -}; - -type ActionDefinition = Action & { - key: TournamentActionKey; -}; - -export const useActions = (openDialog: (data?: ConfirmationDialogData) => void): TournamentActions => { - const tournament = useTournament(); - - // ---- HANDLERS ---- - const navigate = useNavigate(); - const configureTournamentRound = (): void => { - navigate(generatePath(PATHS.tournamentPairings, { id: tournament._id })); - }; - const { open: openMatchResultCreateDialog } = useMatchResultCreateDialog(); - const { mutation: deleteTournament } = useDeleteTournament({ - onSuccess: (): void => { - toast.success(`${tournament.title} deleted!`); - navigate(PATHS.tournaments); - }, - }); - - const { mutation: publishTournament } = usePublishTournament({ - onSuccess: (): void => { - toast.success(`${tournament.title} is now published!`); - }, - }); - - const { mutation: startTournament } = useStartTournament({ - onSuccess: (): void => { - toast.success(`${tournament.title} started!`); - }, - }); - - const { mutation: startTournamentRound } = useStartTournamentRound({ - onSuccess: (): void => { - toast.success(`Round ${currentRoundLabel} started!`); - }, - }); - - const { mutation: endTournamentRound } = useEndTournamentRound({ - onSuccess: (_, args): void => { - if (args.reset) { - toast.success(`Round ${currentRoundLabel} reset!`); - } else { - toast.success(`Round ${currentRoundLabel} completed!`); - } - }, - }); - - const { mutation: endTournament } = useEndTournament({ - onSuccess: (): void => { - toast.success(`${tournament.title} completed!`); - }, - }); - - // ---- DATA ---- - const { data: availableActions } = useGetAvailableTournamentActions({ - id: tournament._id, - }); - const { data: openRound } = useGetTournamentOpenRound({ - id: tournament._id, - }); - const { data: tournamentCompetitors } = useGetTournamentCompetitorsByTournament({ - tournamentId: tournament._id, - }); - - // Labels for messages: - const nextRoundLabel = (tournament.nextRound ?? 0) + 1; - const currentRoundLabel = (tournament.currentRound ?? 0) + 1; - const remainingRoundsLabel = tournament.roundCount - ((tournament.lastRound ?? -1) + 1); - - // ---- ACTIONS ---- - const actions: ActionDefinition[] = [ - { - key: TournamentActionKey.Edit, - label: 'Edit', - handler: () => navigate(generatePath(PATHS.tournamentEdit, { id: tournament._id })), - }, - { - key: TournamentActionKey.Delete, - label: 'Delete', - handler: () => { - // TODO: Implement confirmation dialog - deleteTournament({ id: tournament._id }); - }, - }, - { - key: TournamentActionKey.Publish, - label: 'Publish', - handler: () => { - // TODO: Implement confirmation dialog - publishTournament({ id: tournament._id }); - }, - }, - { - key: TournamentActionKey.Start, - label: 'Start', - handler: () => { - // TODO: Implement confirmation dialog - startTournament({ id: tournament._id }); - }, - }, - { - key: TournamentActionKey.ConfigureRound, - label: `Configure Round ${nextRoundLabel}`, - handler: () => { - const { errors, warnings } = validateConfigureRound(tournament, tournamentCompetitors); - if (errors.length) { - return toast.error('Cannot Configure Round', { - description: errors, - }); - } - if (warnings.length) { - openDialog({ - title: `Configure Round ${nextRoundLabel}`, - children: warnings.map((warning, i) => ( - {warning} - )), - confirmLabel: 'Proceed', - onConfirm: () => configureTournamentRound(), - }); - } else { - configureTournamentRound(); - } - }, - }, - { - key: TournamentActionKey.StartRound, - label: `Start Round ${nextRoundLabel}`, - handler: () => startTournamentRound({ id: tournament._id }), - }, - { - key: TournamentActionKey.SubmitMatchResult, - label: 'Submit Match Result', - handler: () => openMatchResultCreateDialog(), - }, - { - key: TournamentActionKey.ResetRound, - label: `Reset Round ${currentRoundLabel}`, - handler: () => { - const alreadyHasMatchResults = openRound && openRound.matchResultsProgress.submitted > 0; - - openDialog({ - title: 'Warning!', - description: ( - <> - {`Are you sure you want to reset round ${currentRoundLabel}?`} - {alreadyHasMatchResults && ( - - {`This round already has ${openRound.matchResultsProgress.submitted} matches results checked in. They will be deleted as part of the reset.`} - - )} - This action cannot be undone. You'll need to start the round over from the beginning. - - ), - confirmLabel: 'Reset Round', - onConfirm: () => endTournamentRound({ id: tournament._id, reset: true }), - }); - }, - }, - { - key: TournamentActionKey.EndRound, - label: `End Round ${currentRoundLabel}`, - handler: () => { - if (openRound && openRound.matchResultsProgress.remaining > 0) { - openDialog({ - title: 'Warning!', - description: ( - <> - {` - Are you sure you want to end round ${currentRoundLabel}? - There are still ${openRound.matchResultsProgress.remaining} - matches remaining to be checked in. - `} - Once the round is ended, it cannot be repeated! - - ), - confirmLabel: 'End Round', - onConfirm: () => endTournamentRound({ id: tournament._id }), - }); - } else { - endTournamentRound({ id: tournament._id }); - } - }, - }, - { - key: TournamentActionKey.End, - label: 'End Tournament', - handler: () => { - if (tournament.nextRound !== undefined && tournament.nextRound < tournament.roundCount) { - openDialog({ - title: 'Warning!', - description: ( - <> - {`Are you sure you want to end ${tournament.title}? There are still ${remainingRoundsLabel} rounds remaining.`} - Once the tournament is ended, it cannot be restarted! - - ), - onConfirm: () => endTournament({ id: tournament._id }), - confirmLabel: 'End Tournament', - intent: 'danger', - }); - } else { - endTournament({ id: tournament._id }); - } - }, - }, - ]; - - return actions.filter(({ key }) => (availableActions ?? []).includes(key)).reduce((acc, { key, ...action }) => ({ - ...acc, - [key]: action, - }), {} as TournamentActions); -}; diff --git a/src/components/TournamentActionsProvider/TournamentActionsProvider.tsx b/src/components/TournamentActionsProvider/TournamentActionsProvider.tsx deleted file mode 100644 index 766596e1..00000000 --- a/src/components/TournamentActionsProvider/TournamentActionsProvider.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { ReactNode } from 'react'; - -import { ConfirmationDialog, useConfirmationDialog } from '~/components/ConfirmationDialog'; -import { MatchResultCreateDialog } from '~/components/MatchResultCreateDialog'; -import { TournamentActionsContext } from './TournamentActionsProvider.context'; -import { useActions } from './TournamentActionsProvider.hooks'; - -export interface TournamentActionsProviderProps { - children: ReactNode; -} - -export const TournamentActionsProvider = ({ - children, -}: TournamentActionsProviderProps) => { - const { id, open } = useConfirmationDialog(); - const actions = useActions(open); - return ( - - {children} - - - - ); -}; diff --git a/src/components/TournamentActionsProvider/index.ts b/src/components/TournamentActionsProvider/index.ts deleted file mode 100644 index 8321c11a..00000000 --- a/src/components/TournamentActionsProvider/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { TournamentActionsProvider } from './TournamentActionsProvider'; -export { useTournamentActions } from './TournamentActionsProvider.hooks'; diff --git a/src/components/TournamentBanner/TournamentBanner.tsx b/src/components/TournamentBanner/TournamentBanner.tsx index 395e6ed0..2a25b033 100644 --- a/src/components/TournamentBanner/TournamentBanner.tsx +++ b/src/components/TournamentBanner/TournamentBanner.tsx @@ -5,7 +5,6 @@ import { useTournament } from '~/components/TournamentProvider'; import { TournamentTimer } from '~/components/TournamentTimer'; import { DeviceSize, useDeviceSize } from '~/hooks/useDeviceSize'; import { MAX_WIDTH } from '~/settings'; -import { getTournamentDisplayName } from '~/utils/common/getTournamentDisplayName'; import styles from './TournamentBanner.module.scss'; @@ -41,7 +40,7 @@ export const TournamentBanner = ({ // TODO: Add wrapper /> )} -

{getTournamentDisplayName(tournament)}

+

{tournament.displayName}

{showTimer && (
diff --git a/src/components/TournamentCard/TournamentCard.tsx b/src/components/TournamentCard/TournamentCard.tsx index 9f814913..6c2a10f3 100644 --- a/src/components/TournamentCard/TournamentCard.tsx +++ b/src/components/TournamentCard/TournamentCard.tsx @@ -5,14 +5,12 @@ import { Tournament } from '~/api'; import { useAuth } from '~/components/AuthProvider'; import { Button } from '~/components/generic/Button'; import { Tag } from '~/components/generic/Tag'; -import { TournamentActionsProvider } from '~/components/TournamentActionsProvider'; -import { TournamentContextMenu } from '~/components/TournamentContextMenu'; import { TournamentInfoBlock } from '~/components/TournamentInfoBlock/'; import { TournamentLogo } from '~/components/TournamentLogo'; +import { TournamentContextMenu } from '~/components/TournamentProvider'; import { TournamentProvider } from '~/components/TournamentProvider'; import { useElementSize } from '~/hooks/useElementSize'; import { MIN_WIDTH_TABLET, PATHS } from '~/settings'; -import { getTournamentDisplayName } from '~/utils/common/getTournamentDisplayName'; import { isUserTournamentOrganizer } from '~/utils/common/isUserTournamentOrganizer'; import styles from './TournamentCard.module.scss'; @@ -48,40 +46,38 @@ export const TournamentCard = ({ return ( - -
-
- {tournament?.logoUrl && ( - - )} -
-
-

{getTournamentDisplayName(tournament)}

- {tournament.status === 'draft' && ( - Draft +
+
+ {tournament?.logoUrl && ( + + )} +
+
+

{tournament.displayName}

+ {tournament.status === 'draft' && ( + Draft + )} +
+ {showContextMenu && ( + )} -
- {showContextMenu && ( - - )} -
+
- -
- + + +
); }; diff --git a/src/components/TournamentCompetitorEditDialog/TournamentCompetitorEditDialog.hooks.ts b/src/components/TournamentCompetitorEditDialog/TournamentCompetitorEditDialog.hooks.ts deleted file mode 100644 index 428714d4..00000000 --- a/src/components/TournamentCompetitorEditDialog/TournamentCompetitorEditDialog.hooks.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { TournamentCompetitor, TournamentCompetitorId } from '~/api'; -import { - closeModal, - openModal, - useModal, -} from '~/modals'; - -export type UseTournamentCompetitorEditDialogData = { - tournamentCompetitor: TournamentCompetitor; -}; - -export const useTournamentCompetitorEditDialog = (id?: TournamentCompetitorId) => { - const dialogId = `tournament-competitor-edit-dialog-${id ?? 'new'}`; - const { data } = useModal(dialogId); - return { - id: dialogId, - data, - open: (data?: UseTournamentCompetitorEditDialogData) => openModal(dialogId, data), - close: () => closeModal(dialogId), - }; -}; diff --git a/src/components/TournamentCompetitorEditDialog/TournamentCompetitorEditDialog.tsx b/src/components/TournamentCompetitorEditDialog/TournamentCompetitorEditDialog.tsx deleted file mode 100644 index 89f11957..00000000 --- a/src/components/TournamentCompetitorEditDialog/TournamentCompetitorEditDialog.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { MouseEvent } from 'react'; - -import { TournamentCompetitor, VisibilityLevel } from '~/api'; -import { useAuth } from '~/components/AuthProvider'; -import { Button } from '~/components/generic/Button'; -import { - ControlledDialog, - DialogActions, - DialogHeader, -} from '~/components/generic/Dialog'; -import { ScrollArea } from '~/components/generic/ScrollArea'; -import { TournamentCompetitorForm, TournamentCompetitorSubmitData } from '~/components/TournamentCompetitorForm'; -import { useTournament } from '~/components/TournamentProvider'; -import { useConfirmRegisterDialog } from '~/pages/TournamentDetailPage/components/ConfirmRegisterDialog'; -import { useCreateTournamentCompetitor, useUpdateTournamentCompetitor } from '~/services/tournamentCompetitors'; -import { getTournamentCompetitorDisplayName } from '~/utils/common/getTournamentCompetitorDisplayName'; -import { useTournamentCompetitorEditDialog } from './TournamentCompetitorEditDialog.hooks'; - -import styles from './TournamentCompetitorEditDialog.module.scss'; - -const FORM_ID = 'tournament-competitor-edit-form'; - -export interface TournamentCompetitorEditDialogProps { - competitor?: TournamentCompetitor | null; -} - -export const TournamentCompetitorEditDialog = ({ - competitor, -}: TournamentCompetitorEditDialogProps): JSX.Element => { - const user = useAuth(); - const tournament = useTournament(); - const { open: openConfirmNameVisibilityDialog } = useConfirmRegisterDialog(); - const { id: dialogId, close } = useTournamentCompetitorEditDialog(competitor?._id); - const { - mutation: createTournamentCompetitor, - loading: createTournamentCompetitorLoading, - } = useCreateTournamentCompetitor({ - onSuccess: () => { - close(); - }, - }); - const { - mutation: updateTournamentCompetitor, - loading: updateTournamentCompetitorLoading, - } = useUpdateTournamentCompetitor({ - onSuccess: () => { - close(); - }, - }); - - const handleCancel = (e: MouseEvent): void => { - e.stopPropagation(); - close(); - }; - - const handleSubmit = ({ captain, ...restFormData }: TournamentCompetitorSubmitData): void => { - if (competitor) { - updateTournamentCompetitor({ - id: competitor._id, - ...restFormData, - }); - } else { - const captainIsCurrentUser = captain.userId && user && captain.userId === user._id; - if (captainIsCurrentUser) { - if (tournament.requireRealNames && user.nameVisibility < VisibilityLevel.Tournaments) { - openConfirmNameVisibilityDialog(); - } - } else { - // No need to warn as nameVisibility is only forced for users who add register themselves. - createTournamentCompetitor({ - captainUserId: captain.userId!, // Already validated in form - ...restFormData, - }); - } - } - }; - - const loading = createTournamentCompetitorLoading || updateTournamentCompetitorLoading; - - const getTitle = () => { - if (competitor) { - if (tournament.useTeams) { - return `Edit Team ${getTournamentCompetitorDisplayName(competitor)}`; - } else { - return 'Edit Player'; - } - } else { - if (tournament.useTeams) { - return 'Create Team'; - } else { - return 'Create Player'; - } - } - }; - - return ( - - - - - - -
+ {fields.map((field, index) => ( + + ))} + )} - -
-

Bonuses & Penalties

-
- {fields.map((field, index) => ( - - ))} ); }; diff --git a/src/components/TournamentCompetitorForm/components/ScoreAdjustmentFormItem/ScoreAdjustmentFormItem.tsx b/src/components/TournamentCompetitorForm/components/ScoreAdjustmentFormItem/ScoreAdjustmentFormItem.tsx index e3b5bd65..7e76206d 100644 --- a/src/components/TournamentCompetitorForm/components/ScoreAdjustmentFormItem/ScoreAdjustmentFormItem.tsx +++ b/src/components/TournamentCompetitorForm/components/ScoreAdjustmentFormItem/ScoreAdjustmentFormItem.tsx @@ -4,11 +4,11 @@ import { getGameSystem } from '@ianpaschal/combat-command-game-systems/common'; import clsx from 'clsx'; import { Trash } from 'lucide-react'; +import { Tournament } from '~/api'; import { Button } from '~/components/generic/Button'; import { FormField } from '~/components/generic/Form'; import { InputSelect } from '~/components/generic/InputSelect'; import { InputText } from '~/components/generic/InputText'; -import { useTournament } from '~/components/TournamentProvider'; import { getRoundOptions } from '~/utils/common/getRoundOptions'; import { scoreAdjustmentSchema } from './ScoreAdjustmentFormItem.schema'; import { formatScoreAdjustment } from './ScoreAdjustmentFormItem.utils'; @@ -20,6 +20,7 @@ export interface ScoreAdjustmentFormItemProps { disabled?: boolean; index: number; onRemove: (index: number) => void; + tournament: Tournament; } export const ScoreAdjustmentFormItem = ({ @@ -27,8 +28,8 @@ export const ScoreAdjustmentFormItem = ({ disabled = false, index, onRemove, + tournament, }: ScoreAdjustmentFormItemProps): JSX.Element => { - const tournament = useTournament(); const watched = useWatch({ name: `scoreAdjustments.${index}` }); const result = scoreAdjustmentSchema.safeParse(watched); diff --git a/src/components/TournamentCompetitorForm/components/ScoreAdjustmentFormItem/ScoreAdjustmentFormItem.types.ts b/src/components/TournamentCompetitorForm/components/ScoreAdjustmentFormItem/ScoreAdjustmentFormItem.types.ts index 32ea8c11..027509ce 100644 --- a/src/components/TournamentCompetitorForm/components/ScoreAdjustmentFormItem/ScoreAdjustmentFormItem.types.ts +++ b/src/components/TournamentCompetitorForm/components/ScoreAdjustmentFormItem/ScoreAdjustmentFormItem.types.ts @@ -1,5 +1,5 @@ import { FieldArrayWithId } from 'react-hook-form'; -import { TournamentCompetitorFormData } from '~/components/TournamentCompetitorForm/TournamentCompetitorForm.schema'; +import { FormData } from '~/components/TournamentCompetitorForm/TournamentCompetitorForm.schema'; -export type ScoreAdjustmentField = FieldArrayWithId, 'scoreAdjustments', 'id'>; +export type ScoreAdjustmentField = FieldArrayWithId, 'scoreAdjustments', 'id'>; diff --git a/src/components/TournamentCompetitorForm/components/ScoreAdjustmentFormItem/index.ts b/src/components/TournamentCompetitorForm/components/ScoreAdjustmentFormItem/index.ts index 16cab71a..7b7b854d 100644 --- a/src/components/TournamentCompetitorForm/components/ScoreAdjustmentFormItem/index.ts +++ b/src/components/TournamentCompetitorForm/components/ScoreAdjustmentFormItem/index.ts @@ -1,2 +1,3 @@ export type { ScoreAdjustmentFormItemProps } from './ScoreAdjustmentFormItem'; export { ScoreAdjustmentFormItem } from './ScoreAdjustmentFormItem'; +export { scoreAdjustmentSchema } from './ScoreAdjustmentFormItem.schema'; diff --git a/src/components/TournamentCompetitorForm/index.ts b/src/components/TournamentCompetitorForm/index.ts index 182d02e3..07469e85 100644 --- a/src/components/TournamentCompetitorForm/index.ts +++ b/src/components/TournamentCompetitorForm/index.ts @@ -1,2 +1,2 @@ -export type { SubmitData as TournamentCompetitorSubmitData } from './TournamentCompetitorForm'; export { TournamentCompetitorForm } from './TournamentCompetitorForm'; +export type { SubmitData as TournamentCompetitorSubmitData } from './TournamentCompetitorForm.schema'; diff --git a/src/components/TournamentCompetitorProvider/TournamentCompetitorActiveToggle.tsx b/src/components/TournamentCompetitorProvider/TournamentCompetitorActiveToggle.tsx new file mode 100644 index 00000000..b614e4f6 --- /dev/null +++ b/src/components/TournamentCompetitorProvider/TournamentCompetitorActiveToggle.tsx @@ -0,0 +1,23 @@ +import { TournamentCompetitor } from '~/api'; +import { Switch } from '~/components/generic/Switch'; +import { useToggleActiveAction } from './actions/useToggleActiveAction'; + +export interface TournamentCompetitorActiveToggleProps { + className?: string; + tournamentCompetitor: TournamentCompetitor; +} + +export const TournamentCompetitorActiveToggle = ({ + className, + tournamentCompetitor, +}: TournamentCompetitorActiveToggleProps): JSX.Element => { + const action = useToggleActiveAction(tournamentCompetitor); + return ( + action?.handler()} + /> + ); +}; diff --git a/src/components/TournamentCompetitorProvider/TournamentCompetitorContextMenu.tsx b/src/components/TournamentCompetitorProvider/TournamentCompetitorContextMenu.tsx new file mode 100644 index 00000000..a022f159 --- /dev/null +++ b/src/components/TournamentCompetitorProvider/TournamentCompetitorContextMenu.tsx @@ -0,0 +1,25 @@ +import { TournamentCompetitor, TournamentCompetitorActionKey } from '~/api'; +import { ContextMenu } from '~/components/ContextMenu'; +import { useActions } from './TournamentCompetitorProvider.hooks'; + +export interface TournamentCompetitorContextMenuProps { + className?: string; + tournamentCompetitor: TournamentCompetitor; +} + +export const TournamentCompetitorContextMenu = ({ + className, + tournamentCompetitor, +}: TournamentCompetitorContextMenuProps): JSX.Element => { + const actions = useActions(tournamentCompetitor); + return ( + + ); +}; diff --git a/src/components/TournamentCompetitorProvider/TournamentCompetitorPlayerCount.tsx b/src/components/TournamentCompetitorProvider/TournamentCompetitorPlayerCount.tsx new file mode 100644 index 00000000..e8902016 --- /dev/null +++ b/src/components/TournamentCompetitorProvider/TournamentCompetitorPlayerCount.tsx @@ -0,0 +1,22 @@ +import { Users } from 'lucide-react'; + +import { TournamentCompetitor } from '~/api'; +import { Tag } from '~/components/generic/Tag'; +import { useTournament } from '~/components/TournamentProvider'; + +export interface TournamentCompetitorPlayerCountProps { + className?: string; + tournamentCompetitor: TournamentCompetitor; +} + +export const TournamentCompetitorPlayerCount = ({ + className, + tournamentCompetitor, +}: TournamentCompetitorPlayerCountProps): JSX.Element => { + const tournament = useTournament(); + return ( + + {`${tournamentCompetitor?.activeRegistrationCount ?? 0}/${tournament.competitorSize}`} + + ); +}; diff --git a/src/components/TournamentCompetitorProvider/TournamentCompetitorProvider.context.ts b/src/components/TournamentCompetitorProvider/TournamentCompetitorProvider.context.ts new file mode 100644 index 00000000..58ac99cf --- /dev/null +++ b/src/components/TournamentCompetitorProvider/TournamentCompetitorProvider.context.ts @@ -0,0 +1,7 @@ +import { createContext } from 'react'; + +import { TournamentCompetitor } from '~/api'; + +export const tournamentCompetitorContext = createContext(null); + +export const { Provider } = tournamentCompetitorContext; diff --git a/src/components/TournamentCompetitorProvider/TournamentCompetitorProvider.hooks.tsx b/src/components/TournamentCompetitorProvider/TournamentCompetitorProvider.hooks.tsx new file mode 100644 index 00000000..2894f211 --- /dev/null +++ b/src/components/TournamentCompetitorProvider/TournamentCompetitorProvider.hooks.tsx @@ -0,0 +1,33 @@ +import { useContext } from 'react'; + +import { TournamentCompetitor, TournamentCompetitorActionKey } from '~/api'; +import { Action } from '~/components/ContextMenu/ContextMenu.types'; +import { useToggleActiveAction } from '~/components/TournamentCompetitorProvider/actions/useToggleActiveAction'; +import { useAddPlayerAction } from './actions/useAddPlayerAction'; +import { useDeleteAction } from './actions/useDeleteAction'; +import { useEditAction } from './actions/useEditAction'; +import { useJoinAction } from './actions/useJoinAction'; +import { useLeaveAction } from './actions/useLeaveAction'; +import { tournamentCompetitorContext } from './TournamentCompetitorProvider.context'; + +export const useTournamentCompetitor = () => { + const context = useContext(tournamentCompetitorContext); + if (!context) { + throw Error('useTournamentCompetitor must be used within a !'); + } + return context; +}; + +export const useActions = ( + subject: TournamentCompetitor, +): Record => [ + useAddPlayerAction(subject), + useDeleteAction(subject), + useEditAction(subject), + useJoinAction(subject), + useLeaveAction(subject), + useToggleActiveAction(subject), +].filter((a) => a !== null).reduce((acc, { key, ...action }) => ({ + ...acc, + [key]: action, +}), {} as Record); diff --git a/src/components/TournamentCompetitorProvider/TournamentCompetitorProvider.tsx b/src/components/TournamentCompetitorProvider/TournamentCompetitorProvider.tsx new file mode 100644 index 00000000..b63427e5 --- /dev/null +++ b/src/components/TournamentCompetitorProvider/TournamentCompetitorProvider.tsx @@ -0,0 +1,18 @@ +import { ReactNode } from 'react'; + +import { TournamentCompetitor } from '~/api'; +import { Provider } from './TournamentCompetitorProvider.context'; + +export interface TournamentCompetitorProviderProps { + children: ReactNode; + tournamentCompetitor: TournamentCompetitor; +} + +export const TournamentCompetitorProvider = ({ + children, + tournamentCompetitor, +}: TournamentCompetitorProviderProps) => ( + + {children} + +); diff --git a/src/components/TournamentCompetitorProvider/actions/useAddPlayerAction.tsx b/src/components/TournamentCompetitorProvider/actions/useAddPlayerAction.tsx new file mode 100644 index 00000000..07681632 --- /dev/null +++ b/src/components/TournamentCompetitorProvider/actions/useAddPlayerAction.tsx @@ -0,0 +1,47 @@ +import { UserPlus } from 'lucide-react'; + +import { TournamentCompetitor, TournamentCompetitorActionKey } from '~/api'; +import { ActionDefinition } from '~/components/ContextMenu/ContextMenu.types'; +import { toast } from '~/components/ToastProvider'; +import { useTournament } from '~/components/TournamentProvider'; +import { TournamentRegistrationForm } from '~/components/TournamentRegistrationForm'; +import { useFormDialog } from '~/hooks/useFormDialog'; +import { useCreateTournamentRegistration } from '~/services/tournamentRegistrations'; + +const LABEL = 'Add Player'; +const KEY = TournamentCompetitorActionKey.AddPlayer; + +export const useAddPlayerAction = ( + subject: TournamentCompetitor, +): ActionDefinition | null => { + const tournament = useTournament(); + const { mutation } = useCreateTournamentRegistration({ + onSuccess: (response): void => { + toast.success(`${response.user?.displayName ?? 'Unknown Player'} has been added to ${subject.displayName}!`); + close(); + }, + }); + const { open, close } = useFormDialog({ + formId: 'create-tournament-registration-form', + title: LABEL, + submitLabel: LABEL, + content: ( + mutation(data)} + /> + ), + }); + if (subject.availableActions.includes(KEY)) { + return { + key: KEY, + label: LABEL, + icon: , + handler: () => open(), + }; + } + return null; +}; diff --git a/src/components/TournamentCompetitorProvider/actions/useDeleteAction.tsx b/src/components/TournamentCompetitorProvider/actions/useDeleteAction.tsx new file mode 100644 index 00000000..df27c965 --- /dev/null +++ b/src/components/TournamentCompetitorProvider/actions/useDeleteAction.tsx @@ -0,0 +1,56 @@ +import { generatePath } from 'react-router-dom'; +import { Trash } from 'lucide-react'; + +import { TournamentCompetitor, TournamentCompetitorActionKey } from '~/api'; +import { ActionDefinition } from '~/components/ContextMenu/ContextMenu.types'; +import { toast } from '~/components/ToastProvider'; +import { useTournament } from '~/components/TournamentProvider'; +import { useDialogInstance } from '~/hooks/useDialogInstance'; +import { useNavigateAway } from '~/hooks/useNavigateAway'; +import { useDeleteTournamentCompetitor } from '~/services/tournamentCompetitors'; +import { PATHS } from '~/settings'; + +const LABEL = 'Remove'; +const KEY = TournamentCompetitorActionKey.Delete; + +export const useDeleteAction = ( + subject: TournamentCompetitor, +): ActionDefinition | null => { + const tournament = useTournament(); + const navigateToTournament = useNavigateAway(PATHS.tournamentCompetitorDetails, generatePath(PATHS.tournamentDetails, { + id: subject.tournamentId, + })); + const { open, close } = useDialogInstance(); + const { mutation } = useDeleteTournamentCompetitor({ + onSuccess: () => { + navigateToTournament(); + close(); + toast.success(`${subject.displayName} has been removed.`); + }, + }); + if (subject.availableActions.includes(KEY)) { + return { + key: KEY, + label: LABEL, + icon: , + handler: () => open({ + title: 'Warning!', + content: ( + <> + {`Are you sure you want to remove ${subject.displayName} from ${tournament.displayName}?`} +
+ Once removed, it cannot be restored! + + ), + actions: [ + { + intent: 'danger', + onClick: () => mutation({ id: subject._id }), + text: 'Remove', + }, + ], + }), + }; + } + return null; +}; diff --git a/src/components/TournamentCompetitorProvider/actions/useEditAction.tsx b/src/components/TournamentCompetitorProvider/actions/useEditAction.tsx new file mode 100644 index 00000000..efaf1578 --- /dev/null +++ b/src/components/TournamentCompetitorProvider/actions/useEditAction.tsx @@ -0,0 +1,48 @@ +import { Pencil } from 'lucide-react'; + +import { TournamentCompetitor, TournamentCompetitorActionKey } from '~/api'; +import { ActionDefinition } from '~/components/ContextMenu/ContextMenu.types'; +import { toast } from '~/components/ToastProvider'; +import { TournamentCompetitorForm } from '~/components/TournamentCompetitorForm'; +import { useTournament } from '~/components/TournamentProvider'; +import { useFormDialog } from '~/hooks/useFormDialog'; +import { useUpdateTournamentCompetitor } from '~/services/tournamentCompetitors'; + +const LABEL = 'Edit'; +const KEY = TournamentCompetitorActionKey.Edit; + +export const useEditAction = ( + subject: TournamentCompetitor, +): ActionDefinition | null => { + const tournament = useTournament(); + const { mutation } = useUpdateTournamentCompetitor({ + onSuccess: (): void => { + toast.success(`Saved changes to ${subject.displayName}!`); + close(); + }, + }); + const { open, close } = useFormDialog({ + formId: 'edit-tournament-competitor', + title: `Edit ${tournament.useTeams ? 'Team' : 'Player'}`, + submitLabel: 'Save Changes', + content: ( + mutation({ + id: subject._id, + ...data, + })} + /> + ), + }); + if (subject.availableActions.includes(KEY)) { + return { + key: KEY, + label: LABEL, + icon: , + handler: () => open(), + }; + } + return null; +}; diff --git a/src/components/TournamentCompetitorProvider/actions/useJoinAction.tsx b/src/components/TournamentCompetitorProvider/actions/useJoinAction.tsx new file mode 100644 index 00000000..4da1e2b8 --- /dev/null +++ b/src/components/TournamentCompetitorProvider/actions/useJoinAction.tsx @@ -0,0 +1,36 @@ +import { UserPlus } from 'lucide-react'; + +import { TournamentCompetitor, TournamentCompetitorActionKey } from '~/api'; +import { useAuth } from '~/components/AuthProvider'; +import { ActionDefinition } from '~/components/ContextMenu/ContextMenu.types'; +import { toast } from '~/components/ToastProvider'; +import { useCreateTournamentRegistration } from '~/services/tournamentRegistrations'; + +const LABEL = 'Join'; +const KEY = TournamentCompetitorActionKey.Join; + +export const useJoinAction = ( + subject: TournamentCompetitor, +): ActionDefinition | null => { + const user = useAuth(); + const { mutation } = useCreateTournamentRegistration({ + onSuccess: () => toast.success(`You have joined ${subject.displayName}!`), + }); + if (subject.availableActions.includes(KEY) && user) { + return { + key: KEY, + label: LABEL, + icon: , + handler: () => { + // TODO: Show warning if full (thus waitlist) + // TODO: Show warning if tournament has name requirements + mutation({ + tournamentCompetitorId: subject._id, + tournamentId: subject.tournamentId, + userId: user._id, + }); + }, + }; + } + return null; +}; diff --git a/src/components/TournamentCompetitorProvider/actions/useLeaveAction.tsx b/src/components/TournamentCompetitorProvider/actions/useLeaveAction.tsx new file mode 100644 index 00000000..666ecb00 --- /dev/null +++ b/src/components/TournamentCompetitorProvider/actions/useLeaveAction.tsx @@ -0,0 +1,76 @@ +import { generatePath } from 'react-router-dom'; +import { UserMinus } from 'lucide-react'; + +import { TournamentCompetitor, TournamentCompetitorActionKey } from '~/api'; +import { useAuth } from '~/components/AuthProvider'; +import { ActionDefinition } from '~/components/ContextMenu/ContextMenu.types'; +import { toast } from '~/components/ToastProvider'; +import { useTournament } from '~/components/TournamentProvider'; +import { useDialogInstance } from '~/hooks/useDialogInstance'; +import { useNavigateAway } from '~/hooks/useNavigateAway'; +import { useDeleteTournamentRegistration } from '~/services/tournamentRegistrations'; +import { PATHS } from '~/settings'; + +const LABEL = 'Leave'; +const KEY = TournamentCompetitorActionKey.Leave; + +export const useLeaveAction = ( + subject: TournamentCompetitor, +): ActionDefinition | null => { + const user = useAuth(); + const tournament = useTournament(); + const navigateToTournament = useNavigateAway(PATHS.tournamentCompetitorDetails, generatePath(PATHS.tournamentDetails, { + id: subject.tournamentId, + })); + const { open, close } = useDialogInstance(); + const { mutation } = useDeleteTournamentRegistration({ + onSuccess: ({ wasLast }) => { + if (wasLast) { + navigateToTournament(); + } + if (tournament.useTeams) { + if (wasLast) { + toast.success(`${subject.displayName} has left ${tournament.displayName}.`); + } else { + toast.success(`You have left ${subject.displayName}.`); + } + } else { + toast.success(`You have left ${tournament.displayName}.`); + } + close(); + }, + }); + const ownRegistration = subject.registrations.find((r) => r.userId === user?._id); + const hasOtherPlayers = subject.registrations.length > 1; + const hasSparePlayers = subject.registrations.length > tournament.competitorSize; + if (!ownRegistration) { + return null; + } + if (subject.availableActions.includes(KEY)) { + return { + key: KEY, + label: LABEL, + icon: , + handler: () => { + if (user && tournament.useTeams && hasOtherPlayers && !hasSparePlayers) { + open({ + title: 'Warning!', + content: ( + {`Are you sure you want to leave team ${subject.displayName}? You will leave the team short-handed.`} + ), + actions: [ + { + intent: 'danger', + onClick: () => mutation({ id: ownRegistration._id }), + text: 'Leave', + }, + ], + }); + } else { + mutation({ id: ownRegistration._id }); + } + }, + }; + } + return null; +}; diff --git a/src/components/TournamentCompetitorProvider/actions/useToggleActiveAction.tsx b/src/components/TournamentCompetitorProvider/actions/useToggleActiveAction.tsx new file mode 100644 index 00000000..f76a8a9d --- /dev/null +++ b/src/components/TournamentCompetitorProvider/actions/useToggleActiveAction.tsx @@ -0,0 +1,22 @@ +import { TournamentCompetitor, TournamentCompetitorActionKey } from '~/api'; +import { ActionDefinition } from '~/components/ContextMenu/ContextMenu.types'; +import { toast } from '~/components/ToastProvider'; +import { useToggleTournamentCompetitorActive } from '~/services/tournamentCompetitors'; + +const KEY = TournamentCompetitorActionKey.ToggleActive; + +export const useToggleActiveAction = ( + subject: TournamentCompetitor, +): ActionDefinition | null => { + const { mutation } = useToggleTournamentCompetitorActive({ + onSuccess: () => toast.success(`${subject.displayName} is now active!`), + }); + if (subject.availableActions.includes(KEY)) { + return { + key: KEY, + label: subject.active ? 'Make Inactive' : 'Make Active', + handler: () => mutation({ id: subject._id }), + }; + } + return null; +}; diff --git a/src/components/TournamentCompetitorProvider/index.ts b/src/components/TournamentCompetitorProvider/index.ts new file mode 100644 index 00000000..a767fa5b --- /dev/null +++ b/src/components/TournamentCompetitorProvider/index.ts @@ -0,0 +1,24 @@ +export { useAddPlayerAction } from './actions/useAddPlayerAction'; +export { useDeleteAction } from './actions/useDeleteAction'; +export { useEditAction } from './actions/useEditAction'; +export { useJoinAction } from './actions/useJoinAction'; +export { useLeaveAction } from './actions/useLeaveAction'; +export { useToggleActiveAction } from './actions/useToggleActiveAction'; +export { + TournamentCompetitorActiveToggle, +} from './TournamentCompetitorActiveToggle'; +export { + TournamentCompetitorContextMenu, + type TournamentCompetitorContextMenuProps, +} from './TournamentCompetitorContextMenu'; +export { + TournamentCompetitorPlayerCount, + type TournamentCompetitorPlayerCountProps, +} from './TournamentCompetitorPlayerCount'; +export { + TournamentCompetitorProvider, + type TournamentCompetitorProviderProps, +} from './TournamentCompetitorProvider'; +export { + useTournamentCompetitor, +} from './TournamentCompetitorProvider.hooks'; diff --git a/src/components/TournamentContextMenu/TournamentContextMenu.tsx b/src/components/TournamentContextMenu/TournamentContextMenu.tsx deleted file mode 100644 index bb9c6745..00000000 --- a/src/components/TournamentContextMenu/TournamentContextMenu.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { Ellipsis } from 'lucide-react'; - -import { Button } from '~/components/generic/Button'; -import { PopoverMenu } from '~/components/generic/PopoverMenu'; -import { useTournamentActions } from '~/components/TournamentActionsProvider'; -import { ElementSize } from '~/types/componentLib'; - -export interface TournamentContextMenuProps { - className?: string; - size?: ElementSize; - variant?: 'secondary' | 'primary'; -} - -export const TournamentContextMenu = ({ - className, - size = 'normal', - variant = 'secondary', -}: TournamentContextMenuProps): JSX.Element | null => { - const actions = useTournamentActions(); - const visibleMenuItems = Object.values(actions).map(({ label, handler }) => ({ - label, - onClick: handler, - })); - if (!visibleMenuItems.length) { - return null; - } - return ( - -
diff --git a/src/components/TournamentProvider/TournamentContextMenu.tsx b/src/components/TournamentProvider/TournamentContextMenu.tsx new file mode 100644 index 00000000..c31d4535 --- /dev/null +++ b/src/components/TournamentProvider/TournamentContextMenu.tsx @@ -0,0 +1,38 @@ +import { Tournament, TournamentActionKey } from '~/api'; +import { ContextMenu } from '~/components/ContextMenu'; +import { ElementSize } from '~/types/componentLib'; +import { useActions } from './TournamentProvider.hooks'; + +export interface TournamentContextMenuProps { + className?: string; + tournament: Tournament; + size?: ElementSize; +} + +export const TournamentContextMenu = ({ + className, + tournament, + size, +}: TournamentContextMenuProps): JSX.Element => { + const actions = useActions(tournament); + return ( + + ); +}; diff --git a/src/components/TournamentProvider/TournamentProvider.context.ts b/src/components/TournamentProvider/TournamentProvider.context.ts index 4db0d997..3d17dc76 100644 --- a/src/components/TournamentProvider/TournamentProvider.context.ts +++ b/src/components/TournamentProvider/TournamentProvider.context.ts @@ -2,4 +2,6 @@ import { createContext } from 'react'; import { Tournament } from '~/api'; -export const TournamentContext = createContext(null); +export const tournamentContext = createContext(null); + +export const { Provider } = tournamentContext; diff --git a/src/components/TournamentProvider/TournamentProvider.hooks.ts b/src/components/TournamentProvider/TournamentProvider.hooks.ts deleted file mode 100644 index f8e934b2..00000000 --- a/src/components/TournamentProvider/TournamentProvider.hooks.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useContext } from 'react'; - -import { TournamentContext } from './TournamentProvider.context'; - -export const useTournament = () => { - const context = useContext(TournamentContext); - if (!context) { - throw Error('useTournament must be used within a or !'); - } - return context; -}; diff --git a/src/components/TournamentProvider/TournamentProvider.hooks.tsx b/src/components/TournamentProvider/TournamentProvider.hooks.tsx new file mode 100644 index 00000000..bd6e9637 --- /dev/null +++ b/src/components/TournamentProvider/TournamentProvider.hooks.tsx @@ -0,0 +1,47 @@ +import { useContext } from 'react'; + +import { Tournament, TournamentActionKey } from '~/api'; +import { Action } from '~/components/ContextMenu/ContextMenu.types'; +import { useAddPlayerAction } from './actions/useAddPlayerAction'; +import { useConfigureRoundAction } from './actions/useConfigureRoundAction'; +import { useDeleteAction } from './actions/useDeleteAction'; +import { useEditAction } from './actions/useEditAction'; +import { useEndAction } from './actions/useEndAction'; +import { useEndRoundAction } from './actions/useEndRoundAction'; +import { useJoinAction } from './actions/useJoinAction'; +import { useLeaveAction } from './actions/useLeaveAction'; +import { usePublishAction } from './actions/usePublishAction'; +import { useStartAction } from './actions/useStartAction'; +import { useStartRoundAction } from './actions/useStartRoundAction'; +import { useSubmitMatchResultAction } from './actions/useSubmitMatchResultAction'; +import { useUndoStartRoundAction } from './actions/useUndoStartRoundAction'; +import { tournamentContext } from './TournamentProvider.context'; + +export const useTournament = () => { + const context = useContext(tournamentContext); + if (!context) { + throw Error('useTournament must be used within a !'); + } + return context; +}; + +export const useActions = ( + tournament: Tournament, +): Record => [ + useAddPlayerAction(tournament), + useConfigureRoundAction(tournament), + useDeleteAction(tournament), + useEditAction(tournament), + useEndAction(tournament), + useEndRoundAction(tournament), + useJoinAction(tournament), + useLeaveAction(tournament), + usePublishAction(tournament), + useStartAction(tournament), + useStartRoundAction(tournament), + useSubmitMatchResultAction(tournament), + useUndoStartRoundAction(tournament), +].filter((a) => a !== null).reduce((acc, { key, ...action }) => ({ + ...acc, + [key]: action, +}), {} as Record); diff --git a/src/components/TournamentProvider/TournamentProvider.tsx b/src/components/TournamentProvider/TournamentProvider.tsx index c164ea81..e2c470d3 100644 --- a/src/components/TournamentProvider/TournamentProvider.tsx +++ b/src/components/TournamentProvider/TournamentProvider.tsx @@ -1,7 +1,7 @@ import { ReactNode } from 'react'; import { Tournament } from '~/api'; -import { TournamentContext } from './TournamentProvider.context'; +import { Provider } from './TournamentProvider.context'; export interface TournamentProviderProps { children: ReactNode; @@ -12,7 +12,7 @@ export const TournamentProvider = ({ children, tournament, }: TournamentProviderProps) => ( - + {children} - + ); diff --git a/src/components/TournamentProvider/actions/useAddPlayerAction.tsx b/src/components/TournamentProvider/actions/useAddPlayerAction.tsx new file mode 100644 index 00000000..d13fbf0e --- /dev/null +++ b/src/components/TournamentProvider/actions/useAddPlayerAction.tsx @@ -0,0 +1,44 @@ +import { UserPlus } from 'lucide-react'; + +import { Tournament, TournamentActionKey } from '~/api'; +import { ActionDefinition } from '~/components/ContextMenu/ContextMenu.types'; +import { toast } from '~/components/ToastProvider'; +import { TournamentRegistrationForm } from '~/components/TournamentRegistrationForm'; +import { useFormDialog } from '~/hooks/useFormDialog'; +import { useCreateTournamentRegistration } from '~/services/tournamentRegistrations'; + +const LABEL = 'Add Player'; +const KEY = TournamentActionKey.AddPlayer; + +export const useAddPlayerAction = ( + subject: Tournament, +): ActionDefinition | null => { + const { mutation } = useCreateTournamentRegistration({ + onSuccess: (response): void => { + toast.success(`${response.user?.displayName ?? 'Unknown Player'} has been added to ${subject.displayName}!`); + close(); + }, + }); + const { open, close } = useFormDialog({ + formId: 'create-tournament-registration-form', + title: LABEL, + submitLabel: LABEL, + content: ( + mutation(data)} + /> + ), + }); + if (subject.availableActions.includes(KEY)) { + return { + key: KEY, + label: LABEL, + icon: , + handler: () => open(), + }; + } + return null; +}; diff --git a/src/components/TournamentProvider/actions/useConfigureRoundAction.tsx b/src/components/TournamentProvider/actions/useConfigureRoundAction.tsx new file mode 100644 index 00000000..2cb2e445 --- /dev/null +++ b/src/components/TournamentProvider/actions/useConfigureRoundAction.tsx @@ -0,0 +1,61 @@ +import { generatePath, useNavigate } from 'react-router-dom'; + +import { Tournament, TournamentActionKey } from '~/api'; +import { ActionDefinition } from '~/components/ContextMenu/ContextMenu.types'; +import { Warning } from '~/components/generic/Warning'; +import { toast } from '~/components/ToastProvider'; +import { useDialogInstance } from '~/hooks/useDialogInstance'; +import { useGetTournamentCompetitorsByTournament } from '~/services/tournamentCompetitors'; +import { PATHS } from '~/settings'; +import { validateConfigureRound } from '../utils/validateConfigureRound'; + +const KEY = TournamentActionKey.ConfigureRound; + +export const useConfigureRoundAction = ( + subject: Tournament, +): ActionDefinition | null => { + const navigate = useNavigate(); + const nextRoundLabel = (subject.nextRound ?? 0) + 1; + const label = `Configure Round ${nextRoundLabel}`; + const { open, close } = useDialogInstance(); + const { data: tournamentCompetitors } = useGetTournamentCompetitorsByTournament({ + tournamentId: subject._id, + }); + const onConfirm = (): void => { + navigate(generatePath(PATHS.tournamentPairings, { id: subject._id })); + }; + if (subject.availableActions.includes(KEY)) { + return { + key: KEY, + label, + handler: () => { + const { errors, warnings } = validateConfigureRound(subject, tournamentCompetitors); + if (errors.length) { + return toast.error('Cannot Configure Round', { + description: errors, + }); + } + if (warnings.length) { + open({ + title: `Configure Round ${nextRoundLabel}`, + content: warnings.map((warning, i) => ( + {warning} + )), + actions: [ + { + onClick: () => { + onConfirm(); + close(); + }, + text: 'Proceed', + }, + ], + }); + } else { + onConfirm(); + } + }, + }; + } + return null; +}; diff --git a/src/components/TournamentProvider/actions/useDeleteAction.tsx b/src/components/TournamentProvider/actions/useDeleteAction.tsx new file mode 100644 index 00000000..c18ef79b --- /dev/null +++ b/src/components/TournamentProvider/actions/useDeleteAction.tsx @@ -0,0 +1,46 @@ +import { Tournament, TournamentActionKey } from '~/api'; +import { ActionDefinition } from '~/components/ContextMenu/ContextMenu.types'; +import { toast } from '~/components/ToastProvider'; +import { useDialogInstance } from '~/hooks/useDialogInstance'; +import { useNavigateAway } from '~/hooks/useNavigateAway'; +import { useDeleteTournament } from '~/services/tournaments'; +import { PATHS } from '~/settings'; +import { getTournamentDisplayName } from '~/utils/common/getTournamentDisplayName'; + +const LABEL = 'Delete'; +const KEY = TournamentActionKey.Delete; + +export const useDeleteAction = ( + subject: Tournament, +): ActionDefinition | null => { + const displayName = getTournamentDisplayName(subject); + const navigate = useNavigateAway(PATHS.tournamentDetails, PATHS.tournaments); + const { open, close } = useDialogInstance(); + const { mutation } = useDeleteTournament({ + onSuccess: (): void => { + toast.success(`${displayName} deleted!`); + navigate(); + close(); + }, + }); + if (subject.availableActions.includes(KEY)) { + return { + key: KEY, + label: LABEL, + handler: () => open({ + title: 'Warning!', + content: ( + {`Are you sure you want to delete ${displayName}?`}This cannot be undone! + ), + actions: [ + { + intent: 'danger', + onClick: () => mutation({ id: subject._id }), + text: 'Remove', + }, + ], + }), + }; + } + return null; +}; diff --git a/src/components/TournamentProvider/actions/useEditAction.tsx b/src/components/TournamentProvider/actions/useEditAction.tsx new file mode 100644 index 00000000..fa96afec --- /dev/null +++ b/src/components/TournamentProvider/actions/useEditAction.tsx @@ -0,0 +1,22 @@ +import { generatePath, useNavigate } from 'react-router-dom'; + +import { Tournament, TournamentActionKey } from '~/api'; +import { ActionDefinition } from '~/components/ContextMenu/ContextMenu.types'; +import { PATHS } from '~/settings'; + +const LABEL = 'Edit'; +const KEY = TournamentActionKey.Edit; + +export const useEditAction = ( + subject: Tournament, +): ActionDefinition | null => { + const navigate = useNavigate(); + if (subject.availableActions.includes(KEY)) { + return { + key: KEY, + label: LABEL, + handler: () => navigate(generatePath(PATHS.tournamentEdit, { id: subject._id })), + }; + } + return null; +}; diff --git a/src/components/TournamentProvider/actions/useEndAction.tsx b/src/components/TournamentProvider/actions/useEndAction.tsx new file mode 100644 index 00000000..cd259ffd --- /dev/null +++ b/src/components/TournamentProvider/actions/useEndAction.tsx @@ -0,0 +1,50 @@ +import { Tournament, TournamentActionKey } from '~/api'; +import { ActionDefinition } from '~/components/ContextMenu/ContextMenu.types'; +import { toast } from '~/components/ToastProvider'; +import { useDialogInstance } from '~/hooks/useDialogInstance'; +import { useEndTournament } from '~/services/tournaments'; + +const LABEL = 'End Tournament'; +const KEY = TournamentActionKey.End; + +export const useEndAction = ( + subject: Tournament, +): ActionDefinition | null => { + const remainingRoundsLabel = subject.roundCount - ((subject.lastRound ?? -1) + 1); + const { open, close } = useDialogInstance(); + const { mutation } = useEndTournament({ + onSuccess: (): void => { + toast.success(`${subject.displayName} completed!`); + close(); + }, + }); + if (subject.availableActions.includes(KEY)) { + return { + key: KEY, + label: LABEL, + handler: () => { + if (subject.nextRound !== undefined && subject.nextRound < subject.roundCount) { + open({ + title: 'Warning!', + content: ( + <> + {`Are you sure you want to end ${subject.displayName}? There are still ${remainingRoundsLabel} rounds remaining.`} + Once the tournament is ended, it cannot be restarted! + + ), + actions: [ + { + intent: 'danger', + onClick: () => mutation({ id: subject._id }), + text: LABEL, + }, + ], + }); + } else { + mutation({ id: subject._id }); + } + }, + }; + } + return null; +}; diff --git a/src/components/TournamentProvider/actions/useEndRoundAction.tsx b/src/components/TournamentProvider/actions/useEndRoundAction.tsx new file mode 100644 index 00000000..452b21c7 --- /dev/null +++ b/src/components/TournamentProvider/actions/useEndRoundAction.tsx @@ -0,0 +1,55 @@ +import { Tournament, TournamentActionKey } from '~/api'; +import { ActionDefinition } from '~/components/ContextMenu/ContextMenu.types'; +import { toast } from '~/components/ToastProvider'; +import { useDialogInstance } from '~/hooks/useDialogInstance'; +import { useEndTournamentRound, useGetTournamentOpenRound } from '~/services/tournaments'; + +const KEY = TournamentActionKey.EndRound; + +export const useEndRoundAction = ( + subject: Tournament, +): ActionDefinition | null => { + const currentRoundLabel = (subject.currentRound ?? 0) + 1; + const label = `End Round ${currentRoundLabel}`; + const { open, close } = useDialogInstance(); + const { data: openRound } = useGetTournamentOpenRound({ id: subject._id }); + const { mutation } = useEndTournamentRound({ + onSuccess: (): void => { + toast.success(`Round ${currentRoundLabel} completed!`); + close(); + }, + }); + if (subject.availableActions.includes(KEY)) { + return { + key: KEY, + label, + handler: () => { + if (openRound && openRound.matchResultsProgress.remaining > 0) { + open({ + title: 'Warning!', + content: ( + <> + {` + Are you sure you want to end round ${currentRoundLabel}? + There are still ${openRound.matchResultsProgress.remaining} + matches remaining to be checked in. + `} + Once the round is ended, it cannot be repeated! + + ), + actions: [ + { + intent: 'danger', + onClick: () => mutation({ id: subject._id }), + text: 'End Round', + }, + ], + }); + } else { + mutation({ id: subject._id }); + } + }, + }; + } + return null; +}; diff --git a/src/components/TournamentProvider/actions/useJoinAction.tsx b/src/components/TournamentProvider/actions/useJoinAction.tsx new file mode 100644 index 00000000..e2e0e8a7 --- /dev/null +++ b/src/components/TournamentProvider/actions/useJoinAction.tsx @@ -0,0 +1,49 @@ +import { UserPlus } from 'lucide-react'; + +import { Tournament, TournamentActionKey } from '~/api'; +import { useAuth } from '~/components/AuthProvider'; +import { ActionDefinition } from '~/components/ContextMenu/ContextMenu.types'; +import { toast } from '~/components/ToastProvider'; +import { useTournament } from '~/components/TournamentProvider'; +import { TournamentRegistrationForm } from '~/components/TournamentRegistrationForm'; +import { useFormDialog } from '~/hooks/useFormDialog'; +import { useCreateTournamentRegistration } from '~/services/tournamentRegistrations'; + +const LABEL = 'Join'; +const KEY = TournamentActionKey.Join; + +export const useJoinAction = ( + subject: Tournament, +): ActionDefinition | null => { + const user = useAuth(); + const tournament = useTournament(); + const { mutation } = useCreateTournamentRegistration({ + onSuccess: (): void => { + toast.success(`You have joined ${subject.displayName}!`); + close(); + }, + }); + const { open, close } = useFormDialog({ + formId: 'create-tournament-registration-form', + title: LABEL, + submitLabel: LABEL, + content: ( + mutation(data)} + /> + ), + }); + if (subject.availableActions.includes(KEY)) { + return { + key: KEY, + label: LABEL, + icon: , + handler: () => open(), + }; + } + return null; +}; diff --git a/src/components/TournamentProvider/actions/useLeaveAction.tsx b/src/components/TournamentProvider/actions/useLeaveAction.tsx new file mode 100644 index 00000000..4cc37560 --- /dev/null +++ b/src/components/TournamentProvider/actions/useLeaveAction.tsx @@ -0,0 +1,75 @@ +import { LogOut } from 'lucide-react'; + +import { Tournament, TournamentActionKey } from '~/api'; +import { useAuth } from '~/components/AuthProvider'; +import { ActionDefinition } from '~/components/ContextMenu/ContextMenu.types'; +import { toast } from '~/components/ToastProvider'; +import { useDialogInstance } from '~/hooks/useDialogInstance'; +import { useGetTournamentCompetitor } from '~/services/tournamentCompetitors'; +import { useDeleteTournamentRegistration, useGetTournamentRegistrationByTournamentUser } from '~/services/tournamentRegistrations'; + +const LABEL = 'Leave'; +const KEY = TournamentActionKey.Leave; + +export const useLeaveAction = ( + subject: Tournament, +): ActionDefinition | null => { + const user = useAuth(); + const { open, close } = useDialogInstance(); + + const { data: ownRegistration } = useGetTournamentRegistrationByTournamentUser(user ? { + userId: user._id, + tournamentId: subject._id, + } : 'skip'); + const { data: ownCompetitor } = useGetTournamentCompetitor(ownRegistration ? { + id: ownRegistration.tournamentCompetitorId, + } : 'skip'); + + const { mutation } = useDeleteTournamentRegistration({ + onSuccess: ({ wasLast }) => { + if (subject.useTeams && ownCompetitor) { + if (wasLast) { + toast.success(`${ownCompetitor.displayName} has left ${subject.displayName}.`); + } else { + toast.success(`You have left ${ownCompetitor.displayName}.`); + } + } else { + toast.success(`You have left ${subject.displayName}.`); + } + close(); + }, + }); + + if (!ownRegistration || !ownCompetitor) { + return null; + } + const hasOtherPlayers = ownCompetitor.registrations.length > 1; + const hasSparePlayers = ownCompetitor.registrations.length > subject.competitorSize; + if (subject.availableActions.includes(KEY)) { + return { + key: KEY, + label: LABEL, + icon: , + handler: () => { + if (user && subject.useTeams && hasOtherPlayers && !hasSparePlayers) { + open({ + title: 'Warning!', + content: ( + {`Are you sure you want to leave ${subject.displayName}? You will leave team ${ownCompetitor.displayName} short-handed.`} + ), + actions: [ + { + intent: 'danger', + onClick: () => mutation({ id: ownRegistration._id }), + text: 'Leave', + }, + ], + }); + } else { + mutation({ id: ownRegistration._id }); + } + }, + }; + } + return null; +}; diff --git a/src/components/TournamentProvider/actions/usePublishAction.tsx b/src/components/TournamentProvider/actions/usePublishAction.tsx new file mode 100644 index 00000000..2539185e --- /dev/null +++ b/src/components/TournamentProvider/actions/usePublishAction.tsx @@ -0,0 +1,25 @@ +import { Tournament, TournamentActionKey } from '~/api'; +import { ActionDefinition } from '~/components/ContextMenu/ContextMenu.types'; +import { toast } from '~/components/ToastProvider'; +import { usePublishTournament } from '~/services/tournaments'; + +const LABEL = 'Publish'; +const KEY = TournamentActionKey.Publish; + +export const usePublishAction = ( + subject: Tournament, +): ActionDefinition | null => { + const { mutation } = usePublishTournament({ + onSuccess: (): void => { + toast.success(`${subject.displayName} is now published!`); + }, + }); + if (subject.availableActions.includes(KEY)) { + return { + key: KEY, + label: LABEL, + handler: () => mutation({ id: subject._id }), + }; + } + return null; +}; diff --git a/src/components/TournamentProvider/actions/useStartAction.tsx b/src/components/TournamentProvider/actions/useStartAction.tsx new file mode 100644 index 00000000..0c04f90e --- /dev/null +++ b/src/components/TournamentProvider/actions/useStartAction.tsx @@ -0,0 +1,25 @@ +import { Tournament, TournamentActionKey } from '~/api'; +import { ActionDefinition } from '~/components/ContextMenu/ContextMenu.types'; +import { toast } from '~/components/ToastProvider'; +import { useStartTournament } from '~/services/tournaments'; + +const LABEL = 'Start'; +const KEY = TournamentActionKey.Start; + +export const useStartAction = ( + subject: Tournament, +): ActionDefinition | null => { + const { mutation } = useStartTournament({ + onSuccess: (): void => { + toast.success(`${subject.displayName} started!`); + }, + }); + if (subject.availableActions.includes(KEY)) { + return { + key: KEY, + label: LABEL, + handler: () => mutation({ id: subject._id }), + }; + } + return null; +}; diff --git a/src/components/TournamentProvider/actions/useStartRoundAction.tsx b/src/components/TournamentProvider/actions/useStartRoundAction.tsx new file mode 100644 index 00000000..015be8d7 --- /dev/null +++ b/src/components/TournamentProvider/actions/useStartRoundAction.tsx @@ -0,0 +1,25 @@ +import { Tournament, TournamentActionKey } from '~/api'; +import { ActionDefinition } from '~/components/ContextMenu/ContextMenu.types'; +import { toast } from '~/components/ToastProvider'; +import { useStartTournamentRound } from '~/services/tournaments'; +const KEY = TournamentActionKey.StartRound; + +export const useStartRoundAction = ( + subject: Tournament, +): ActionDefinition | null => { + const nextRoundLabel = (subject.nextRound ?? 0) + 1; + const currentRoundLabel = (subject.currentRound ?? 0) + 1; + const { mutation } = useStartTournamentRound({ + onSuccess: (): void => { + toast.success(`Round ${currentRoundLabel} started!`); + }, + }); + if (subject.availableActions.includes(KEY)) { + return { + key: KEY, + label: `Start Round ${nextRoundLabel}`, + handler: () => mutation({ id: subject._id }), + }; + } + return null; +}; diff --git a/src/components/TournamentProvider/actions/useSubmitMatchResultAction.tsx b/src/components/TournamentProvider/actions/useSubmitMatchResultAction.tsx new file mode 100644 index 00000000..68c79e3c --- /dev/null +++ b/src/components/TournamentProvider/actions/useSubmitMatchResultAction.tsx @@ -0,0 +1,21 @@ +import { Tournament, TournamentActionKey } from '~/api'; +import { ActionDefinition } from '~/components/ContextMenu/ContextMenu.types'; +import { useMatchResultCreateDialog } from '~/components/MatchResultCreateDialog'; + +const LABEL = 'Submit Match Result'; +const KEY = TournamentActionKey.SubmitMatchResult; + +export const useSubmitMatchResultAction = ( + subject: Tournament, +): ActionDefinition | null => { + // TODO: Replace with new dialog + const { open: openMatchResultCreateDialog } = useMatchResultCreateDialog(); + if (subject.availableActions.includes(KEY)) { + return { + key: KEY, + label: LABEL, + handler: () => openMatchResultCreateDialog(), + }; + } + return null; +}; diff --git a/src/components/TournamentProvider/actions/useUndoStartRoundAction.tsx b/src/components/TournamentProvider/actions/useUndoStartRoundAction.tsx new file mode 100644 index 00000000..c690254c --- /dev/null +++ b/src/components/TournamentProvider/actions/useUndoStartRoundAction.tsx @@ -0,0 +1,53 @@ +import { Tournament, TournamentActionKey } from '~/api'; +import { ActionDefinition } from '~/components/ContextMenu/ContextMenu.types'; +import { toast } from '~/components/ToastProvider'; +import { useDialogInstance } from '~/hooks/useDialogInstance'; +import { useEndTournamentRound, useGetTournamentOpenRound } from '~/services/tournaments'; + +const KEY = TournamentActionKey.UndoStartRound; + +export const useUndoStartRoundAction = ( + subject: Tournament, +): ActionDefinition | null => { + const currentRoundLabel = (subject.currentRound ?? 0) + 1; + const label = `Undo Start Round ${currentRoundLabel}`; + const { open, close } = useDialogInstance(); + const { data: openRound } = useGetTournamentOpenRound({ id: subject._id }); + const { mutation } = useEndTournamentRound({ + onSuccess: (): void => { + toast.success(`Round ${currentRoundLabel} reset!`); + close(); + }, + }); + if (subject.availableActions.includes(KEY)) { + return { + key: KEY, + label, + handler: () => { + const alreadyHasMatchResults = openRound && openRound.matchResultsProgress.submitted > 0; + open({ + title: 'Warning!', + content: ( + <> + {`Are you sure you want to reset round ${currentRoundLabel}?`} + {alreadyHasMatchResults && ( + + {`This round already has ${openRound.matchResultsProgress.submitted} matches results checked in. They will be deleted as part of the reset.`} + + )} + This action cannot be undone. You'll need to start the round over from the beginning. + + ), + actions: [ + { + intent: 'danger', + onClick: () => mutation({ id: subject._id, reset: true }), + text: label, + }, + ], + }); + }, + }; + } + return null; +}; diff --git a/src/components/TournamentProvider/index.ts b/src/components/TournamentProvider/index.ts index d1f905ce..3db610df 100644 --- a/src/components/TournamentProvider/index.ts +++ b/src/components/TournamentProvider/index.ts @@ -1,2 +1,25 @@ -export { TournamentProvider } from './TournamentProvider'; -export { useTournament } from './TournamentProvider.hooks'; +export { useAddPlayerAction } from './actions/useAddPlayerAction'; +export { useConfigureRoundAction } from './actions/useConfigureRoundAction'; +export { useDeleteAction } from './actions/useDeleteAction'; +export { useEditAction } from './actions/useEditAction'; +export { useEndAction } from './actions/useEndAction'; +export { useEndRoundAction } from './actions/useEndRoundAction'; +export { useJoinAction } from './actions/useJoinAction'; +export { usePublishAction } from './actions/usePublishAction'; +export { useStartAction } from './actions/useStartAction'; +export { useStartRoundAction } from './actions/useStartRoundAction'; +export { useSubmitMatchResultAction } from './actions/useSubmitMatchResultAction'; +export { useUndoStartRoundAction } from './actions/useUndoStartRoundAction'; +export { + TournamentContextMenu, + type TournamentContextMenuProps, +} from './TournamentContextMenu'; +export { + TournamentProvider, + type TournamentProviderProps, +} from './TournamentProvider'; +export { + useActions, + useTournament, +} from './TournamentProvider.hooks'; +export { validateConfigureRound } from './utils/validateConfigureRound'; diff --git a/src/components/TournamentActionsProvider/utils/validateConfigureRound.tsx b/src/components/TournamentProvider/utils/validateConfigureRound.tsx similarity index 83% rename from src/components/TournamentActionsProvider/utils/validateConfigureRound.tsx rename to src/components/TournamentProvider/utils/validateConfigureRound.tsx index c4da791f..03912793 100644 --- a/src/components/TournamentActionsProvider/utils/validateConfigureRound.tsx +++ b/src/components/TournamentProvider/utils/validateConfigureRound.tsx @@ -2,7 +2,6 @@ import { ReactNode } from 'react'; import { Tournament, TournamentCompetitor } from '~/api'; import { IdentityBadge } from '~/components/IdentityBadge'; -import { getTournamentCompetitorDisplayName } from '~/utils/common/getTournamentCompetitorDisplayName'; export const validateConfigureRound = ( tournament: Tournament, @@ -30,10 +29,10 @@ export const validateConfigureRound = ( for (const competitor of active) { const activePlayers = competitor.registrations.filter(({ active }) => active); if (activePlayers.length > tournament.competitorSize) { - errors.push(`${getTournamentCompetitorDisplayName(competitor)} has too many active players.`); + errors.push(`${competitor.displayName} has too many active players.`); } if (activePlayers.length < tournament.competitorSize) { - errors.push(`${getTournamentCompetitorDisplayName(competitor)} has too few active players.`); + errors.push(`${competitor.displayName} has too few active players.`); } } if (inactive.length > 0) { @@ -46,7 +45,12 @@ export const validateConfigureRound = ( `}

{inactive.map((tournamentCompetitor) => ( - + ))} , ); diff --git a/src/components/TournamentCompetitorEditDialog/TournamentCompetitorEditDialog.module.scss b/src/components/TournamentRegistrationForm/TournamentRegistrationForm.module.scss similarity index 59% rename from src/components/TournamentCompetitorEditDialog/TournamentCompetitorEditDialog.module.scss rename to src/components/TournamentRegistrationForm/TournamentRegistrationForm.module.scss index 3efd0820..a092b277 100644 --- a/src/components/TournamentCompetitorEditDialog/TournamentCompetitorEditDialog.module.scss +++ b/src/components/TournamentRegistrationForm/TournamentRegistrationForm.module.scss @@ -1,8 +1,7 @@ @use "/src/style/flex"; +@use "/src/style/text"; @use "/src/style/variables"; -.Form { +.TournamentRegistrationForm { @include flex.column; - - padding: 1rem var(--container-padding-x); } diff --git a/src/components/TournamentRegistrationForm/TournamentRegistrationForm.schema.ts b/src/components/TournamentRegistrationForm/TournamentRegistrationForm.schema.ts new file mode 100644 index 00000000..ab03ef46 --- /dev/null +++ b/src/components/TournamentRegistrationForm/TournamentRegistrationForm.schema.ts @@ -0,0 +1,39 @@ +import { z } from 'zod'; + +import { + TournamentCompetitorId, + TournamentId, + UserId, +} from '~/api'; + +// Helper to convert empty strings and null to undefined +const emptyToUndefined = (schema: T) => z.preprocess((val) => (val === '' || val === null ? undefined : val), schema); + +export const createSchema = () => z.object({ + tournamentCompetitor: emptyToUndefined(z.object({ + teamName: emptyToUndefined(z.string().min(2).optional()), + }).optional()), + tournamentCompetitorId: emptyToUndefined(z.string().transform((val) => val as TournamentCompetitorId).optional()), + tournamentId: z.string().transform((val) => val as TournamentId), + userId: z.string({ message: 'Please select a user.' }).transform((val) => val as UserId), +}); + +export type SubmitData = z.infer>; + +export type FormData = { + tournamentCompetitor: { + teamName: string; + }; + tournamentCompetitorId: TournamentCompetitorId | null; + tournamentId: TournamentId | null; + userId: UserId | null; +}; + +export const defaultValues: FormData = { + tournamentCompetitor: { + teamName: '', + }, + tournamentCompetitorId: null, + tournamentId: null, + userId: null, +}; diff --git a/src/components/TournamentRegistrationForm/TournamentRegistrationForm.tsx b/src/components/TournamentRegistrationForm/TournamentRegistrationForm.tsx new file mode 100644 index 00000000..340243b1 --- /dev/null +++ b/src/components/TournamentRegistrationForm/TournamentRegistrationForm.tsx @@ -0,0 +1,170 @@ +import { MouseEvent, useEffect } from 'react'; +import { SubmitHandler, useForm } from 'react-hook-form'; +import { Button } from '@ianpaschal/combat-command-components'; +import clsx from 'clsx'; +import { Trash } from 'lucide-react'; + +import { TournamentId } from '~/api'; +import { Form, FormField } from '~/components/generic/Form'; +import { InputSelect } from '~/components/generic/InputSelect'; +import { InputText } from '~/components/generic/InputText'; +import { Separator } from '~/components/generic/Separator'; +import { InputUser } from '~/components/InputUser'; +import { useGetTournamentCompetitorsByTournament } from '~/services/tournamentCompetitors'; +import { useGetTournamentRegistrationsByTournament } from '~/services/tournamentRegistrations'; +import { useGetTournament } from '~/services/tournaments'; +import { getEtcCountryOptions } from '~/utils/common/getCountryOptions'; +import { validateForm } from '~/utils/validateForm'; +import { + createSchema, + defaultValues, + FormData, + SubmitData, +} from './TournamentRegistrationForm.schema'; + +import styles from './TournamentRegistrationForm.module.scss'; + +export interface TournamentRegistrationFormProps { + className?: string; + disabled?: boolean; + existingValues?: Partial; + forcedValues?: Partial & { tournamentId: TournamentId }; + id?: string; + loading?: boolean; + onSubmit: (data: SubmitData) => void; + setDirty?: (dirty: boolean) => void; +} + +export const TournamentRegistrationForm = ({ + className, + disabled = false, + existingValues, + forcedValues, + id, + loading = false, + onSubmit, + setDirty, +}: TournamentRegistrationFormProps): JSX.Element => { + const schema = createSchema(); + const form = useForm({ + defaultValues: { + ...defaultValues, + ...existingValues, + ...forcedValues, + }, + mode: 'onSubmit', + }); + + const tournamentId = form.watch('tournamentId') as TournamentId | undefined; + + const { + data: existingTournament, + loading: existingTournamentLoading, + } = useGetTournament(tournamentId ? { + id: tournamentId, + } : 'skip'); + const { + data: existingCompetitors, + loading: existingCompetitorsLoading, + } = useGetTournamentCompetitorsByTournament(tournamentId ? { + tournamentId, + } : 'skip'); + const { + data: existingRegistrations, + loading: existingRegistrationsLoading, + } = useGetTournamentRegistrationsByTournament(tournamentId ? { + tournamentId, + } : 'skip'); + const existingCompetitorTeamNames = new Set(); + const existingCompetitorOptions = (existingCompetitors ?? []).map((r) => { + if (r.teamName) { + existingCompetitorTeamNames.add(r.teamName); + } + return { + value: r._id, + label: r.displayName, + }; + }); + const availableCompetitorOptions = getEtcCountryOptions().filter((option) => ( + !existingCompetitorTeamNames.has(option.value) + )); + const excludedUserIds = (existingRegistrations ?? []).map((r) => r.userId); + + // Track form dirty state and notify parent + useEffect(() => { + setDirty?.(form.formState.isDirty); + }, [form.formState.isDirty, setDirty]); + + const handleClearTournamentCompetitorId = (e: MouseEvent): void => { + e.preventDefault(); + form.setValue('tournamentCompetitorId', null); + }; + + const handleClearTeamName = (e: MouseEvent): void => { + e.preventDefault(); + form.setValue('tournamentCompetitor.teamName', ''); + }; + + const handleSubmit: SubmitHandler = async (formData): Promise => { + const validFormData = validateForm(schema, { + ...formData, + ...forcedValues, + }, form.setError); + if (validFormData) { + onSubmit(validFormData); + } + }; + + const showLoading = [ + loading, + existingTournamentLoading, + existingCompetitorsLoading, + existingRegistrationsLoading, + ].some((l) => !!l); + + const showUserInput = !forcedValues?.userId; + const showTeamNameInput = !forcedValues?.tournamentCompetitorId && existingTournament?.useTeams; + + return ( +
+ {showUserInput && ( + + + + )} + {showTeamNameInput && ( +
+ + + +
+ )} +
+ ); +}; diff --git a/src/components/TournamentRegistrationForm/index.ts b/src/components/TournamentRegistrationForm/index.ts new file mode 100644 index 00000000..bcff8c6d --- /dev/null +++ b/src/components/TournamentRegistrationForm/index.ts @@ -0,0 +1,4 @@ +export { + TournamentRegistrationForm, + type TournamentRegistrationFormProps, +} from './TournamentRegistrationForm'; diff --git a/src/components/TournamentRegistrationProvider/TournamentRegistrationActiveToggle.tsx b/src/components/TournamentRegistrationProvider/TournamentRegistrationActiveToggle.tsx new file mode 100644 index 00000000..cb098fb1 --- /dev/null +++ b/src/components/TournamentRegistrationProvider/TournamentRegistrationActiveToggle.tsx @@ -0,0 +1,23 @@ +import { TournamentRegistration } from '~/api'; +import { Switch } from '~/components/generic/Switch'; +import { useToggleActiveAction } from './actions/useToggleActiveAction'; + +export interface TournamentRegistrationActiveToggleProps { + className?: string; + tournamentRegistration: TournamentRegistration; +} + +export const TournamentRegistrationActiveToggle = ({ + className, + tournamentRegistration, +}: TournamentRegistrationActiveToggleProps): JSX.Element => { + const action = useToggleActiveAction(tournamentRegistration); + return ( + action?.handler()} + /> + ); +}; diff --git a/src/components/TournamentRegistrationProvider/TournamentRegistrationContextMenu.tsx b/src/components/TournamentRegistrationProvider/TournamentRegistrationContextMenu.tsx new file mode 100644 index 00000000..c5405a27 --- /dev/null +++ b/src/components/TournamentRegistrationProvider/TournamentRegistrationContextMenu.tsx @@ -0,0 +1,21 @@ +import { TournamentRegistration, TournamentRegistrationActionKey } from '~/api'; +import { ContextMenu } from '~/components/ContextMenu'; +import { useActions } from './TournamentRegistrationProvider.hooks'; + +export interface TournamentRegistrationContextMenuProps { + className?: string; + tournamentRegistration: TournamentRegistration; +} + +export const TournamentRegistrationContextMenu = ({ + className, + tournamentRegistration, +}: TournamentRegistrationContextMenuProps): JSX.Element => { + const actions = useActions(tournamentRegistration); + return ( + + ); +}; diff --git a/src/components/TournamentRegistrationProvider/TournamentRegistrationProvider.context.tsx b/src/components/TournamentRegistrationProvider/TournamentRegistrationProvider.context.tsx new file mode 100644 index 00000000..fda17c3e --- /dev/null +++ b/src/components/TournamentRegistrationProvider/TournamentRegistrationProvider.context.tsx @@ -0,0 +1,7 @@ +import { createContext } from 'react'; + +import { TournamentRegistration } from '~/api'; + +export const tournamentRegistrationContext = createContext(null); + +export const { Provider } = tournamentRegistrationContext; diff --git a/src/components/TournamentRegistrationProvider/TournamentRegistrationProvider.hooks.ts b/src/components/TournamentRegistrationProvider/TournamentRegistrationProvider.hooks.ts new file mode 100644 index 00000000..15161c84 --- /dev/null +++ b/src/components/TournamentRegistrationProvider/TournamentRegistrationProvider.hooks.ts @@ -0,0 +1,34 @@ +import { useContext } from 'react'; + +import { TournamentRegistration, TournamentRegistrationActionKey } from '~/api'; +import { Action } from '~/components/ContextMenu/ContextMenu.types'; +import { useTournamentCompetitor } from '~/components/TournamentCompetitorProvider'; +import { useLeaveAction } from '~/components/TournamentCompetitorProvider/actions/useLeaveAction'; +import { useDeleteAction } from './actions/useDeleteAction'; +import { useToggleActiveAction } from './actions/useToggleActiveAction'; +import { tournamentRegistrationContext } from './TournamentRegistrationProvider.context'; + +export const useTournamentRegistration = () => { + const context = useContext(tournamentRegistrationContext); + if (!context) { + throw Error('useTournamentRegistration must be used within a !'); + } + return context; +}; + +export const useActions = ( + subject: TournamentRegistration, +): Record => { + const leaveAction = useLeaveAction(useTournamentCompetitor()); + return [ + useDeleteAction(subject), + ...(leaveAction ? [{ + ...leaveAction, + key: TournamentRegistrationActionKey.Leave, + }] : []), + useToggleActiveAction(subject), + ].filter((a) => a !== null).reduce((acc, { key, ...action }) => ({ + ...acc, + [key]: action, + }), {} as Record); +}; diff --git a/src/components/TournamentRegistrationProvider/TournamentRegistrationProvider.tsx b/src/components/TournamentRegistrationProvider/TournamentRegistrationProvider.tsx new file mode 100644 index 00000000..f85cb7ac --- /dev/null +++ b/src/components/TournamentRegistrationProvider/TournamentRegistrationProvider.tsx @@ -0,0 +1,18 @@ +import { ReactNode } from 'react'; + +import { TournamentRegistration } from '~/api'; +import { Provider } from './TournamentRegistrationProvider.context'; + +export interface TournamentRegistrationProviderProps { + children: ReactNode; + tournamentRegistration: TournamentRegistration; +} + +export const TournamentRegistrationProvider = ({ + children, + tournamentRegistration, +}: TournamentRegistrationProviderProps) => ( + + {children} + +); diff --git a/src/components/TournamentRegistrationProvider/actions/useDeleteAction.tsx b/src/components/TournamentRegistrationProvider/actions/useDeleteAction.tsx new file mode 100644 index 00000000..567a37b4 --- /dev/null +++ b/src/components/TournamentRegistrationProvider/actions/useDeleteAction.tsx @@ -0,0 +1,65 @@ +import { generatePath } from 'react-router-dom'; +import { Trash } from 'lucide-react'; + +import { TournamentRegistration, TournamentRegistrationActionKey } from '~/api'; +import { ActionDefinition } from '~/components/ContextMenu/ContextMenu.types'; +import { toast } from '~/components/ToastProvider'; +import { useTournamentCompetitor } from '~/components/TournamentCompetitorProvider/TournamentCompetitorProvider.hooks'; +import { useTournament } from '~/components/TournamentProvider'; +import { useDialogInstance } from '~/hooks/useDialogInstance'; +import { useNavigateAway } from '~/hooks/useNavigateAway'; +import { useDeleteTournamentRegistration } from '~/services/tournamentRegistrations'; +import { PATHS } from '~/settings'; + +const LABEL = 'Remove'; +const KEY = TournamentRegistrationActionKey.Delete; + +export const useDeleteAction = ( + subject: TournamentRegistration, +): ActionDefinition | null => { + const tournamentCompetitor = useTournamentCompetitor(); + const tournament = useTournament(); + const navigateToTournament = useNavigateAway(PATHS.tournamentCompetitorDetails, generatePath(PATHS.tournamentDetails, { + id: subject.tournamentId, + })); + const { open, close } = useDialogInstance(); + const { mutation } = useDeleteTournamentRegistration({ + onSuccess: ({ wasLast }) => { + if (wasLast) { + navigateToTournament(); + } + if (tournament.useTeams) { + if (wasLast) { + toast.success(`${tournamentCompetitor.displayName} has been removed from ${tournament.displayName}.`); + } else { + toast.success(`${subject.displayName} has been removed from ${tournamentCompetitor.displayName}`); + } + } else { + toast.success(`${subject.displayName} has been removed from ${tournament.displayName}.`); + } + close(); + }, + }); + if (subject.availableActions.includes(KEY)) { + return { + key: KEY, + label: LABEL, + icon: , + intent: 'danger', + handler: () => open({ + title: 'Warning!', + content: ( + {`Are you sure you want to remove ${subject.displayName}?`} + ), + actions: [ + { + intent: 'danger', + onClick: () => mutation({ id: subject._id }), + text: 'Remove', + }, + ], + }), + }; + } + return null; +}; diff --git a/src/components/TournamentRegistrationProvider/actions/useToggleActiveAction.tsx b/src/components/TournamentRegistrationProvider/actions/useToggleActiveAction.tsx new file mode 100644 index 00000000..b5fc96bf --- /dev/null +++ b/src/components/TournamentRegistrationProvider/actions/useToggleActiveAction.tsx @@ -0,0 +1,27 @@ +import { UserCheck, UserX } from 'lucide-react'; + +import { TournamentRegistration, TournamentRegistrationActionKey } from '~/api'; +import { useAuth } from '~/components/AuthProvider'; +import { ActionDefinition } from '~/components/ContextMenu/ContextMenu.types'; +import { toast } from '~/components/ToastProvider'; +import { useToggleTournamentRegistrationActive } from '~/services/tournamentRegistrations'; + +const KEY = TournamentRegistrationActionKey.ToggleActive; + +export const useToggleActiveAction = ( + subject: TournamentRegistration, +): ActionDefinition | null => { + const user = useAuth(); + const { mutation } = useToggleTournamentRegistrationActive({ + onSuccess: (active): void => toast.success(`${user?._id === subject.userId ? 'You are' : `${subject.displayName} is`} now ${active ? 'active' : 'inactive'}.`), + }); + if (subject.availableActions.includes(KEY)) { + return { + key: KEY, + label: subject.active ? 'Deactivate' : 'Activate', + icon: subject.active ? : , + handler: () => mutation({ id: subject._id }), + }; + } + return null; +}; diff --git a/src/components/TournamentRegistrationProvider/index.ts b/src/components/TournamentRegistrationProvider/index.ts new file mode 100644 index 00000000..45d7728f --- /dev/null +++ b/src/components/TournamentRegistrationProvider/index.ts @@ -0,0 +1,19 @@ +export { useDeleteAction } from './actions/useDeleteAction'; +export { useToggleActiveAction } from './actions/useToggleActiveAction'; +export { + TournamentRegistrationActiveToggle, + type TournamentRegistrationActiveToggleProps, +} from './TournamentRegistrationActiveToggle'; +export { + TournamentRegistrationContextMenu, + type TournamentRegistrationContextMenuProps, +} from './TournamentRegistrationContextMenu'; +export { + TournamentRegistrationProvider, + type TournamentRegistrationProviderProps, +} from './TournamentRegistrationProvider'; +export { + useActions, + useTournamentRegistration, +} from './TournamentRegistrationProvider.hooks'; +export { useLeaveAction } from '~/components/TournamentCompetitorProvider/actions/useLeaveAction'; diff --git a/src/components/TournamentRoster/TournamentRoster.module.scss b/src/components/TournamentRoster/TournamentRoster.module.scss deleted file mode 100644 index c7c9e565..00000000 --- a/src/components/TournamentRoster/TournamentRoster.module.scss +++ /dev/null @@ -1,48 +0,0 @@ -@use "/src/style/flex"; -@use "/src/style/variables"; -@use "/src/style/variants"; -@use "/src/style/borders"; -@use "/src/style/corners"; -@use "/src/style/shadows"; -@use "/src/style/text"; - -.TournamentRoster { - &_Header { - display: grid; - grid-template-areas: "Identity PlayerCount Actions"; - grid-template-columns: 1fr auto auto; - grid-template-rows: 2.5rem; - gap: 1rem; - - width: 100%; - } - - &_Identity { - grid-area: Identity; - } - - &_PlayerCount { - grid-area: PlayerCount; - } - - &_Actions { - grid-area: Actions; - } - - &_Content { - @include flex.column; - - padding: 1rem calc(1rem - var(--border-width)); - padding-left: calc(3.75rem - var(--border-width)); - } - - &_RegistrationRow { - @include flex.row; - } - - &_RegistrationSwitch { - @include flex.row; - - margin-left: auto; - } -} diff --git a/src/components/TournamentRoster/TournamentRoster.tsx b/src/components/TournamentRoster/TournamentRoster.tsx deleted file mode 100644 index 09a2c75d..00000000 --- a/src/components/TournamentRoster/TournamentRoster.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { MouseEvent } from 'react'; - -import { useAuth } from '~/components/AuthProvider'; -import { Accordion, AccordionItem } from '~/components/generic/Accordion'; -import { Label } from '~/components/generic/Label'; -import { Switch } from '~/components/generic/Switch'; -import { IdentityBadge } from '~/components/IdentityBadge'; -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 { useToggleTournamentRegistrationActive } from '~/services/tournamentRegistrations'; -import { isUserTournamentOrganizer } from '~/utils/common/isUserTournamentOrganizer'; - -import styles from './TournamentRoster.module.scss'; - -export interface TournamentRosterProps { - className?: string; -} - -export const TournamentRoster = ({ - className, -}: TournamentRosterProps): JSX.Element => { - const user = useAuth(); - const tournament = useTournament(); - const isOrganizer = isUserTournamentOrganizer(user, tournament); - - const competitors = useTournamentCompetitors(); - const { mutation: toggleActive } = useToggleTournamentRegistrationActive(); - - const showActiveToggle = isOrganizer && tournament.status === 'active' && tournament.currentRound === undefined; - return ( - - {(competitors || []).map((competitor) => ( - -
- - {tournament.useTeams && ( - - )} - -
-
- {competitor.registrations.map((r) => ( -
- - {showActiveToggle && ( -
- - { - e.stopPropagation(); - toggleActive({ id: r._id }); - }} - checked={r.active} - /> -
- )} -
- ))} -
-
- ))} -
- ); -}; diff --git a/src/components/TournamentRoster/components/CompetitorActions/CompetitorActions.module.scss b/src/components/TournamentRoster/components/CompetitorActions/CompetitorActions.module.scss deleted file mode 100644 index 4efbf490..00000000 --- a/src/components/TournamentRoster/components/CompetitorActions/CompetitorActions.module.scss +++ /dev/null @@ -1,13 +0,0 @@ -@use "/src/style/flex"; -@use "/src/style/variables"; -@use "/src/style/variants"; -@use "/src/style/borders"; -@use "/src/style/corners"; -@use "/src/style/shadows"; -@use "/src/style/text"; - -.CompetitorActions { - @include flex.row; - - flex-shrink: 0; -} diff --git a/src/components/TournamentRoster/components/CompetitorActions/CompetitorActions.tsx b/src/components/TournamentRoster/components/CompetitorActions/CompetitorActions.tsx deleted file mode 100644 index 5df08ee7..00000000 --- a/src/components/TournamentRoster/components/CompetitorActions/CompetitorActions.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { MouseEvent } from 'react'; -import clsx from 'clsx'; -import { Ellipsis, UserPlus } from 'lucide-react'; - -import { TournamentCompetitor } from '~/api'; -import { useAuth } from '~/components/AuthProvider'; -import { ConfirmationDialog, useConfirmationDialog } from '~/components/ConfirmationDialog'; -import { Button } from '~/components/generic/Button'; -import { Label } from '~/components/generic/Label'; -import { PopoverMenu } from '~/components/generic/PopoverMenu'; -import { Switch } from '~/components/generic/Switch'; -import { TournamentCompetitorEditDialog, useTournamentCompetitorEditDialog } from '~/components/TournamentCompetitorEditDialog'; -import { useTournament } from '~/components/TournamentProvider'; -import { useDeleteTournamentCompetitor, useToggleTournamentCompetitorActive } from '~/services/tournamentCompetitors'; -import { - useCreateTournamentRegistration, - useDeleteTournamentRegistration, - useGetTournamentRegistrationsByTournament, -} from '~/services/tournamentRegistrations'; -import { isUserTournamentOrganizer } from '~/utils/common/isUserTournamentOrganizer'; - -import styles from './CompetitorActions.module.scss'; - -export interface CompetitorActionsProps { - className?: string; - competitor: TournamentCompetitor; -} - -export const CompetitorActions = ({ - className, - competitor, -}: CompetitorActionsProps): JSX.Element => { - const user = useAuth(); - const tournament = useTournament(); - const { - data: registrations, - loading, - } = useGetTournamentRegistrationsByTournament({ - tournamentId: competitor.tournamentId, - }); - const { open: openEditDialog } = useTournamentCompetitorEditDialog(competitor._id); - const confirmDeleteCompetitorDialogId = `confirm-delete-competitor-${competitor._id}`; - const { open: openConfirmDeleteDialog } = useConfirmationDialog(confirmDeleteCompetitorDialogId); - const { mutation: toggleActive } = useToggleTournamentCompetitorActive(); - const { mutation: createRegistration } = useCreateTournamentRegistration({ - successMessage: `Successfully joined ${tournament.title}!`, - }); - const { mutation: deleteRegistration } = useDeleteTournamentRegistration({ - successMessage: `Successfully left ${tournament.title}!`, - }); - const { mutation: deleteCompetitor } = useDeleteTournamentCompetitor({ - successMessage: 'Successfully deleted!', - }); - - const handleToggleActive = (e: MouseEvent): void => { - e.stopPropagation(); - toggleActive({ id: competitor._id }); - }; - - const handleJoin = (e: MouseEvent): void => { - e.stopPropagation(); - createRegistration({ - userId: user!._id, - tournamentId: tournament._id, - tournamentCompetitorId: competitor._id, - }); - }; - - const handleDelete = (): void => { - deleteCompetitor({ id: competitor._id }); - }; - - const ownRegistration = (registrations ?? []).find((r) => r.userId === user?._id); - const competitorRegistrations = (registrations ?? []).filter((r) => r.tournamentCompetitorId === competitor._id); - const isOrganizer = isUserTournamentOrganizer(user, tournament); - const isPlayerForCompetitor = user && !loading && ownRegistration?.tournamentCompetitorId === competitor._id; - const isCaptain = user && competitor.captainUserId === user._id; - const isFull = competitorRegistrations.filter((r) => r.active).length >= tournament.competitorSize; - - const showCheckInToggle = isOrganizer && tournament.status === 'active' && tournament.currentRound === undefined; - const showJoinButton = !isFull && !loading && !ownRegistration && tournament.useTeams && tournament.status === 'published'; - - // TODO: Add list submissions - // const showListsButton = user && (isOrganizer || isPlayer) && status !== 'archived'; - - const menuItems = [ - { - label: 'Edit', - onClick: () => openEditDialog({ tournamentCompetitor: competitor }), - visible: (isOrganizer || isCaptain) && tournament.status !== 'archived' && tournament.currentRound === undefined, - }, - { - label: 'Leave', - onClick: () => { - if (isPlayerForCompetitor) { - deleteRegistration({ id: ownRegistration._id }); - } - }, - visible: isPlayerForCompetitor && tournament.status === 'published', - }, - { - label: 'Remove', - onClick: () => openConfirmDeleteDialog(), - visible: isOrganizer && tournament.status === 'published', - }, - ]; - - const visibleMenuItems = menuItems.filter((item) => item.visible); - - return ( - <> -
- {showCheckInToggle && ( - <> - - - - )} - {showJoinButton && ( -
- - - - ); -}; diff --git a/src/components/TournamentRoster/components/CompetitorActions/index.ts b/src/components/TournamentRoster/components/CompetitorActions/index.ts deleted file mode 100644 index be922d34..00000000 --- a/src/components/TournamentRoster/components/CompetitorActions/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { CompetitorActions } from './CompetitorActions'; diff --git a/src/components/TournamentRoster/components/PlayerCount/PlayerCount.module.scss b/src/components/TournamentRoster/components/PlayerCount/PlayerCount.module.scss deleted file mode 100644 index e6c4b647..00000000 --- a/src/components/TournamentRoster/components/PlayerCount/PlayerCount.module.scss +++ /dev/null @@ -1,12 +0,0 @@ -@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 deleted file mode 100644 index 431e5e50..00000000 --- a/src/components/TournamentRoster/components/PlayerCount/PlayerCount.tsx +++ /dev/null @@ -1,26 +0,0 @@ -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.registrations ?? []).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 deleted file mode 100644 index 6e47b7b8..00000000 --- a/src/components/TournamentRoster/components/PlayerCount/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export type { PlayerCountProps } from './PlayerCount'; -export { PlayerCount } from './PlayerCount'; diff --git a/src/components/TournamentRoster/index.ts b/src/components/TournamentRoster/index.ts deleted file mode 100644 index bf3538f5..00000000 --- a/src/components/TournamentRoster/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { TournamentRoster } from './TournamentRoster'; diff --git a/src/components/generic/Avatar/Avatar.tsx b/src/components/generic/Avatar/Avatar.tsx index e2d866ea..f70726eb 100644 --- a/src/components/generic/Avatar/Avatar.tsx +++ b/src/components/generic/Avatar/Avatar.tsx @@ -1,4 +1,9 @@ -import { ReactElement } from 'react'; +import { + ComponentPropsWithoutRef, + ElementRef, + forwardRef, + ReactElement, +} from 'react'; import clsx from 'clsx'; import { User, Users } from 'lucide-react'; import { Avatar as RadixAvatar } from 'radix-ui'; @@ -7,7 +12,8 @@ import { FlagCircle } from '~/components/generic/FlagCircle'; import styles from './Avatar.module.scss'; -export interface AvatarProps { +export type AvatarRef = ElementRef; +export type AvatarProps = ComponentPropsWithoutRef & { className?: string; countryCode?: string; isTeam?: boolean; @@ -16,9 +22,9 @@ export interface AvatarProps { loading?: boolean; url?: string; userId?: string; -} +}; -export const Avatar = ({ +export const Avatar = forwardRef(({ className, countryCode, isTeam = false, @@ -26,7 +32,7 @@ export const Avatar = ({ loading = false, icon, url, -}: AvatarProps): JSX.Element => { +}, ref) => { const getInnerContent = (): ReactElement | null => { if (loading) { return null; @@ -43,7 +49,7 @@ export const Avatar = ({ return ; }; return ( - +
{getInnerContent()}
@@ -52,4 +58,6 @@ export const Avatar = ({ )}
); -}; +}); + +Avatar.displayName = RadixAvatar.Root.displayName; diff --git a/src/components/generic/Button/Button.module.scss b/src/components/generic/Button/Button.module.scss index 4ddb7fea..48d9a7e6 100644 --- a/src/components/generic/Button/Button.module.scss +++ b/src/components/generic/Button/Button.module.scss @@ -72,6 +72,10 @@ padding: 0 0.375rem; + &[data-collapse-padding="true"] { + margin: 0 -0.375rem; + } + svg { width: 0.75rem; height: 0.75rem; @@ -83,6 +87,10 @@ padding: 0 0.5rem; + &[data-collapse-padding="true"] { + margin: 0 -0.5rem; + } + svg { width: 1rem; height: 1rem; @@ -94,6 +102,10 @@ padding: 0 0.75rem; + &[data-collapse-padding="true"] { + margin: 0 -0.75rem; + } + svg { width: 1rem; height: 1rem; @@ -105,6 +117,10 @@ padding: 0 0.875rem; + &[data-collapse-padding="true"] { + margin: 0 -0.875rem; + } + svg { width: 1.25rem; height: 1.25rem; diff --git a/src/components/generic/Button/Button.tsx b/src/components/generic/Button/Button.tsx index 7ab984a2..60c6b432 100644 --- a/src/components/generic/Button/Button.tsx +++ b/src/components/generic/Button/Button.tsx @@ -20,10 +20,11 @@ export interface ButtonProps extends Omit(({ @@ -32,10 +33,11 @@ export const Button = forwardRef(({ iconPosition, intent = 'default', loading = false, - round, + rounded, size = 'normal', text, variant = 'primary', + collapsePadding, ...props }, ref): JSX.Element => (
))} diff --git a/src/components/generic/ScrollArea/ScrollArea.hooks.tsx b/src/components/generic/ScrollArea/ScrollArea.hooks.tsx index f8d9dcd7..0d208988 100644 --- a/src/components/generic/ScrollArea/ScrollArea.hooks.tsx +++ b/src/components/generic/ScrollArea/ScrollArea.hooks.tsx @@ -12,15 +12,17 @@ import { ScrollArea } from 'radix-ui'; import styles from './ScrollArea.module.scss'; +export type IndicatorSide = 'top' | 'right' | 'bottom' | 'left'; +export type IndicatorState = { + visible?: boolean; + border?: boolean; +}; +export type IndicatorConfig = Partial>; + /** * Type to track the state of an attribute-per-side of a box model. */ -export interface FourSidedState { - top: boolean; - bottom: boolean; - left: boolean; - right: boolean; -} +export type FourSidedState = Record; /** * Default state for an attribute-per-side of a box model. @@ -34,17 +36,14 @@ const defaultState: FourSidedState = { top: false, bottom: false, left: false, r * @returns The border visibility state per side. */ const getBorderVisibility = ( - borders?: string | string[], + config?: IndicatorConfig, ): FourSidedState => { - if (borders) { - if (Array.isArray(borders)) { - return borders.reduce((acc, value) => ({ - ...acc, - [value]: true, - }), defaultState); - } else { - return { ...defaultState, [borders]: true }; - } + if (config) { + return Object.keys(defaultState).reduce((acc, side) => { + const key = side as IndicatorSide; + acc[key] = config[key]?.border ?? defaultState[key]; + return acc; + }, {} as Record); } else { return defaultState; } @@ -92,7 +91,7 @@ type UseScrollIndicatorsReturn = { * @param borders - Side or sides to render a border on. * @returns A object containing a viewport ref, onScroll handler, and four sides' indicator elements. */ -export const useScrollIndicators = (borders?: string | string[]): UseScrollIndicatorsReturn => { +export const useScrollIndicators = (config: IndicatorConfig = {}): UseScrollIndicatorsReturn => { const ref = useRef>(null); const [visible, setVisible] = useState({ @@ -103,7 +102,7 @@ export const useScrollIndicators = (borders?: string | string[]): UseScrollIndic }); // Memoize since it will rarely, if ever, change - const bordered = useMemo(() => getBorderVisibility(borders), [borders]); + const bordered = useMemo(() => getBorderVisibility(config), [config]); const updateIndicatorVisibility = () => { setVisible(getIndicatorVisibility(ref)); @@ -129,8 +128,8 @@ export const useScrollIndicators = (borders?: string | string[]): UseScrollIndic className={styles.ScrollArea_Indicator} key={key} data-side={key} - data-visible={visible[key as keyof typeof defaultState]} - data-bordered={bordered[key as keyof typeof defaultState]} + data-visible={config[key as IndicatorSide]?.visible ?? visible[key as IndicatorSide]} + data-bordered={bordered[key as IndicatorSide]} /> )), }; diff --git a/src/components/generic/ScrollArea/ScrollArea.tsx b/src/components/generic/ScrollArea/ScrollArea.tsx index 9ba5352e..b8f06e18 100644 --- a/src/components/generic/ScrollArea/ScrollArea.tsx +++ b/src/components/generic/ScrollArea/ScrollArea.tsx @@ -13,18 +13,24 @@ import styles from './ScrollArea.module.scss'; type ScrollAreaRef = ElementRef; +type IndicatorSide = 'top' | 'right' | 'bottom' | 'left'; +type IndicatorState = { + visible?: boolean; + border?: boolean; +}; + type ScrollAreaProps = ComponentPropsWithoutRef & { - indicatorBorders?: string | string[]; + indicators?: Partial>; }; export const ScrollArea = forwardRef(({ className, children, - indicatorBorders, + indicators: indicatorConfig, onScroll, ...props }, ref) => { - const { ref: viewportRef, updateIndicators, indicators } = useScrollIndicators(indicatorBorders); + const { ref: viewportRef, updateIndicators, indicators } = useScrollIndicators(indicatorConfig); const handleScroll = (e: UIEvent): void => { updateIndicators(e); if (onScroll) { diff --git a/src/components/generic/Table/README.md b/src/components/generic/Table/README.md deleted file mode 100644 index dee3000a..00000000 --- a/src/components/generic/Table/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Table - -The `` component is a minimalist, scrollable table to provide a level of organization above what simple stack/list would. - -This component behaves like a table embedded in a document, rather than a spreadsheet. -There is no horizontal scrolling. -The features are similar to what you would find in a Markdown table or on Confluence: - -- Set explicit column widths; Unset columns are evenly divided among remaining space. -- Set column alignment, defaults to `left`. -- Default rendering for text and numbers, or via custom `render()` method. \ No newline at end of file diff --git a/src/components/generic/Table/Table.module.scss b/src/components/generic/Table/Table.module.scss deleted file mode 100644 index 9c614f7e..00000000 --- a/src/components/generic/Table/Table.module.scss +++ /dev/null @@ -1,78 +0,0 @@ -@use "/src/style/flex"; -@use "/src/style/borders"; -@use "/src/style/text"; -@use "/src/style/variables"; - -@mixin row { - display: grid; - gap: 1rem; - min-height: calc(2.5rem + var(--border-width)); - background-color: var(--card-bg); -} - -@mixin row-text { - overflow: hidden; - max-width: 100%; - text-overflow: ellipsis; - white-space: nowrap; -} - -.Table { - @include flex.column($gap: 0); - - min-height: 0; - - &_ScrollArea { - position: relative; - overflow: hidden; // Radix handles scroll - } - - &_Inner { - @include flex.column($gap: 0); - - max-width: 100%; - } - - &_HeaderRow { - @include borders.normal($side: bottom); - @include row; - - h3 { - @include row-text; - } - } - - &_Row { - @include borders.muted($side: bottom); - @include row; - - &:last-child { - border: none; - } - } - - &_Cell { - @include text.ui; - - overflow: hidden; - display: flex; - align-items: center; - min-width: 1.5rem; - - &[data-align="left"] { - justify-content: start; - } - - &[data-align="center"] { - justify-content: center; - } - - &[data-align="right"] { - justify-content: end; - } - - &_Text { - @include row-text; - } - } -} diff --git a/src/components/generic/Table/Table.tsx b/src/components/generic/Table/Table.tsx deleted file mode 100644 index d4c1960e..00000000 --- a/src/components/generic/Table/Table.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import clsx from 'clsx'; - -import { ScrollArea } from '~/components/generic/ScrollArea'; -import { ColumnDef, RowData } from './Table.types'; -import { TableRow } from './TableRow'; - -import styles from './Table.module.scss'; - -export interface TableProps { - className?: string; - columns: ColumnDef[]; - rowClassName?: string; - rows: T[]; -} - -export const Table = ({ - className, - columns, - rowClassName, - rows, -}: TableProps): JSX.Element => ( -
- - -
- {rows.map((r, i) => ( - - ))} -
-
-
-); diff --git a/src/components/generic/Table/Table.types.ts b/src/components/generic/Table/Table.types.ts deleted file mode 100644 index 3b32fbf2..00000000 --- a/src/components/generic/Table/Table.types.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ReactNode } from 'react'; - -export type RowData = { [key: string]: unknown }; - -export type ColumnDef = { - align?: 'left' | 'center' | 'right'; - className?: string; - key: string; - label?: string; - renderCell?: (row: T, index: number) => ReactNode; - renderHeader?: () => ReactNode; - width?: number | 'auto'; -}; - -export type Row = [T, number]; diff --git a/src/components/generic/Table/TableCell.tsx b/src/components/generic/Table/TableCell.tsx deleted file mode 100644 index 43d65098..00000000 --- a/src/components/generic/Table/TableCell.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { isValidElement, ReactElement } from 'react'; -import clsx from 'clsx'; - -import { ColumnDef, RowData } from './Table.types'; - -import styles from './Table.module.scss'; - -export interface TableCellProps { - column: ColumnDef; - row?: T; - index: number; -} - -export const TableCell = ({ - column, - row, - index, -}: TableCellProps): JSX.Element => { - const className = clsx(styles.Table_Cell, column.className); - const renderInner = (): ReactElement | null => { - if (!row) { - if (column.renderHeader) { - const el = column.renderHeader(); - return isValidElement(el) ? el :

{el}

; - } - return

{column?.label ?? ''}

; - } - if (row) { - if (column.renderCell) { - const el = column.renderCell(row, index); - return isValidElement(el) ? el : {el}; - } - return {`${row?.[column.key]}`}; - } - return null; - }; - - return ( -
- {renderInner()} -
- ); -}; diff --git a/src/components/generic/Table/TableRow.tsx b/src/components/generic/Table/TableRow.tsx deleted file mode 100644 index 93f29c62..00000000 --- a/src/components/generic/Table/TableRow.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import clsx from 'clsx'; - -import { - ColumnDef, - Row, - RowData, -} from './Table.types'; -import { TableCell } from './TableCell'; - -import styles from './Table.module.scss'; - -export interface TableRowProps { - className?: string; - columns: ColumnDef[]; - row?: Row; - index?: number; -} - -export const TableRow = ({ - className, - columns, - row, - index = -1, -}: TableRowProps): JSX.Element => { - const gridTemplateColumns = columns.map((c) => c.width ? `${c.width}px` : '1fr').join(' '); - if (!row) { - return ( -
- {columns.map((c) => ( - - ))} -
- ); - } else { - const [r, i] = row; - return ( -
- {columns.map((c) => ( - - ))} -
- ); - } -}; diff --git a/src/components/generic/Table/index.ts b/src/components/generic/Table/index.ts deleted file mode 100644 index d534009c..00000000 --- a/src/components/generic/Table/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type { TableProps } from './Table'; -export { Table } from './Table'; -export type { ColumnDef } from './Table.types'; diff --git a/src/components/generic/Tabs/TabsList.module.scss b/src/components/generic/Tabs/TabsList.module.scss index 04aceb14..2d715b99 100644 --- a/src/components/generic/Tabs/TabsList.module.scss +++ b/src/components/generic/Tabs/TabsList.module.scss @@ -3,26 +3,19 @@ @use "/src/style/sizes"; @use "/src/style/variables"; -.Root { +.TabsList { @include corners.normal; - min-width: min-content; + display: grid; + gap: 0.25rem; padding: 0.25rem; background-color: var(--secondary-default-bg); - &-min { + &[data-width="min"] { width: min-content; } - &-max { + &[data-width="max"] { width: 100%; } - - &-vertical { - @include flex.column($gap: 0.25rem); - } - - &-horizontal { - @include flex.row($gap: 0.25rem); - } } diff --git a/src/components/generic/Tabs/TabsList.tsx b/src/components/generic/Tabs/TabsList.tsx index 772075d9..2b38a001 100644 --- a/src/components/generic/Tabs/TabsList.tsx +++ b/src/components/generic/Tabs/TabsList.tsx @@ -1,5 +1,6 @@ import { ComponentPropsWithoutRef, + CSSProperties, ElementRef, forwardRef, ReactElement, @@ -25,6 +26,7 @@ type TabsListProps = ComponentPropsWithoutRef & { size?: ElementSize; tabs: TabDef[]; hideLabels?: boolean; + stretch?: boolean; }; export const TabsList = forwardRef(({ className, @@ -33,18 +35,31 @@ export const TabsList = forwardRef(({ size = 'normal', tabs, hideLabels = false, + stretch = false, ...props -}, ref): JSX.Element => ( - - {tabs.map(({ value, label, icon }) => ( - - {icon} - {!hideLabels && label} - - ))} - -)); +}, ref): JSX.Element => { + const createGridStyle = (): CSSProperties => { + const size = stretch ? '1fr' : 'min-content'; + const key = orientation === 'vertical' ? 'gridTemplateRows' : 'gridTemplateColumns'; + return { + [key]: tabs.map((_) => size).join(' '), + }; + }; + return ( + + {tabs.map(({ value, label, icon }) => ( + + {icon} + {!hideLabels && label} + + ))} + + ); +}); diff --git a/src/components/generic/Tabs/TabsTrigger.scss b/src/components/generic/Tabs/TabsTrigger.scss index edcdf198..d6601be3 100644 --- a/src/components/generic/Tabs/TabsTrigger.scss +++ b/src/components/generic/Tabs/TabsTrigger.scss @@ -8,17 +8,14 @@ .TabsTrigger { @include corners.tight; - @include flex.row($gap: 0.25rem); + @include flex.row($gap: 0.25rem, $xAlign: center); @include flex.stretchy; @include text.ui; @include variants.secondary; - flex-grow: 0; - height: 100%; min-height: 2rem; padding: 0 0.75rem; - white-space: nowrap; &[data-state="active"] { diff --git a/src/components/generic/Tag/Tag.scss b/src/components/generic/Tag/Tag.scss index 594b729b..26d1c701 100644 --- a/src/components/generic/Tag/Tag.scss +++ b/src/components/generic/Tag/Tag.scss @@ -6,19 +6,14 @@ .Tag { @include corners.tight; - @include borders.normal; - @include text.ui($bold: true); + @include text.ui; @include flex.row($gap: 0.25rem); width: min-content; - - // display: inline-block; - height: 1.5rem; margin: -2px 0; padding: 0 5px; - line-height: calc(1.5rem - (2 * variables.$border-width)); color: var(--secondary-default-text); text-wrap: nowrap; diff --git a/src/hooks/useDebouncedState.ts b/src/hooks/useDebouncedState.ts new file mode 100644 index 00000000..22b43310 --- /dev/null +++ b/src/hooks/useDebouncedState.ts @@ -0,0 +1,40 @@ +import { useState } from 'react'; +import debounce from 'debounce'; + +type UseDebouncedStateResult = [ + { + value: T; + debouncedValue: T; + debouncing: boolean; + }, + (value: T) => void, +]; + +export const useDebouncedState = ( + initialState: T, + debounceTime = 250, +): UseDebouncedStateResult => { + const [value, setValue] = useState(initialState); + const [debouncedValue, setDebouncedValue] = useState(initialState); + const [debouncing, setDebouncing] = useState(false); + + const debouncedSetSearch = debounce((v: T): void => { + setDebouncedValue(v); + setDebouncing(false); + }, debounceTime); + + const update = (v: T): void => { + setValue(v); + debouncedSetSearch(v); + setDebouncing(true); + }; + + return [ + { + value, + debouncedValue, + debouncing, + }, + update, + ]; +}; diff --git a/src/hooks/useDialogInstance.ts b/src/hooks/useDialogInstance.ts new file mode 100644 index 00000000..a87e9676 --- /dev/null +++ b/src/hooks/useDialogInstance.ts @@ -0,0 +1,29 @@ +import { useCallback, useRef } from 'react'; +import { DialogProps, useDialogManager } from '@ianpaschal/combat-command-components'; + +type OpenDialogArgs = Omit; + +export const useDialogInstance = (): { + open: (props: OpenDialogArgs) => void; + close: () => void; +} => { + const { open, close } = useDialogManager(); + const rootIdRef = useRef(null); + + const openConfirm = useCallback((props: OpenDialogArgs) => { + const id = open(props); + rootIdRef.current = id; + }, [open]); + + const closeConfirm = useCallback(() => { + if (rootIdRef.current) { + close(rootIdRef.current); + rootIdRef.current = null; + } + }, [close]); + + return { + open: openConfirm, + close: closeConfirm, + }; +}; diff --git a/src/hooks/useFormDialog.tsx b/src/hooks/useFormDialog.tsx new file mode 100644 index 00000000..9bdf42b5 --- /dev/null +++ b/src/hooks/useFormDialog.tsx @@ -0,0 +1,73 @@ +import { + cloneElement, + ReactElement, + useRef, +} from 'react'; +import { DialogProps, useDialogManager } from '@ianpaschal/combat-command-components'; + +type UseFormDialogProps = Pick & { + content: ReactElement; + formId: string; + submitLabel: string; +}; +type UseFormDialogResult = { + open: () => void; + close: () => void; +}; + +export const useFormDialog = ({ + content, + formId, + submitLabel, + title, +}: UseFormDialogProps): UseFormDialogResult => { + const { open, close, setDirty } = useDialogManager(); + const dialogId = useRef(undefined); + return { + open: () => dialogId.current = open({ + title, + content: cloneElement(content, { + id: formId, + setDirty: (dirty: boolean): void => { + if (dialogId.current) { + setDirty(dialogId.current, dirty); + } + }, + }), + onCancel: (dirty: boolean): void => { + if (dirty) { + open({ + title: 'Discard Changes', + content: ( + Are you sure you want to navigate away? Your changes will be lost. + ), + actions: [{ + intent: 'danger', + onClick: () => { + if (dialogId.current) { + close(dialogId.current); + dialogId.current = undefined; + } + }, + text: 'Discard', + }], + }); + } else if (dialogId.current) { + close(dialogId.current); + dialogId.current = undefined; + } + }, + actions: [{ + form: formId, + text: submitLabel, + type: 'submit', + }], + }), + close: () => { + if (dialogId.current) { + close(dialogId.current); + dialogId.current = undefined; + } + }, + }; +}; diff --git a/src/hooks/useNavigateAway.ts b/src/hooks/useNavigateAway.ts new file mode 100644 index 00000000..06b3c7f4 --- /dev/null +++ b/src/hooks/useNavigateAway.ts @@ -0,0 +1,15 @@ +import { + matchPath, + useLocation, + useNavigate, +} from 'react-router-dom'; + +export const useNavigateAway = (pattern: string, target: string): () => void => { + const location = useLocation(); + const navigate = useNavigate(); + return () => { + if (matchPath(pattern, location.pathname)) { + navigate(target, { replace: true }); + } + }; +}; diff --git a/src/pages/DashboardPage/components/ActiveTournament/ActiveTournament.module.scss b/src/pages/DashboardPage/components/ActiveTournament/ActiveTournament.module.scss index 7efb0066..1689c230 100644 --- a/src/pages/DashboardPage/components/ActiveTournament/ActiveTournament.module.scss +++ b/src/pages/DashboardPage/components/ActiveTournament/ActiveTournament.module.scss @@ -62,10 +62,9 @@ } &_Rankings { - flex-grow: 1; + --table-padding: var(--container-padding-x); + --cell-padding-y: 1rem; - &_Row { - padding: 0.5rem var(--container-padding-x); - } + flex-grow: 1; } } diff --git a/src/pages/DashboardPage/components/ActiveTournament/ActiveTournament.tsx b/src/pages/DashboardPage/components/ActiveTournament/ActiveTournament.tsx index 1d4fe0ee..57380dc9 100644 --- a/src/pages/DashboardPage/components/ActiveTournament/ActiveTournament.tsx +++ b/src/pages/DashboardPage/components/ActiveTournament/ActiveTournament.tsx @@ -7,8 +7,7 @@ import { useAuth } from '~/components/AuthProvider'; import { Button } from '~/components/generic/Button'; import { Separator } from '~/components/generic/Separator'; import { IdentityBadge } from '~/components/IdentityBadge'; -import { TournamentActionsProvider } from '~/components/TournamentActionsProvider'; -import { TournamentContextMenu } from '~/components/TournamentContextMenu'; +import { TournamentContextMenu } from '~/components/TournamentProvider'; import { TournamentProvider } from '~/components/TournamentProvider'; import { TournamentTimer } from '~/components/TournamentTimer'; import { useGetTournamentCompetitorsByTournament } from '~/services/tournamentCompetitors'; @@ -67,9 +66,7 @@ export const ActiveTournament = ({
{isOrganizer && ( - - - + )}
diff --git a/src/pages/DashboardPage/components/ActiveTournament/ActiveTournament.utils.tsx b/src/pages/DashboardPage/components/ActiveTournament/ActiveTournament.utils.tsx index 937debcb..aa247343 100644 --- a/src/pages/DashboardPage/components/ActiveTournament/ActiveTournament.utils.tsx +++ b/src/pages/DashboardPage/components/ActiveTournament/ActiveTournament.utils.tsx @@ -1,4 +1,5 @@ import { ReactElement } from 'react'; +import { Table } from '@ianpaschal/combat-command-components'; import { Trophy } from 'lucide-react'; import { @@ -9,7 +10,6 @@ import { } from '~/api'; import { EmptyState } from '~/components/EmptyState'; import { Pulsar } from '~/components/generic/Pulsar'; -import { Table } from '~/components/generic/Table'; import { IdentityBadge } from '~/components/IdentityBadge'; import styles from './ActiveTournament.module.scss'; @@ -56,14 +56,13 @@ export const renderRankings = ( ) : (
(a.rank ?? Infinity) - (b.rank ?? Infinity))} columns={[ { key: 'rank', label: 'Rank', - width: 40, - align: 'center', + width: 'auto', + xAlign: 'center', renderCell: ({ rank }) => (
{tournament.lastRound !== undefined && rank !== undefined ? rank + 1 : '-'} @@ -72,6 +71,7 @@ export const renderRankings = ( }, { key: 'identity', + width: '1fr', label: tournament?.useTeams ? 'Team' : 'Player', renderCell: (competitor) => ( } /> ) : ( // TODO: Add pagination to table -
+
) )} diff --git a/src/pages/LeagueDetailPage/components/LeagueRankingsCard/LeagueRankingsCard.utils.tsx b/src/pages/LeagueDetailPage/components/LeagueRankingsCard/LeagueRankingsCard.utils.tsx index 7f6238a5..de3d1146 100644 --- a/src/pages/LeagueDetailPage/components/LeagueRankingsCard/LeagueRankingsCard.utils.tsx +++ b/src/pages/LeagueDetailPage/components/LeagueRankingsCard/LeagueRankingsCard.utils.tsx @@ -1,8 +1,8 @@ +import { ColumnDef } from '@ianpaschal/combat-command-components'; import { getGameSystem } from '@ianpaschal/combat-command-game-systems/common'; import { League, LeagueRanking } from '~/api'; import { InfoPopover } from '~/components/generic/InfoPopover'; -import { ColumnDef } from '~/components/generic/Table'; import { IdentityBadge } from '~/components/IdentityBadge'; import styles from './LeagueRankingsCard.module.scss'; @@ -15,13 +15,14 @@ export const getLeagueRankingTableConfig = ( { key: 'rank', label: 'Rank', - width: 40, - align: 'center', + width: '3rem', + xAlign: 'center', renderCell: (r) =>
{r.rank + 1}
, }, { key: 'identity', label: 'Player', + width: '1fr', renderCell: (r) => ( r.tournamentCount, renderHeader: () => ( @@ -44,8 +45,8 @@ export const getLeagueRankingTableConfig = ( }, ...league.rankingFactors.map((key): ColumnDef => ({ key, - width: 40, - align: 'center', + width: '3rem', + xAlign: 'center', renderCell: (r) => r.rankingFactors[key] ?? '-', renderHeader: () => ( diff --git a/src/pages/TournamentCompetitorDetailPage/TournamentCompetitorDetailPage.module.scss b/src/pages/TournamentCompetitorDetailPage/TournamentCompetitorDetailPage.module.scss new file mode 100644 index 00000000..0c871463 --- /dev/null +++ b/src/pages/TournamentCompetitorDetailPage/TournamentCompetitorDetailPage.module.scss @@ -0,0 +1,156 @@ +@use "/src/style/flex"; +@use "/src/style/text"; +@use "/src/style/variables"; +@use "/src/style/corners"; +@use "/src/style/variants"; +@use "/src/style/shadows"; +@use "/src/style/borders"; + +.TournamentCompetitorPage { + --ranking-gap: 0.5rem; + --avatar-size: 8rem; + --ranking-size: 4rem; + + @include variants.card; + @include flex.column($gap: 0); + + &_Header { + padding: var(--container-padding-y) var(--container-padding-x); + } + + &_Avatar { + grid-area: avatar; + width: 8rem; + height: 8rem; + } + + &_Details { + display: grid; + grid-area: details; + grid-template-areas: + "name actions" + "tournament actions"; + grid-template-columns: auto 1fr; + grid-template-rows: min-content min-content; + row-gap: 0.25rem; + column-gap: 1rem; + align-self: center; + } + + &_Name { + @include flex.column($xAlign: left, $gap: 0.25rem); + + h1 { + @include text.single-line; + } + } + + &_Tournament { + @include text.ui($muted: true); + @include text.single-line; + } + + &_Actions { + grid-area: actions; + align-self: center; + justify-self: start; + } + + &_ContextMenu { + margin-left: auto; + } + + &_Table { + --table-padding: var(--container-padding-x); + } + + &_Players { + @include text.ui; + @include flex.row($gap: 0.25rem, $xAlign: center); + + grid-area: players; + } + + &_Ranking { + display: grid; + grid-area: ranking; + grid-template-areas: "." "rank" "label"; + grid-template-columns: 1fr; + grid-template-rows: 1fr auto 1fr; + gap: 0.75rem; + } + + &_Rank { + @include text.large; + + grid-area: rank; + align-self: center; + justify-self: center; + + font-size: 3rem; + line-height: 2rem; + } + + &_RankLabel { + @include text.small($muted: true); + + grid-area: label; + align-self: start; + justify-self: center; + } + + &_Manage { + @include flex.row; + + padding: 0.5rem var(--container-padding-x) var(--container-padding-y); + } + + &_Tabs { + overflow: hidden; + display: grid; + grid-template-rows: auto 1fr; + min-height: 0; + } + + &_TabsList { + margin-bottom: 1rem; + padding: 0.5rem var(--container-padding-x); + border-radius: 0; + } + + &_TabsContent { + @include flex.column($gap: 1rem); + } + + &_Warnings { + @include flex.column; + + padding: 0 var(--container-padding-x); + } + + &_Actions { + @include flex.row($xAlign: right); + @include borders.normal($side: top); + + width: 100%; + padding: 1rem var(--container-padding-x); + } +} + +@media (width >= 480px) { + .TournamentCompetitorPage { + --ranking-size: var(--avatar-size); + + &_Name { + @include flex.row; + } + } +} + +@media (width >= 688px) { + .TournamentCompetitorPage { + &_Name { + @include flex.row; + } + } +} diff --git a/src/pages/TournamentCompetitorDetailPage/TournamentCompetitorDetailPage.tsx b/src/pages/TournamentCompetitorDetailPage/TournamentCompetitorDetailPage.tsx new file mode 100644 index 00000000..b554bb82 --- /dev/null +++ b/src/pages/TournamentCompetitorDetailPage/TournamentCompetitorDetailPage.tsx @@ -0,0 +1,110 @@ +import { useParams } from 'react-router-dom'; +import clsx from 'clsx'; +import { + Construction, + LineChart, + Swords, + Users, +} from 'lucide-react'; +import { useQueryState } from 'nuqs'; + +import { TournamentCompetitorId, TournamentId } from '~/api'; +import { useAuth } from '~/components/AuthProvider'; +import { EmptyState } from '~/components/EmptyState'; +import { + Tabs, + TabsContent, + TabsList, +} from '~/components/generic/Tabs'; +import { NotFoundView } from '~/components/NotFoundView'; +import { PageWrapper } from '~/components/PageWrapper'; +import { TournamentCompetitorProvider } from '~/components/TournamentCompetitorProvider'; +import { TournamentProvider } from '~/components/TournamentProvider'; +import { DeviceSize, useDeviceSize } from '~/hooks/useDeviceSize'; +import { TournamentRegistrationsTable } from '~/pages/TournamentCompetitorDetailPage/components/TournamentRegistrationsTable'; +import { useGetTournamentCompetitor } from '~/services/tournamentCompetitors'; +import { useGetTournament } from '~/services/tournaments'; +import { getLastVisibleTournamentRound } from '~/utils/common/getLastVisibleTournamentRound'; +import { Header } from './components/Header'; + +import styles from './TournamentCompetitorDetailPage.module.scss'; + +export interface TournamentCompetitorDetailPageProps { + className?: string; +} + +export const TournamentCompetitorDetailPage = ({ + className, +}: TournamentCompetitorDetailPageProps): JSX.Element => { + const user = useAuth(); + const params = useParams(); + const tournamentId = params.tournamentId! as TournamentId; // Must exist or else how did we get to this route? + const tournamentCompetitorId = params.tournamentCompetitorId! as TournamentCompetitorId; // Must exist or else how did we get to this route? + const { + data: tournament, + loading: isTournamentLoading, + } = useGetTournament({ id: tournamentId }); + + const { + data: tournamentCompetitor, + loading: isTournamentCompetitorLoading, + } = useGetTournamentCompetitor({ + id: tournamentCompetitorId, + includeRankings: tournament ? getLastVisibleTournamentRound(tournament, user) : undefined, + }); + const isLoading = isTournamentLoading || isTournamentCompetitorLoading; + + const tabs = [ + { value: 'roster', label: 'Roster', icon: }, + { value: 'matchResults', label: 'Match Results', icon: }, + { value: 'stats', label: 'Stats', icon: }, + ]; + const [tab, setTab] = useQueryState('tab', { defaultValue: 'roster' }); + const [deviceSize] = useDeviceSize(); + const showTabLabels = deviceSize >= DeviceSize.Default; + + if (isLoading) { + return
Loading...
; + } + + if (!tournament || !tournamentCompetitor) { + return ; + } + + // const showJoinButton = tournamentCompetitor.availableActions.includes(TournamentCompetitorActionKey.Join); + + return ( + + + +
+
+ + + + {/*
+ warnings +
*/} + + {/* {showJoinButton && ( +
+
+ )} */} +
+ + } message="Under Construction" /> + + + } message="Under Construction" /> + +
+
+
+
+
+ ); +}; diff --git a/src/pages/TournamentCompetitorDetailPage/components/Header/Header.module.scss b/src/pages/TournamentCompetitorDetailPage/components/Header/Header.module.scss new file mode 100644 index 00000000..d30e7909 --- /dev/null +++ b/src/pages/TournamentCompetitorDetailPage/components/Header/Header.module.scss @@ -0,0 +1,77 @@ +@use "/src/style/flex"; +@use "/src/style/text"; +@use "/src/style/corners"; +@use "/src/style/borders"; +@use "/src/style/variables"; +@use "/src/style/variants"; + +.Header { + display: grid; + grid-template-areas: + "avatar . actions" + "avatar name actions" + "avatar tournament actions" + "avatar standing actions"; + grid-template-columns: 8rem 1fr auto; + grid-template-rows: 1fr auto auto 1fr; + row-gap: 0.25rem; + column-gap: 1rem; + + box-sizing: content-box; + height: 8rem; + + &_Avatar { + grid-area: avatar; + } + + &_Badge { + @include corners.round; + @include borders.outlined; + @include flex.centered; + @include text.ui; + @include variants.primary($hover: false); + + position: relative; + + grid-column: 1 / 2; + grid-row: 4 / -1; + align-self: end; + justify-self: end; + + aspect-ratio: 1; + height: 100%; + padding-left: 2px; // Ugh, it looks better though. + + font-size: 1.25rem; + letter-spacing: 1px; // Ugh, it looks better though. + } + + &_Name { + @include flex.row; + + grid-area: name; + + h1 { + @include text.single-line; + } + } + + &_Tournament { + @include text.small($muted: true); + @include text.single-line; + + grid-area: tournament; + } + + &_ViewAllRankingsButton { + grid-area: standing; + align-self: center; + justify-self: start; + } + + &_Actions { + grid-area: actions; + align-self: start; + justify-self: end; + } +} diff --git a/src/pages/TournamentCompetitorDetailPage/components/Header/Header.tsx b/src/pages/TournamentCompetitorDetailPage/components/Header/Header.tsx new file mode 100644 index 00000000..d8af2702 --- /dev/null +++ b/src/pages/TournamentCompetitorDetailPage/components/Header/Header.tsx @@ -0,0 +1,70 @@ +import { generatePath, useNavigate } from 'react-router-dom'; +import clsx from 'clsx'; +import { ChevronRight, Users } from 'lucide-react'; + +import { Button } from '~/components/generic/Button'; +import { Tag } from '~/components/generic/Tag'; +import { TournamentCompetitorAvatar } from '~/components/IdentityBadge'; +import { TournamentCompetitorContextMenu } from '~/components/TournamentCompetitorProvider'; +import { useTournamentCompetitor } from '~/components/TournamentCompetitorProvider/TournamentCompetitorProvider.hooks'; +import { useTournament } from '~/components/TournamentProvider'; +import { PATHS } from '~/settings'; +import { getPathWithQuery } from '~/utils/common/getPathWithQuery'; + +import styles from './Header.module.scss'; + +export interface HeaderProps { + className?: string; +} + +export const Header = ({ + className, +}: HeaderProps): JSX.Element => { + const navigate = useNavigate(); + const tournament = useTournament(); + const tournamentCompetitor = useTournamentCompetitor(); + const tournamentCompetitorRank = tournamentCompetitor?.rank ?? -1; + const handleViewAllRankings = (): void => { + navigate(getPathWithQuery(generatePath(PATHS.tournamentDetails, { + id: tournament._id, + }), { + tab: 'rankings', + })); + }; + return ( +
+ + {tournamentCompetitorRank > -1 && ( +
+ {tournamentCompetitorRank + 1} +
+ )} +
+

{tournamentCompetitor.displayName}

+ + {`${tournamentCompetitor?.activeRegistrationCount ?? 0}/${tournament.competitorSize}`} + +
+ + {tournament.displayName} + +
+ ); +}; diff --git a/src/pages/TournamentCompetitorDetailPage/components/Header/index.ts b/src/pages/TournamentCompetitorDetailPage/components/Header/index.ts new file mode 100644 index 00000000..67b73e8c --- /dev/null +++ b/src/pages/TournamentCompetitorDetailPage/components/Header/index.ts @@ -0,0 +1,4 @@ +export { + Header, + type HeaderProps, +} from './Header'; diff --git a/src/pages/TournamentCompetitorDetailPage/components/MatchResultsList/MatchResultsList.module.scss b/src/pages/TournamentCompetitorDetailPage/components/MatchResultsList/MatchResultsList.module.scss new file mode 100644 index 00000000..831cbd69 --- /dev/null +++ b/src/pages/TournamentCompetitorDetailPage/components/MatchResultsList/MatchResultsList.module.scss @@ -0,0 +1,27 @@ +@use "/src/style/flex"; + +.MatchResultsList { + overflow: hidden; + display: grid; + grid-template-columns: 1fr; + grid-template-rows: auto 1fr; + gap: 1rem; + + min-height: 0; + max-height: 100%; + + &_Header { + padding: 0 var(--container-padding-x); + } + + &_List { + @include flex.column($xAlign: center); + + align-items: stretch; + padding: 0 var(--container-padding-x) var(--container-padding-y); + } + + &_LoadMoreButton { + justify-self: center; + } +} diff --git a/src/pages/TournamentCompetitorDetailPage/components/MatchResultsList/MatchResultsList.tsx b/src/pages/TournamentCompetitorDetailPage/components/MatchResultsList/MatchResultsList.tsx new file mode 100644 index 00000000..9464357a --- /dev/null +++ b/src/pages/TournamentCompetitorDetailPage/components/MatchResultsList/MatchResultsList.tsx @@ -0,0 +1,56 @@ +import { useState } from 'react'; +import clsx from 'clsx'; +import { Swords } from 'lucide-react'; + +import { TournamentCompetitorId } from '~/api'; +import { EmptyState } from '~/components/EmptyState'; +import { InputSelect } from '~/components/generic/InputSelect'; +import { ScrollArea } from '~/components/generic/ScrollArea'; +import { MatchResultCard } from '~/components/MatchResultCard'; +import { PaginatedList } from '~/components/PaginatedList'; +import { useTournament } from '~/components/TournamentProvider'; +import { useGetMatchResults } from '~/services/matchResults'; + +import styles from './MatchResultsList.module.scss'; + +export interface MatchResultsListProps { + className?: string; + tournamentCompetitorId?: TournamentCompetitorId; +} + +export const MatchResultsList = ({ + className, + // tournamentCompetitorId, +}: MatchResultsListProps): JSX.Element => { + const tournament = useTournament(); + const [round, setRound] = useState(tournament.lastRound ?? 0); + const roundOptions = Array.from({ + length: (tournament.currentRound ?? tournament.lastRound ?? 0) + 1, + }, (_, i) => ({ + label: `Round ${i + 1}`, + value: i, + })); + + const query = useGetMatchResults({}); + + return ( +
+
+ setRound(selected as number)} + disabled={query.loading} + /> +
+ + } + emptyState={} />} + /> + +
+ ); +}; diff --git a/src/pages/TournamentCompetitorDetailPage/components/MatchResultsList/index.ts b/src/pages/TournamentCompetitorDetailPage/components/MatchResultsList/index.ts new file mode 100644 index 00000000..18387807 --- /dev/null +++ b/src/pages/TournamentCompetitorDetailPage/components/MatchResultsList/index.ts @@ -0,0 +1,4 @@ +export { + MatchResultsList, + type MatchResultsListProps, +} from './MatchResultsList'; diff --git a/src/pages/TournamentCompetitorDetailPage/components/TournamentRegistrationsTable/TournamentRegistrationsTable.module.scss b/src/pages/TournamentCompetitorDetailPage/components/TournamentRegistrationsTable/TournamentRegistrationsTable.module.scss new file mode 100644 index 00000000..5c0e061f --- /dev/null +++ b/src/pages/TournamentCompetitorDetailPage/components/TournamentRegistrationsTable/TournamentRegistrationsTable.module.scss @@ -0,0 +1,16 @@ +@use "/src/style/flex"; +@use "/src/style/variables"; + +@import "@radix-ui/colors/amber-dark.css"; +@import "@radix-ui/colors/amber.css"; + +.TournamentRegistrationsTable { + --cell-padding-y: 1rem; + --table-padding: var(--container-padding-x); + + &_CaptainIcon { + width: 1.5rem; + height: 1.5rem; + stroke-width: 1.5px; + } +} diff --git a/src/pages/TournamentCompetitorDetailPage/components/TournamentRegistrationsTable/TournamentRegistrationsTable.tsx b/src/pages/TournamentCompetitorDetailPage/components/TournamentRegistrationsTable/TournamentRegistrationsTable.tsx new file mode 100644 index 00000000..741e4b2c --- /dev/null +++ b/src/pages/TournamentCompetitorDetailPage/components/TournamentRegistrationsTable/TournamentRegistrationsTable.tsx @@ -0,0 +1,76 @@ +import { ColumnDef, Table } from '@ianpaschal/combat-command-components'; +import clsx from 'clsx'; +import { ShieldUser } from 'lucide-react'; + +import { TournamentRegistration } from '~/api'; +import { InfoPopover } from '~/components/generic/InfoPopover'; +import { IdentityBadge } from '~/components/IdentityBadge'; +import { useTournamentCompetitor } from '~/components/TournamentCompetitorProvider'; +import { useTournament } from '~/components/TournamentProvider'; +import { TournamentRegistrationActiveToggle, TournamentRegistrationContextMenu } from '~/components/TournamentRegistrationProvider'; + +import styles from './TournamentRegistrationsTable.module.scss'; + +export interface TournamentRegistrationsTableProps { + className?: string; + registrations: TournamentRegistration[]; +} + +export const TournamentRegistrationsTable = ({ + className, + registrations, +}: TournamentRegistrationsTableProps): JSX.Element => { + const tournament = useTournament(); + const tournamentCompetitor = useTournamentCompetitor(); + return ( +
( + + ), + }, + { + key: 'identity', + label: 'Player', + width: 'auto', + xAlign: 'left', + renderCell: ({ user }) => ( + + ), + }, + ...(tournament.useTeams ? [ + { + key: 'isCaptain', + width: '1fr', + renderCell: ({ user }) => user._id === tournamentCompetitor.captainUserId ? ( + + + + ) : null, + } as ColumnDef, + ] : []), + // { + // key: 'lists', + // label: 'List', + // renderCell: (row) => { + // const isCaptain = user && (tournamentCompetitors ?? []).find((c) => c._id === row.tournamentCompetitorId)?.registrations.find((r) => r.userId === user._id); + // return ( + // + // ); + // }, + // }, + { + key: 'actions', + renderCell: (r) => ( + + ), + }, + ]} + /> + ); +}; diff --git a/src/pages/TournamentCompetitorDetailPage/components/TournamentRegistrationsTable/index.ts b/src/pages/TournamentCompetitorDetailPage/components/TournamentRegistrationsTable/index.ts new file mode 100644 index 00000000..6ec12613 --- /dev/null +++ b/src/pages/TournamentCompetitorDetailPage/components/TournamentRegistrationsTable/index.ts @@ -0,0 +1,2 @@ +export type { TournamentRegistrationsTableProps } from './TournamentRegistrationsTable'; +export { TournamentRegistrationsTable } from './TournamentRegistrationsTable'; diff --git a/src/pages/TournamentCompetitorDetailPage/index.ts b/src/pages/TournamentCompetitorDetailPage/index.ts new file mode 100644 index 00000000..ab9d7bab --- /dev/null +++ b/src/pages/TournamentCompetitorDetailPage/index.ts @@ -0,0 +1,2 @@ +export type { TournamentCompetitorDetailPageProps } from './TournamentCompetitorDetailPage'; +export { TournamentCompetitorDetailPage } from './TournamentCompetitorDetailPage'; diff --git a/src/pages/TournamentDetailPage/TournamentDetailPage.tsx b/src/pages/TournamentDetailPage/TournamentDetailPage.tsx index 4b001503..dbc38695 100644 --- a/src/pages/TournamentDetailPage/TournamentDetailPage.tsx +++ b/src/pages/TournamentDetailPage/TournamentDetailPage.tsx @@ -17,10 +17,9 @@ import { } from '~/components/generic/Tabs'; import { NotFoundView } from '~/components/NotFoundView'; import { PageWrapper } from '~/components/PageWrapper'; -import { TournamentActionsProvider } from '~/components/TournamentActionsProvider'; import { TournamentBanner } from '~/components/TournamentBanner'; import { TournamentCompetitorsProvider } from '~/components/TournamentCompetitorsProvider'; -import { TournamentContextMenu } from '~/components/TournamentContextMenu'; +import { TournamentContextMenu } from '~/components/TournamentProvider'; import { TournamentProvider } from '~/components/TournamentProvider'; import { DeviceSize, useDeviceSize } from '~/hooks/useDeviceSize'; import { useGetTournamentCompetitorsByTournament } from '~/services/tournamentCompetitors'; @@ -109,49 +108,47 @@ export const TournamentDetailPage = (): JSX.Element => { return ( - - - } - title={tournament.title} - hideTitle - > -
- {showInfoSidebar && ( -
- -
- )} - -
- {tabs.length > 1 && ( - - )} - -
- - - - - - - - - - - - - - - - - - -
-
-
-
-
+ + } + title={tournament.title} + hideTitle + > +
+ {showInfoSidebar && ( +
+ +
+ )} + +
+ {tabs.length > 1 && ( + + )} + +
+ + + + + + + + + + + + + + + + + + +
+
+
+
); }; diff --git a/src/pages/TournamentDetailPage/components/TournamentExportDataDialog/TournamentExportDataDialog.tsx b/src/pages/TournamentDetailPage/components/TournamentExportDataDialog/TournamentExportDataDialog.tsx index bfb38f52..b533210a 100644 --- a/src/pages/TournamentDetailPage/components/TournamentExportDataDialog/TournamentExportDataDialog.tsx +++ b/src/pages/TournamentDetailPage/components/TournamentExportDataDialog/TournamentExportDataDialog.tsx @@ -9,7 +9,6 @@ import { } from '~/components/generic/Dialog'; import { useTournament } from '~/components/TournamentProvider'; import { useExportFowV4TournamentMatchData } from '~/services/tournaments'; -import { getTournamentDisplayName } from '~/utils/common/getTournamentDisplayName'; import { useTournamentExportDataDialog } from './TournamentExportDataDialog.hooks'; import styles from './TournamentExportDataDialog.module.scss'; @@ -37,7 +36,7 @@ export const TournamentExportDataDialog = (): JSX.Element => { const blob = await response.blob(); const url = window.URL.createObjectURL(blob); - const fileName = `${getTournamentDisplayName(tournament).toLowerCase().replace(' ', '_')}_${new Date().toISOString()}.csv`; + const fileName = `${tournament.displayName.toLowerCase().replace(' ', '_')}_${new Date().toISOString()}.csv`; const a = document.createElement('a'); a.href = url; diff --git a/src/pages/TournamentDetailPage/components/TournamentPairingsCard/TournamentPairingsCard.module.scss b/src/pages/TournamentDetailPage/components/TournamentPairingsCard/TournamentPairingsCard.module.scss index 0fbcc6ae..dbaf764d 100644 --- a/src/pages/TournamentDetailPage/components/TournamentPairingsCard/TournamentPairingsCard.module.scss +++ b/src/pages/TournamentDetailPage/components/TournamentPairingsCard/TournamentPairingsCard.module.scss @@ -14,14 +14,14 @@ @include flex.centered; @include text.ui($muted: true); - padding-bottom: 4rem; + padding: 4rem; } - &_Row { - padding: 0 var(--container-padding-x); + &_Table { + --table-padding: var(--container-padding-x); } - &_Table { + &_TableNumber { @include text.large; } diff --git a/src/pages/TournamentDetailPage/components/TournamentPairingsCard/TournamentPairingsCard.tsx b/src/pages/TournamentDetailPage/components/TournamentPairingsCard/TournamentPairingsCard.tsx index 19418769..660c8ec2 100644 --- a/src/pages/TournamentDetailPage/components/TournamentPairingsCard/TournamentPairingsCard.tsx +++ b/src/pages/TournamentDetailPage/components/TournamentPairingsCard/TournamentPairingsCard.tsx @@ -1,13 +1,13 @@ import { ReactElement, useState } from 'react'; import { useNavigate } from 'react-router-dom'; +import { Table } from '@ianpaschal/combat-command-components'; import clsx from 'clsx'; import { Zap } from 'lucide-react'; import { EmptyState } from '~/components/EmptyState'; import { Button } from '~/components/generic/Button'; import { InputSelect } from '~/components/generic/InputSelect'; -import { Table } from '~/components/generic/Table'; -import { useTournamentActions } from '~/components/TournamentActionsProvider/TournamentActionsProvider.hooks'; +import { useConfigureRoundAction } from '~/components/TournamentProvider'; import { useTournament } from '~/components/TournamentProvider'; import { useGetTournamentPairings } from '~/services/tournamentPairings'; import { TournamentDetailCard } from '../TournamentDetailCard'; @@ -23,16 +23,16 @@ export const TournamentPairingsCard = ({ className, }: TournamentPairingsCardProps): JSX.Element => { const navigate = useNavigate(); - const { _id: tournamentId, lastRound, roundCount } = useTournament(); - const actions = useTournamentActions(); + const tournament = useTournament(); + const configureRoundAction = useConfigureRoundAction(tournament); - const roundIndexes = lastRound !== undefined ? Array.from({ - length: Math.min(lastRound + 2, roundCount), + const roundIndexes = tournament.lastRound !== undefined ? Array.from({ + length: Math.min(tournament.lastRound + 2, tournament.roundCount), }, (_, i) => i) : [0]; const [round, setRound] = useState(roundIndexes.length - 1); const { data: tournamentPairings, loading } = useGetTournamentPairings({ - tournamentId, + tournamentId: tournament._id, round, }); @@ -70,15 +70,15 @@ export const TournamentPairingsCard = ({ ) : ( showEmptyState ? ( }> - {actions?.configureRound && ( + {configureRoundAction && (
+
) )} diff --git a/src/pages/TournamentDetailPage/components/TournamentPairingsCard/TournamentPairingsCard.utils.tsx b/src/pages/TournamentDetailPage/components/TournamentPairingsCard/TournamentPairingsCard.utils.tsx index 4458b5cb..d5bca9fe 100644 --- a/src/pages/TournamentDetailPage/components/TournamentPairingsCard/TournamentPairingsCard.utils.tsx +++ b/src/pages/TournamentDetailPage/components/TournamentPairingsCard/TournamentPairingsCard.utils.tsx @@ -1,26 +1,23 @@ import { generatePath, NavigateFunction } from 'react-router-dom'; +import { ColumnDef } from '@ianpaschal/combat-command-components'; import { ChevronRight, Swords } from 'lucide-react'; import { TournamentPairing } from '~/api'; import { Button } from '~/components/generic/Button'; import { CircularProgress } from '~/components/generic/CircularProgress'; import { InfoPopover } from '~/components/generic/InfoPopover'; -import { ColumnDef } from '~/components/generic/Table'; import { TournamentPairingRow } from '~/components/TournamentPairingRow'; import { PATHS } from '~/settings'; import styles from './TournamentPairingsCard.module.scss'; -const matchIndicatorSize = 40; // 2.5rem - export const getTournamentPairingTableConfig = (navigate: NavigateFunction): ColumnDef[] => [ { key: 'table', label: 'Table', - width: 40, - align: 'center', + xAlign: 'center', renderCell: (r) => ( -
+
{r.table === null ? '-' : r.table + 1}
), @@ -28,7 +25,8 @@ export const getTournamentPairingTableConfig = (navigate: NavigateFunction): Col { key: 'pairing', label: 'Pairing', - align: 'center', + xAlign: 'center', + width: '1fr', renderCell: (r) => ( ( - + {submitted} ), @@ -53,8 +50,7 @@ export const getTournamentPairingTableConfig = (navigate: NavigateFunction): Col }, { key: 'viewPairing', - width: matchIndicatorSize, - align: 'center', + xAlign: 'center', renderCell: ({ _id }) => (
+
) )} diff --git a/src/pages/TournamentDetailPage/components/TournamentRankingsCard/TournamentRankingsCard.utils.tsx b/src/pages/TournamentDetailPage/components/TournamentRankingsCard/TournamentRankingsCard.utils.tsx index d5ddfcf9..2a6873e1 100644 --- a/src/pages/TournamentDetailPage/components/TournamentRankingsCard/TournamentRankingsCard.utils.tsx +++ b/src/pages/TournamentDetailPage/components/TournamentRankingsCard/TournamentRankingsCard.utils.tsx @@ -1,3 +1,4 @@ +import { ColumnDef } from '@ianpaschal/combat-command-components'; import { getRankingFactorDisplayName } from '@ianpaschal/combat-command-game-systems/flamesOfWarV4'; import { @@ -9,7 +10,6 @@ import { TournamentRegistrationId, } from '~/api'; import { InfoPopover } from '~/components/generic/InfoPopover'; -import { ColumnDef } from '~/components/generic/Table'; import { IdentityBadge } from '~/components/IdentityBadge'; import styles from './TournamentRankingsCard.module.scss'; @@ -33,12 +33,16 @@ export const getTournamentRankingTableConfig = ( { key: 'rank', label: 'Rank', - width: 40, - align: 'center', - renderCell: (r) =>
{r.rank + 1}
, + xAlign: 'center', + renderCell: (r) => ( +
+ {r.rank + 1} +
+ ), }, { key: 'identity', + width: '1fr', label: config.view === 'competitors' ? (config.tournament.useTeams ? 'Team' : 'Player') : 'Player', renderCell: (r) => { const competitor = config.competitors.find((c) => c._id === r.id); @@ -67,7 +71,7 @@ export const getTournamentRankingTableConfig = ( ...config.tournament.rankingFactors.map((key): ColumnDef => ({ key, width: 40, - align: 'center', + xAlign: 'center', renderCell: (r) => r.rankingFactors[key], renderHeader: () => { const long = getRankingFactorDisplayName(key); diff --git a/src/pages/TournamentDetailPage/components/TournamentRosterCard/TournamentRosterCard.module.scss b/src/pages/TournamentDetailPage/components/TournamentRosterCard/TournamentRosterCard.module.scss index d0786f3c..b1a72389 100644 --- a/src/pages/TournamentDetailPage/components/TournamentRosterCard/TournamentRosterCard.module.scss +++ b/src/pages/TournamentDetailPage/components/TournamentRosterCard/TournamentRosterCard.module.scss @@ -2,15 +2,29 @@ @use "/src/style/variables"; @use "/src/style/text"; -.TournamentRoster { +.TournamentRosterCard { + padding-bottom: var(--container-padding-y); + + &_Filters { + @include flex.row; + } + &_EmptyState { - @include flex.centered; @include flex.stretchy; + @include flex.centered; + @include text.ui($muted: true); + + padding: 4rem; + } + + &_Table { + --table-padding: var(--container-padding-x); + --cell-padding-y: 1rem; } - &_CompetitorList { - @include flex.column($gap: 0.5rem); + &_ReadyIndicatorIcon { + @include text.ui; - padding: 1rem var(--container-padding-x); + height: 1.5rem; } } diff --git a/src/pages/TournamentDetailPage/components/TournamentRosterCard/TournamentRosterCard.tsx b/src/pages/TournamentDetailPage/components/TournamentRosterCard/TournamentRosterCard.tsx index 49925d3c..30fa2463 100644 --- a/src/pages/TournamentDetailPage/components/TournamentRosterCard/TournamentRosterCard.tsx +++ b/src/pages/TournamentDetailPage/components/TournamentRosterCard/TournamentRosterCard.tsx @@ -1,28 +1,25 @@ import { ReactElement } from 'react'; -import { generatePath } from 'react-router-dom'; +import { generatePath, useNavigate } from 'react-router-dom'; +import { Table } from '@ianpaschal/combat-command-components'; import clsx from 'clsx'; import { EyeOff, Link, - UserPlus, Users, } from 'lucide-react'; -import { VisibilityLevel } from '~/api'; +import { TournamentActionKey } from '~/api'; import { useAuth } from '~/components/AuthProvider'; import { EmptyState } from '~/components/EmptyState'; import { Button } from '~/components/generic/Button'; import { toast } from '~/components/ToastProvider'; -import { TournamentCompetitorEditDialog, useTournamentCompetitorEditDialog } from '~/components/TournamentCompetitorEditDialog'; -import { useTournamentCompetitors } from '~/components/TournamentCompetitorsProvider'; -import { useTournament } from '~/components/TournamentProvider'; -import { TournamentRoster } from '~/components/TournamentRoster'; -import { useCreateTournamentCompetitor, useGetTournamentCompetitorsByTournament } from '~/services/tournamentCompetitors'; +import { useActions, useTournament } from '~/components/TournamentProvider'; +import { useGetTournamentCompetitorsByTournament } from '~/services/tournamentCompetitors'; import { usePublishTournament } from '~/services/tournaments'; import { PATHS } from '~/settings'; import { isUserTournamentOrganizer } from '~/utils/common/isUserTournamentOrganizer'; -import { ConfirmRegisterDialog, useConfirmRegisterDialog } from '../ConfirmRegisterDialog'; import { TournamentDetailCard } from '../TournamentDetailCard'; +import { getTournamentCompetitorTableConfig } from './TournamentRosterCard.utils'; import styles from './TournamentRosterCard.module.scss'; @@ -33,43 +30,36 @@ export interface TournamentRosterCardProps { export const TournamentRosterCard = ({ className, }: TournamentRosterCardProps): JSX.Element => { + const navigate = useNavigate(); const user = useAuth(); const tournament = useTournament(); - const competitors = useTournamentCompetitors(); - const { open: openConfirmNameVisibilityDialog } = useConfirmRegisterDialog(); - const { open: openCreateDialog } = useTournamentCompetitorEditDialog(); + const actions = useActions(tournament); + const { data: tournamentCompetitors, loading } = useGetTournamentCompetitorsByTournament({ tournamentId: tournament._id }); - const { mutation: createTournamentCompetitor } = useCreateTournamentCompetitor({ - successMessage: `Successfully joined ${tournament.title}!`, - }); + const { mutation: publishTournament } = usePublishTournament({ successMessage: `${tournament.title} is now live!`, }); + const columns = getTournamentCompetitorTableConfig(navigate, tournament); + const rows = (tournamentCompetitors || []); const showLoadingState = loading; const showEmptyState = !loading && !tournamentCompetitors?.length; const isOrganizer = isUserTournamentOrganizer(user, tournament); - const handleRegister = (): void => { - if (tournament.requireRealNames && user && user.nameVisibility < VisibilityLevel.Tournaments) { - openConfirmNameVisibilityDialog({ - onConfirm: handleConfirmRegister, - }); - } else { - handleConfirmRegister(); - } - }; - - const handleConfirmRegister = (): void => { - if (!user) { - return; - } - createTournamentCompetitor({ - tournamentId: tournament._id, - captainUserId: user._id, - }); - }; + const getControls = (): ReactElement[] => [ + TournamentActionKey.AddPlayer, + TournamentActionKey.Join, + TournamentActionKey.Leave, + ].reduce((acc, key) => actions[key] ? [...acc, ( +
) )} - - ); }; diff --git a/src/pages/TournamentDetailPage/components/TournamentRosterCard/TournamentRosterCard.utils.tsx b/src/pages/TournamentDetailPage/components/TournamentRosterCard/TournamentRosterCard.utils.tsx new file mode 100644 index 00000000..face8d48 --- /dev/null +++ b/src/pages/TournamentDetailPage/components/TournamentRosterCard/TournamentRosterCard.utils.tsx @@ -0,0 +1,59 @@ +import { generatePath, NavigateFunction } from 'react-router-dom'; +import { ColumnDef } from '@ianpaschal/combat-command-components'; +import { ChevronRight } from 'lucide-react'; + +import { Tournament, TournamentCompetitor } from '~/api'; +import { Button } from '~/components/generic/Button'; +import { IdentityBadge } from '~/components/IdentityBadge'; +import { TournamentCompetitorActiveToggle, TournamentCompetitorPlayerCount } from '~/components/TournamentCompetitorProvider'; +import { PATHS } from '~/settings'; + +export const getTournamentCompetitorTableConfig = ( + navigate: NavigateFunction, + tournament: Tournament, +): ColumnDef[] => ([ + { + key: 'identity', + label: tournament.useTeams ? 'Team' : 'Player', + width: '1fr', + xAlign: 'left', + renderCell: (r) => ( + + ), + }, + ...(tournament.useTeams ? [ + { + key: 'players', + label: 'Players', + xAlign: 'left', + renderCell: (r) => ( + + ), + } as ColumnDef, + ] : []), + ...(tournament.status === 'active' ? [ + { + key: 'active', + label: 'Ready', + xAlign: 'center', + renderCell: (r) => ( + + ), + } as ColumnDef, + ] : []), + { + key: 'viewDetails', + xAlign: 'center', + renderCell: ({ _id: tournamentCompetitorId, tournamentId }) => ( +
+
Once created, pairings cannot be edited. Please ensure all competitors are present and ready to play! diff --git a/src/pages/TournamentPairingsPage/components/ConfirmPairingsDialog/ConfirmPairingsDialog.utils.tsx b/src/pages/TournamentPairingsPage/components/ConfirmPairingsDialog/ConfirmPairingsDialog.utils.tsx index 734fbd7d..089d3fa3 100644 --- a/src/pages/TournamentPairingsPage/components/ConfirmPairingsDialog/ConfirmPairingsDialog.utils.tsx +++ b/src/pages/TournamentPairingsPage/components/ConfirmPairingsDialog/ConfirmPairingsDialog.utils.tsx @@ -1,5 +1,6 @@ +import { ColumnDef } from '@ianpaschal/combat-command-components'; + import { DraftTournamentPairing, TournamentCompetitor } from '~/api'; -import { ColumnDef } from '~/components/generic/Table'; import { TournamentPairingRow } from '~/components/TournamentPairingRow'; import { TournamentPairingFormItem } from '../../TournamentPairingsPage.schema'; @@ -9,10 +10,10 @@ export const getTableColumns = (competitors: TournamentCompetitor[]): ColumnDef< { key: 'table', label: 'Table', - width: 40, - align: 'center', + width: 'auto', + xAlign: 'center', renderCell: (r) => ( -
+
{r.table === null ? '-' : r.table + 1}
), @@ -20,7 +21,8 @@ export const getTableColumns = (competitors: TournamentCompetitor[]): ColumnDef< { key: 'pairing', label: 'Pairing', - align: 'center', + width: '1fr', + xAlign: 'center', renderCell: (r) => { const tournamentCompetitor0 = competitors.find((c) => c._id === r.tournamentCompetitor0Id) ?? null; const tournamentCompetitor1 = competitors.find((c) => c._id === r.tournamentCompetitor1Id) ?? null; @@ -28,14 +30,11 @@ export const getTableColumns = (competitors: TournamentCompetitor[]): ColumnDef< return null; } return ( - + ); }, }, diff --git a/src/pages/UserProfilePage/components/UserTournamentItem/UserTournamentItem.tsx b/src/pages/UserProfilePage/components/UserTournamentItem/UserTournamentItem.tsx index 72089bc3..237f4153 100644 --- a/src/pages/UserProfilePage/components/UserTournamentItem/UserTournamentItem.tsx +++ b/src/pages/UserProfilePage/components/UserTournamentItem/UserTournamentItem.tsx @@ -1,7 +1,5 @@ import { Tournament, UserId } from '~/api'; import { useGetTournamentCompetitorsByTournament } from '~/services/tournamentCompetitors'; -// import { useGetTournamentRankings } from '~/services/tournaments'; -import { getTournamentCompetitorDisplayName } from '~/utils/common/getTournamentCompetitorDisplayName'; import styles from './UserTournamentItem.module.scss'; @@ -28,8 +26,8 @@ export const UserTournamentItem = ({

{tournament.title}

- {tournament.useTeams && ( -

{getTournamentCompetitorDisplayName(competitor)}

+ {tournament.useTeams && competitor && ( +

{competitor.displayName}

)} ); diff --git a/src/pages/UserProfilePage/components/UserTournamentsCard/UserTournamentsCard.tsx b/src/pages/UserProfilePage/components/UserTournamentsCard/UserTournamentsCard.tsx index 57cd9898..324fa720 100644 --- a/src/pages/UserProfilePage/components/UserTournamentsCard/UserTournamentsCard.tsx +++ b/src/pages/UserProfilePage/components/UserTournamentsCard/UserTournamentsCard.tsx @@ -36,7 +36,7 @@ export const UserTournamentsCard = ({ disabled={status === 'LoadingMore'} icon={} text="Load More" - onClick={loadMore} + onClick={() => loadMore()} /> )} diff --git a/src/routes.tsx b/src/routes.tsx index fbefcfe4..4c00fdeb 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -29,6 +29,7 @@ import { UserPreferencesForm, UserProfileForm, } from '~/pages/SettingsPage'; +import { TournamentCompetitorDetailPage } from '~/pages/TournamentCompetitorDetailPage'; import { TournamentCreatePage } from '~/pages/TournamentCreatePage'; import { TournamentDetailPage } from '~/pages/TournamentDetailPage'; import { TournamentEditPage } from '~/pages/TournamentEditPage/TournamentEditPage'; @@ -168,6 +169,11 @@ export const routes = [ visibility: [], element: , }, + { + path: PATHS.tournamentCompetitorDetails, + visibility: [], + element: , + }, { path: PATHS.userProfile, visibility: [], diff --git a/src/services/tournamentRegistrations.ts b/src/services/tournamentRegistrations.ts index 7e1ef0a3..41f5cfb2 100644 --- a/src/services/tournamentRegistrations.ts +++ b/src/services/tournamentRegistrations.ts @@ -2,6 +2,7 @@ import { api } from '~/api'; import { createMutationHook, createQueryHook } from '~/services/utils'; // Special Queries +export const useGetTournamentRegistrationByTournamentUser = createQueryHook(api.tournamentRegistrations.getTournamentRegistrationByTournamentUser); export const useGetTournamentRegistrationsByCompetitor = createQueryHook(api.tournamentRegistrations.getTournamentRegistrationsByCompetitor); export const useGetTournamentRegistrationsByTournament = createQueryHook(api.tournamentRegistrations.getTournamentRegistrationsByTournament); export const useGetTournamentRegistrationsByUser = createQueryHook(api.tournamentRegistrations.getTournamentRegistrationsByUser); diff --git a/src/services/tournaments.ts b/src/services/tournaments.ts index 4558c889..ca812eb8 100644 --- a/src/services/tournaments.ts +++ b/src/services/tournaments.ts @@ -7,7 +7,6 @@ export const useGetTournament = createQueryHook(api.tournaments.getTournament); export const useGetTournaments = createQueryHook(api.tournaments.getTournaments); // Special Queries -export const useGetAvailableTournamentActions = createQueryHook(api.tournaments.getAvailableTournamentActions); export const useGetTournamentByTournamentPairing = createQueryHook(api.tournaments.getTournamentByTournamentPairing); export const useGetTournamentOpenRound = createQueryHook(api.tournaments.getTournamentOpenRound); export type TournamentOpenRound = typeof api.tournaments.getTournamentOpenRound._returnType; // TODO: Move to back-end diff --git a/src/services/utils/createMutationHook.ts b/src/services/utils/createMutationHook.ts index 231401c9..92e77cbd 100644 --- a/src/services/utils/createMutationHook.ts +++ b/src/services/utils/createMutationHook.ts @@ -16,7 +16,7 @@ export const createMutationHook = (mutationFn: T) => (conf const handler = useMutation(mutationFn); const [loading, setIsLoading] = useState(false); return { - mutation: async (args: T['_args']) => { + mutation: async (args: T['_args'], instanceConfig?: MutationHookConfig) => { setIsLoading(true); try { const response = await handler(args); @@ -26,16 +26,21 @@ export const createMutationHook = (mutationFn: T) => (conf if (config?.onSuccess) { config.onSuccess(response, args); } + if (instanceConfig?.onSuccess) { + instanceConfig.onSuccess(response, args); + } } catch (error) { if (error instanceof ConvexError) { toast.error('Error', { description: error.data.message }); - if (config?.onError) { - config.onError(error); - } - } - if (error instanceof Error) { + } else if (error instanceof Error) { toast.error('Error', { description: error.message as string }); } + if (config?.onError) { + config.onError(error); + } + if (instanceConfig?.onError) { + instanceConfig.onError(error); + } } setIsLoading(false); }, diff --git a/src/services/utils/createPaginatedQueryHook.ts b/src/services/utils/createPaginatedQueryHook.ts index 6998f81b..721eb537 100644 --- a/src/services/utils/createPaginatedQueryHook.ts +++ b/src/services/utils/createPaginatedQueryHook.ts @@ -1,5 +1,5 @@ import { useRef } from 'react'; -import { usePaginatedQuery } from 'convex/react'; +import { PaginatedQueryItem, usePaginatedQuery } from 'convex/react'; import { BetterOmit, Expand, @@ -9,13 +9,31 @@ import { import { DEFAULT_PAGE_SIZE } from '~/settings'; -type QueryFn = FunctionReference<'query'>; +export type QueryFn = FunctionReference<'query'>; -export const createPaginatedQueryHook = (queryFn: T) => { - function isArgs(args: unknown): args is 'skip' | Expand, 'paginationOpts'>> { +export type PaginatedQueryArgs = Omit | 'skip'; + +export type PaginatedQueryHookResult = { + data?: PaginatedQueryItem[]; + loading: boolean; + loadMore: () => void; + status: 'LoadingFirstPage' | 'CanLoadMore' | 'LoadingMore' | 'Exhausted' | null; +}; + +export const createPaginatedQueryHook = ( + queryFn: T, +): (args: PaginatedQueryArgs, pageSize?: number) => PaginatedQueryHookResult => { + function isArgs(args: unknown): args is Expand, 'paginationOpts'>> { return args !== 'skip' && args !== undefined && args !== null; } return (args: Omit | 'skip', pageSize = DEFAULT_PAGE_SIZE) => { + const { results: data, isLoading, loadMore, status } = usePaginatedQuery(queryFn, isArgs(args) ? args : 'skip', { + initialNumItems: pageSize, + }); + const stored = useRef(data); + if (data !== undefined) { + stored.current = data; + } if (!isArgs(args)) { return { data: undefined, @@ -24,13 +42,6 @@ export const createPaginatedQueryHook = (queryFn: T) => { status: null, }; } - const { results: data, isLoading, loadMore, status } = usePaginatedQuery(queryFn, args, { - initialNumItems: pageSize, - }); - const stored = useRef(data); - if (data !== undefined) { - stored.current = data; - } return { data: stored.current, loading: isLoading || stored.current === undefined, diff --git a/src/settings.ts b/src/settings.ts index 662a70a7..87f65c73 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -21,11 +21,19 @@ export const PATHS = { leagues: '/leagues', matchResultDetails: '/matches/:id', matchResults: '/matches', + + // Tournament Competitors + tournamentCompetitorDetails: '/tournaments/:tournamentId/competitors/:tournamentCompetitorId', + + // Tournament Pairings + tournamentPairingDetails: '/pairings/:id', + tournamentPairings: '/tournaments/:id/pairings', + + // Tournaments tournamentCreate: '/tournaments/create', tournamentDetails: '/tournaments/:id', tournamentEdit: '/tournaments/:id/edit', - tournamentPairingDetails: '/pairings/:id', - tournamentPairings: '/tournaments/:id/pairings', tournaments: '/tournaments', + userProfile: '/users/:id', } as const; diff --git a/src/style/_borders.scss b/src/style/_borders.scss index 94a18180..a38f514a 100644 --- a/src/style/_borders.scss +++ b/src/style/_borders.scss @@ -75,3 +75,7 @@ border-style: dashed; border-width: var(--border-width); } + +@mixin outlined { + outline: 4px solid var(--card-bg); +} diff --git a/src/style/_text.scss b/src/style/_text.scss index c607fa6a..6c128fe5 100644 --- a/src/style/_text.scss +++ b/src/style/_text.scss @@ -66,7 +66,10 @@ @mixin single-line { overflow: hidden; + min-width: 0; + max-width: 100%; + text-overflow: ellipsis; white-space: nowrap; } diff --git a/src/style/_variants.scss b/src/style/_variants.scss index a18a38e3..88ebb32a 100644 --- a/src/style/_variants.scss +++ b/src/style/_variants.scss @@ -200,6 +200,7 @@ @include corners.normal; @include shadows.surface; + overflow: hidden; background-color: var(--card-bg); @if $elevated { diff --git a/src/utils/common/getPathWithQuery.ts b/src/utils/common/getPathWithQuery.ts new file mode 100644 index 00000000..e8c50dc6 --- /dev/null +++ b/src/utils/common/getPathWithQuery.ts @@ -0,0 +1,8 @@ +import qs from 'qs'; + +export const getPathWithQuery = (basePath: string, params: unknown): string => { + const queryString = qs.stringify(params, { + arrayFormat: 'comma', + }); + return `${basePath}?${queryString}`; +}; diff --git a/src/utils/common/getTournamentCompetitorDisplayName.ts b/src/utils/common/getTournamentCompetitorDisplayName.ts deleted file mode 100644 index 2f1d5475..00000000 --- a/src/utils/common/getTournamentCompetitorDisplayName.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { TournamentCompetitor } from '~/api'; -import { getCountryName } from '~/utils/common/getCountryName'; - -/** - * Gets the display name for a tournament competitor. - * - * @param competitor - A (deep) tournament competitor. - * @returns - A display name string. - */ -export const getTournamentCompetitorDisplayName = ( - competitor?: TournamentCompetitor | null, -): string => { - - if (!competitor) { - return ''; - } - - // If competitor has only 1 player, just use the player's name: - if (competitor.registrations.length === 1 && competitor.registrations[0].user) { - return competitor.registrations[0].user.displayName; - } - - // Use the country name if there is one, otherwise just use the team name: - if (competitor.teamName) { - const countryName = getCountryName(competitor.teamName); - return countryName ?? competitor.teamName; - } - - // Fallback: - return 'Unknown Competitor'; -}; diff --git a/src/utils/common/getTournamentPairingDisplayName.ts b/src/utils/common/getTournamentPairingDisplayName.ts index e218f38a..ce88f54b 100644 --- a/src/utils/common/getTournamentPairingDisplayName.ts +++ b/src/utils/common/getTournamentPairingDisplayName.ts @@ -1,5 +1,4 @@ import { TournamentPairing } from '~/api'; -import { getTournamentCompetitorDisplayName } from '~/utils/common/getTournamentCompetitorDisplayName'; /** * Gets the display name for a tournament pairing. @@ -14,7 +13,5 @@ export const getTournamentPairingDisplayName = ( tournamentCompetitor0, tournamentCompetitor1, } = tournamentPairing; - const displayName0 = getTournamentCompetitorDisplayName(tournamentCompetitor0); - const displayName1 = tournamentCompetitor1 ? getTournamentCompetitorDisplayName(tournamentCompetitor1) : 'Bye'; - return `${displayName0} vs. ${displayName1}`; + return `${tournamentCompetitor0.displayName} vs. ${tournamentCompetitor1?.displayName ?? 'Bye'}`; }; diff --git a/src/utils/common/isUserTournamentCompetitorCaptain.ts b/src/utils/common/isUserTournamentCompetitorCaptain.ts new file mode 100644 index 00000000..4764e002 --- /dev/null +++ b/src/utils/common/isUserTournamentCompetitorCaptain.ts @@ -0,0 +1,11 @@ +import { TournamentCompetitor, User } from '~/api'; + +export const isUserTournamentCompetitorCaptain = ( + user: User | null, + tournamentCompetitor?: TournamentCompetitor | null, +): boolean => { + if (!user || !tournamentCompetitor) { + return false; + } + return user?._id && tournamentCompetitor.captainUserId === user._id; +}; diff --git a/src/utils/validateForm.ts b/src/utils/validateForm.ts index ef9f757b..102681c0 100644 --- a/src/utils/validateForm.ts +++ b/src/utils/validateForm.ts @@ -7,15 +7,15 @@ import { ZodTypeAny } from 'zod'; import { toast } from '~/components/ToastProvider'; -export const validateForm = ( +export const validateForm = ( schema: T, formData: unknown, - setError: UseFormSetError, + setError: UseFormSetError, ): ReturnType | void => { const result = schema.safeParse(formData); if (!result.success) { result.error.issues.forEach((issue) => { - const fieldPath = issue.path.join('.') as Path; + const fieldPath = issue.path.join('.') as Path; setError(fieldPath, { message: issue.message }); toast.error('Error', { description: issue.message }); }); From 3539694737b0484c6efceaa171d31a5a50d261fb Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Tue, 13 Jan 2026 14:33:26 +0100 Subject: [PATCH 2/7] WIP --- .../_helpers/getAvailableActions.ts | 30 +++++-- .../_helpers/getAvailableActions.ts | 25 +++++- .../_helpers/aggregateTournamentData.ts | 1 + .../_helpers/getAvailableActions.ts | 87 ++++++++++++++----- package-lock.json | 28 +++--- package.json | 2 +- 6 files changed, 124 insertions(+), 49 deletions(-) diff --git a/convex/_model/tournamentCompetitors/_helpers/getAvailableActions.ts b/convex/_model/tournamentCompetitors/_helpers/getAvailableActions.ts index 66f0997f..f72cbf56 100644 --- a/convex/_model/tournamentCompetitors/_helpers/getAvailableActions.ts +++ b/convex/_model/tournamentCompetitors/_helpers/getAvailableActions.ts @@ -7,13 +7,31 @@ import { getTournamentRegistrationsByCompetitor } from '../../tournamentRegistra import { checkUserIsRegistered } from '../../tournamentRegistrations/_helpers/checkUserIsRegistered'; export enum TournamentCompetitorActionKey { - AddPlayer = 'addPlayer', // Create TournamentRegistration as TO - Delete = 'delete', - Edit = 'edit', - Join = 'join', // Create TournamentRegistration as player - Leave = 'leave', // Delete TournamentRegistration as player - // TransferPlayers = 'transferPlayers', + // ---- TO Actions ---- + + /** Create a TournamentRegistration for a given TournamentCompetitor. */ + AddPlayer = 'addPlayer', + + /** Set a TournamentCompetitor's 'active' field to true or false. */ ToggleActive = 'toggleActive', + + // TODO + // TransferPlayers = 'transferPlayers', + + // ---- TO or Captain Actions ---- + + /** Edit a TournamentCompetitor. */ + Edit = 'edit', + + /** Delete a TournamentCompetitor. */ + Delete = 'delete', + + // ---- Player Actions ---- + /** Create own TournamentRegistration. */ + Join = 'join', + + /** Delete own TournamentRegistration. */ + Leave = 'leave', } /** diff --git a/convex/_model/tournamentRegistrations/_helpers/getAvailableActions.ts b/convex/_model/tournamentRegistrations/_helpers/getAvailableActions.ts index 5ebb4972..0cc413a7 100644 --- a/convex/_model/tournamentRegistrations/_helpers/getAvailableActions.ts +++ b/convex/_model/tournamentRegistrations/_helpers/getAvailableActions.ts @@ -5,13 +5,30 @@ import { QueryCtx } from '../../../_generated/server'; import { checkUserIsTournamentOrganizer } from '../../tournamentOrganizers'; export enum TournamentRegistrationActionKey { - // ApproveList = 'approveList', + // ---- TO Actions ---- + + /** Delete a TournamentRegistration. */ Delete = 'delete', - Leave = 'leave', + + // TODO + // ApproveList = 'approveList', + + // TODO // RejectList = 'rejectList', - // SubmitList = 'submitList', - ToggleActive = 'toggleActive', + + // TODO // Transfer = 'transfer', + + // ---- TO or Captain Actions ---- + ToggleActive = 'toggleActive', + + // ---- Player Actions ---- + + /** Delete own TournamentRegistration. */ + Leave = 'leave', + + // TODO + // SubmitList = 'submitList', } /** diff --git a/convex/_model/tournamentResults/_helpers/aggregateTournamentData.ts b/convex/_model/tournamentResults/_helpers/aggregateTournamentData.ts index 83d419b8..04c5ec1e 100644 --- a/convex/_model/tournamentResults/_helpers/aggregateTournamentData.ts +++ b/convex/_model/tournamentResults/_helpers/aggregateTournamentData.ts @@ -42,6 +42,7 @@ export const aggregateTournamentData = async ( r.tournamentPairingId && relevantTournamentPairingIds.includes(r.tournamentPairingId) )); + // FIXME: This will break if users transfer teams... they will take their score with them. // For faster look-up: const playerUserIdMap = tournamentRegistrations.reduce((acc, registration) => ({ ...acc, diff --git a/convex/_model/tournaments/_helpers/getAvailableActions.ts b/convex/_model/tournaments/_helpers/getAvailableActions.ts index 04accae5..f122e369 100644 --- a/convex/_model/tournaments/_helpers/getAvailableActions.ts +++ b/convex/_model/tournaments/_helpers/getAvailableActions.ts @@ -2,29 +2,66 @@ import { getAuthUserId } from '@convex-dev/auth/server'; import { Doc } from '../../../_generated/dataModel'; import { QueryCtx } from '../../../_generated/server'; -import { getMatchResultsByTournamentRound } from '../../matchResults'; import { checkUserIsTournamentOrganizer } from '../../tournamentOrganizers'; import { checkUserIsRegistered } from '../../tournamentRegistrations'; import { checkTournamentVisibility } from './checkTournamentVisibility'; import { getTournamentNextRound } from './getTournamentNextRound'; export enum TournamentActionKey { - Cancel = 'cancel', - ConfigureRound = 'configureRound', - AddPlayer = 'addPlayer', // Create TournamentRegistration (+ TournamentCompetitor) as TO - Delete = 'delete', + // ---- TO Actions ---- + + /** Edit a Tournament. */ Edit = 'edit', - End = 'end', - EndRound = 'endRound', - Join = 'join', // Create TournamentRegistration (+ TournamentCompetitor) as player - Leave = 'leave', // Delete TournamentRegistration (+ TournamentCompetitor) as player + + /** Delete a Tournament. */ + Delete = 'delete', + + /** Set a Tournament's status to 'published'. */ Publish = 'publish', - ResetRound = 'resetRound', + + // TODO: UndoPublish + + /** Create a TournamentRegistration (+ TournamentCompetitor). */ + AddPlayer = 'addPlayer', // + + /** Set a (published) Tournament's status to 'archived', before it starts. */ + Cancel = 'cancel', + + // TODO: UndoCancel + + /** Set a published Tournament's status to 'active'. */ Start = 'start', + + // TODO: UndoStart + + /** Create TournamentPairings for a given round. */ + ConfigureRound = 'configureRound', + + /** Start a Tournament round. */ StartRound = 'startRound', + + /** Undo starting a Tournament round. */ + UndoStartRound = 'undoStartRound', + + /** Submit a MatchResult for the given Tournament. */ SubmitMatchResult = 'submitMatchResult', + + /** End a Tournament round. */ + EndRound = 'endRound', + + /** Undo ending a Tournament round. */ UndoEndRound = 'undoEndRound', - UndoStartRound = 'undoStartRound', + + /** Set a (active) Tournament's status to 'archived'. */ + End = 'end', + + // ---- Player Actions ---- + + /** Create own TournamentRegistration (+ TournamentCompetitor). */ + Join = 'join', + + /** Delete own TournamentRegistration (+ TournamentCompetitor). */ + Leave = 'leave', } /** @@ -49,21 +86,15 @@ export const getAvailableActions = async ( const isPlayer = await checkUserIsRegistered(ctx, doc._id, userId); // ---- GATHER DATA ---- - const hasCurrentRound = doc.currentRound !== undefined; - const nextRound = getTournamentNextRound(doc); - const hasNextRound = nextRound !== undefined; - const nextRoundPairings = await ctx.db.query('tournamentPairings') .withIndex('by_tournament_round', (q) => q.eq('tournamentId', doc._id).eq('round', nextRound ?? -1)) .collect(); const nextRoundPairingCount = (nextRoundPairings ?? []).length; - - const currentRoundMatchResults = await getMatchResultsByTournamentRound(ctx, { - tournamentId: doc._id, - round: doc.currentRound ?? 0, - }); - const currentRoundMatchResultCount = (currentRoundMatchResults ?? []).length; + + const hasNextRound = nextRound !== undefined; + const hasCurrentRound = doc.currentRound !== undefined; + const hasLastRound = doc.lastRound !== undefined; // ---- PRIMARY ACTIONS ---- const actions: TournamentActionKey[] = []; @@ -91,6 +122,10 @@ export const getAvailableActions = async ( if (isPlayer && doc.status === 'published') { actions.push(TournamentActionKey.Leave); } + + if (isOrganizer && doc.status === 'published') { + actions.push(TournamentActionKey.Cancel); + } if (isOrganizer && doc.status === 'published') { // TODO: Check for at least 2 competitors actions.push(TournamentActionKey.Start); @@ -104,17 +139,21 @@ export const getAvailableActions = async ( actions.push(TournamentActionKey.StartRound); } - if (isOrganizer && doc.status === 'active' && hasCurrentRound && currentRoundMatchResultCount === 0) { - actions.push(TournamentActionKey.ResetRound); + if (isOrganizer && doc.status === 'active' && hasCurrentRound) { + actions.push(TournamentActionKey.UndoStartRound); } if ((isOrganizer || isPlayer) && hasCurrentRound) { // TODO: Don't show if all matches checked in actions.push(TournamentActionKey.SubmitMatchResult); } - if (isOrganizer && hasCurrentRound) { + if (isOrganizer && doc.status === 'active' && hasCurrentRound) { actions.push(TournamentActionKey.EndRound); } + + if (isOrganizer && doc.status === 'active' && !hasCurrentRound && hasLastRound) { + actions.push(TournamentActionKey.UndoEndRound); + } if (isOrganizer && doc.status === 'active' && !hasCurrentRound) { actions.push(TournamentActionKey.End); diff --git a/package-lock.json b/package-lock.json index 708e7139..6c00ec74 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@dnd-kit/sortable": "^10.0.0", "@fontsource/figtree": "^5.1.0", "@hookform/resolvers": "^3.9.0", - "@ianpaschal/combat-command-components": "file:../combat-command-components/ianpaschal-combat-command-components-1.6.0.tgz", + "@ianpaschal/combat-command-components": "^1.6.1", "@ianpaschal/combat-command-game-systems": "^1.1.4", "@mapbox/search-js-core": "^1.0.0-beta.25", "@radix-ui/colors": "^3.0.0", @@ -1486,9 +1486,9 @@ "license": "BSD-3-Clause" }, "node_modules/@ianpaschal/combat-command-components": { - "version": "1.6.0", - "resolved": "file:../combat-command-components/ianpaschal-combat-command-components-1.6.0.tgz", - "integrity": "sha512-/hyD74iYuBVfzoYA9IyCcEFxGLDkIkvxqTnuj9sr3X/qInswVoD5FXgQu4bEqpQCzEGYCO3xz46uctfVzmFYYA==", + "version": "1.6.1", + "resolved": "https://npm.pkg.github.com/download/@ianpaschal/combat-command-components/1.6.1/464caa4910f2c6cd9503846cf47eeb2cb0219b61", + "integrity": "sha512-wDGcisNn0uzjRD7A9LNNsaU3JRPqjq1UIsPr5zWEoC2wRsfBlDcF3oKljeGGqHh/1f4Ul+EBmaALKU/RbrX9oA==", "license": "MIT", "dependencies": { "@base-ui/react": "^1.0.0", @@ -11519,9 +11519,9 @@ } }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -11802,9 +11802,9 @@ } }, "node_modules/react-router": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.5.tgz", - "integrity": "sha512-JmxqrnBZ6E9hWmf02jzNn9Jm3UqyeimyiwzD69NjxGySG6lIz/1LVPsoTCwN7NBX2XjCEa1LIX5EMz1j2b6u6A==", + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.12.0.tgz", + "integrity": "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -11824,12 +11824,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.5.tgz", - "integrity": "sha512-mkEmq/K8tKN63Ae2M7Xgz3c9l9YNbY+NHH6NNeUmLA3kDkhKXRsNb/ZpxaEunvGo2/3YXdk5EJU3Hxp3ocaBPw==", + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.12.0.tgz", + "integrity": "sha512-pfO9fiBcpEfX4Tx+iTYKDtPbrSLLCbwJ5EqP+SPYQu1VYCXdy79GSj0wttR0U4cikVdlImZuEZ/9ZNCgoaxwBA==", "license": "MIT", "dependencies": { - "react-router": "7.9.5" + "react-router": "7.12.0" }, "engines": { "node": ">=20.0.0" diff --git a/package.json b/package.json index 2e5acb80..b052db3e 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "@dnd-kit/sortable": "^10.0.0", "@fontsource/figtree": "^5.1.0", "@hookform/resolvers": "^3.9.0", - "@ianpaschal/combat-command-components": "file:../combat-command-components/ianpaschal-combat-command-components-1.6.0.tgz", + "@ianpaschal/combat-command-components": "^1.6.1", "@ianpaschal/combat-command-game-systems": "^1.1.4", "@mapbox/search-js-core": "^1.0.0-beta.25", "@radix-ui/colors": "^3.0.0", From 576513e0c1b0e718267938515733204bbc716f01 Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Tue, 13 Jan 2026 18:04:40 +0100 Subject: [PATCH 3/7] WIP --- convex/_model/common/errors.ts | 1 - .../_helpers/getAvailableActions.ts | 38 ++++++++----------- convex/_model/tournamentCompetitors/table.ts | 4 +- .../_helpers/deepenTournamentPairing.ts | 1 + .../_helpers/getAvailableActions.ts | 7 ++-- .../_model/tournamentRegistrations/index.ts | 1 - .../mutations/createTournamentRegistration.ts | 1 - .../_model/tournamentRegistrations/types.ts | 5 --- .../_helpers/aggregateTournamentData.ts | 1 - .../_helpers/getAvailableActions.ts | 28 ++++++++------ convex/_model/tournaments/table.ts | 6 --- src/api.ts | 1 - .../MatchResultForm/MatchResultForm.tsx | 3 +- .../actions/useDeleteAction.tsx | 9 +++-- .../TournamentRegistrationForm.schema.ts | 16 ++++++-- src/utils/common/getTournamentDisplayName.ts | 8 ---- .../common/getTournamentPairingDisplayName.ts | 17 --------- 17 files changed, 57 insertions(+), 90 deletions(-) delete mode 100644 src/utils/common/getTournamentDisplayName.ts delete mode 100644 src/utils/common/getTournamentPairingDisplayName.ts diff --git a/convex/_model/common/errors.ts b/convex/_model/common/errors.ts index b4f625b4..1db9d149 100644 --- a/convex/_model/common/errors.ts +++ b/convex/_model/common/errors.ts @@ -20,7 +20,6 @@ export const errors = { COMPETITOR_ALREADY_HAS_MAX_PLAYERS: 'Team already has the maximum number of active players.', CANNOT_CREATE_REGISTRATION_WITHOUT_COMPETITOR: 'Cannot create a registration without a competitor ID or name.', CANNOT_CREATE_REGISTRATION_WITH_COMPETITOR_NAME_ID: 'Cannot create a registration with both a competitor ID and name.', - CANNOT_CREATE_REGISTRATION_WITHOUT_GROUP: 'Tournament requires a competitor group.', // Tournament Lifecycle CANNOT_CLOSE_ROUND_ON_ARCHIVED_TOURNAMENT: 'Cannot close a round on an archived tournament.', diff --git a/convex/_model/tournamentCompetitors/_helpers/getAvailableActions.ts b/convex/_model/tournamentCompetitors/_helpers/getAvailableActions.ts index f72cbf56..e776f18c 100644 --- a/convex/_model/tournamentCompetitors/_helpers/getAvailableActions.ts +++ b/convex/_model/tournamentCompetitors/_helpers/getAvailableActions.ts @@ -3,7 +3,6 @@ import { getAuthUserId } from '@convex-dev/auth/server'; import { Doc } from '../../../_generated/dataModel'; import { QueryCtx } from '../../../_generated/server'; import { checkUserIsTournamentOrganizer } from '../../tournamentOrganizers'; -import { getTournamentRegistrationsByCompetitor } from '../../tournamentRegistrations'; import { checkUserIsRegistered } from '../../tournamentRegistrations/_helpers/checkUserIsRegistered'; export enum TournamentCompetitorActionKey { @@ -15,9 +14,6 @@ export enum TournamentCompetitorActionKey { /** Set a TournamentCompetitor's 'active' field to true or false. */ ToggleActive = 'toggleActive', - // TODO - // TransferPlayers = 'transferPlayers', - // ---- TO or Captain Actions ---- /** Edit a TournamentCompetitor. */ @@ -51,18 +47,15 @@ export const getAvailableActions = async ( if (!tournament) { return []; } - const tournamentRegistrations = await getTournamentRegistrationsByCompetitor(ctx, { - tournamentCompetitorId: doc._id, - }); // --- CHECK AUTH ---- const userId = await getAuthUserId(ctx); const isOrganizer = await checkUserIsTournamentOrganizer(ctx, tournament._id, userId); const isPlayer = await checkUserIsRegistered(ctx, tournament._id, userId); - const isCaptain = userId && doc.captainUserId === userId; const isTeamTournament = tournament.competitorSize > 1; - const hasSparePlayers = tournamentRegistrations.length > tournament.competitorSize; + const isCaptain = isTeamTournament && userId && doc.captainUserId === userId; + const hasCurrentRound = tournament.currentRound !== undefined; // ---- PRIMARY ACTIONS ---- const actions: TournamentCompetitorActionKey[] = []; @@ -71,33 +64,32 @@ export const getAvailableActions = async ( return actions; } - if (isOrganizer || (isCaptain && hasSparePlayers)) { - actions.push(TournamentCompetitorActionKey.Edit); + // TO Actions: + if (isOrganizer && isTeamTournament && !hasCurrentRound) { + actions.push(TournamentCompetitorActionKey.AddPlayer); } - if ((isOrganizer || (isCaptain && isTeamTournament)) && tournament.status !== 'active') { - actions.push(TournamentCompetitorActionKey.Delete); + if (isOrganizer && tournament.status === 'active' && !hasCurrentRound) { + actions.push(TournamentCompetitorActionKey.ToggleActive); } - if (isOrganizer && isTeamTournament) { - actions.push(TournamentCompetitorActionKey.AddPlayer); + // TO or Captain Actions: + if (isOrganizer || isCaptain) { + actions.push(TournamentCompetitorActionKey.Edit); } - // if (isOrganizer) { - // actions.push(TournamentCompetitorActionKey.TransferPlayers); - // } + if ((isOrganizer || isCaptain) && tournament.status !== 'active') { + actions.push(TournamentCompetitorActionKey.Delete); + } + // Player Actions: if (!isPlayer && tournament.status === 'published' && isTeamTournament) { actions.push(TournamentCompetitorActionKey.Join); } - if (isPlayer && ['draft', 'published'].includes(tournament.status) && isTeamTournament) { + if (isPlayer && tournament.status === 'published' && isTeamTournament) { actions.push(TournamentCompetitorActionKey.Leave); } - if (isOrganizer && tournament.status === 'active' && tournament.currentRound === undefined) { - actions.push(TournamentCompetitorActionKey.ToggleActive); - } - return actions; }; diff --git a/convex/_model/tournamentCompetitors/table.ts b/convex/_model/tournamentCompetitors/table.ts index d401ccbc..6b015cd4 100644 --- a/convex/_model/tournamentCompetitors/table.ts +++ b/convex/_model/tournamentCompetitors/table.ts @@ -7,7 +7,6 @@ export const editableFields = { captainUserId: v.optional(v.id('users')), scoreAdjustments: v.optional(v.array(scoreAdjustment)), teamName: v.optional(v.string()), - tournamentGroupId: v.optional(v.id('tournamentGroups')), tournamentId: v.id('tournaments'), }; @@ -23,5 +22,4 @@ export default defineTable({ ...editableFields, ...computedFields, }) - .index('by_tournament_id', ['tournamentId']) - .index('by_tournament_group', ['tournamentGroupId']); + .index('by_tournament_id', ['tournamentId']); diff --git a/convex/_model/tournamentPairings/_helpers/deepenTournamentPairing.ts b/convex/_model/tournamentPairings/_helpers/deepenTournamentPairing.ts index a8cc3b8d..f5dd6362 100644 --- a/convex/_model/tournamentPairings/_helpers/deepenTournamentPairing.ts +++ b/convex/_model/tournamentPairings/_helpers/deepenTournamentPairing.ts @@ -52,6 +52,7 @@ export const deepenTournamentPairing = async ( return { ...tournamentPairing, + displayName: `${tournamentCompetitor0.displayName} vs. ${tournamentCompetitor1?.displayName ?? 'Bye'}`, tournamentCompetitor0, tournamentCompetitor1, playerUserIds: [ diff --git a/convex/_model/tournamentRegistrations/_helpers/getAvailableActions.ts b/convex/_model/tournamentRegistrations/_helpers/getAvailableActions.ts index 0cc413a7..2301d663 100644 --- a/convex/_model/tournamentRegistrations/_helpers/getAvailableActions.ts +++ b/convex/_model/tournamentRegistrations/_helpers/getAvailableActions.ts @@ -60,7 +60,8 @@ export const getAvailableActions = async ( const isOrganizer = await checkUserIsTournamentOrganizer(ctx, tournament._id, userId); const isSelf = userId && doc.userId === userId; - const isCaptain = userId && tournamentCompetitor.captainUserId === userId; + const isTeamTournament = tournament.competitorSize > 1; + const isCaptain = isTeamTournament && userId && tournamentCompetitor.captainUserId === userId; const hasSparePlayers = tournamentRegistrations.length > tournament.competitorSize; // const isListSubmissionOpen = Date.now() < tournament.listSubmissionClosesAt; @@ -75,11 +76,11 @@ export const getAvailableActions = async ( // actions.push(TournamentRegistrationActionKey.ApproveList); // } - if ((isOrganizer || (isCaptain && !isSelf)) && tournament.status !== 'active') { + if ((isOrganizer || (isCaptain && !isSelf)) && tournament.status === 'published') { actions.push(TournamentRegistrationActionKey.Delete); } - if (isSelf && tournament.status !== 'active') { + if (isSelf && tournament.status === 'published') { actions.push(TournamentRegistrationActionKey.Leave); } diff --git a/convex/_model/tournamentRegistrations/index.ts b/convex/_model/tournamentRegistrations/index.ts index 7faf4336..0043da2c 100644 --- a/convex/_model/tournamentRegistrations/index.ts +++ b/convex/_model/tournamentRegistrations/index.ts @@ -1,7 +1,6 @@ // Types export type { TournamentRegistration, - TournamentRegistrationFormData, TournamentRegistrationId, } from './types'; diff --git a/convex/_model/tournamentRegistrations/mutations/createTournamentRegistration.ts b/convex/_model/tournamentRegistrations/mutations/createTournamentRegistration.ts index c372803b..e85832ec 100644 --- a/convex/_model/tournamentRegistrations/mutations/createTournamentRegistration.ts +++ b/convex/_model/tournamentRegistrations/mutations/createTournamentRegistration.ts @@ -76,7 +76,6 @@ export const createTournamentRegistration = async ( tournamentCompetitorId = await ctx.db.insert('tournamentCompetitors', { captainUserId: args.userId, teamName: args.tournamentCompetitor?.teamName, - // tournamentGroupId, tournamentId: args.tournamentId, }); } diff --git a/convex/_model/tournamentRegistrations/types.ts b/convex/_model/tournamentRegistrations/types.ts index f1951e5e..a3729534 100644 --- a/convex/_model/tournamentRegistrations/types.ts +++ b/convex/_model/tournamentRegistrations/types.ts @@ -1,10 +1,5 @@ -import { Infer, v } from 'convex/values'; - import { deepenTournamentRegistration } from './_helpers/deepenTournamentRegistration'; import { Id } from '../../_generated/dataModel'; -import { editableFields } from './table'; export type TournamentRegistrationId = Id<'tournamentRegistrations'>; export type TournamentRegistration = Awaited>; -const formData = v.object(editableFields); -export type TournamentRegistrationFormData = Infer; diff --git a/convex/_model/tournamentResults/_helpers/aggregateTournamentData.ts b/convex/_model/tournamentResults/_helpers/aggregateTournamentData.ts index 04c5ec1e..83d419b8 100644 --- a/convex/_model/tournamentResults/_helpers/aggregateTournamentData.ts +++ b/convex/_model/tournamentResults/_helpers/aggregateTournamentData.ts @@ -42,7 +42,6 @@ export const aggregateTournamentData = async ( r.tournamentPairingId && relevantTournamentPairingIds.includes(r.tournamentPairingId) )); - // FIXME: This will break if users transfer teams... they will take their score with them. // For faster look-up: const playerUserIdMap = tournamentRegistrations.reduce((acc, registration) => ({ ...acc, diff --git a/convex/_model/tournaments/_helpers/getAvailableActions.ts b/convex/_model/tournaments/_helpers/getAvailableActions.ts index f122e369..1ca0923f 100644 --- a/convex/_model/tournaments/_helpers/getAvailableActions.ts +++ b/convex/_model/tournaments/_helpers/getAvailableActions.ts @@ -98,12 +98,17 @@ export const getAvailableActions = async ( // ---- PRIMARY ACTIONS ---- const actions: TournamentActionKey[] = []; + + if (doc.status === 'archived') { + return actions; + } - if (isOrganizer && ['draft', 'published'].includes(doc.status)) { + // TO Actions: + if (isOrganizer && doc.status !== 'active') { actions.push(TournamentActionKey.Edit); } - if (isOrganizer && ['draft', 'published'].includes(doc.status)) { + if (isOrganizer && doc.status !== 'active') { actions.push(TournamentActionKey.Delete); } @@ -111,17 +116,9 @@ export const getAvailableActions = async ( actions.push(TournamentActionKey.Publish); } - if (isOrganizer && doc.status === 'published') { + if (isOrganizer && doc.status !== 'draft' && !hasCurrentRound) { actions.push(TournamentActionKey.AddPlayer); } - - if (!isPlayer && doc.status === 'published') { - actions.push(TournamentActionKey.Join); - } - - if (isPlayer && doc.status === 'published') { - actions.push(TournamentActionKey.Leave); - } if (isOrganizer && doc.status === 'published') { actions.push(TournamentActionKey.Cancel); @@ -158,6 +155,15 @@ export const getAvailableActions = async ( if (isOrganizer && doc.status === 'active' && !hasCurrentRound) { actions.push(TournamentActionKey.End); } + + // Player Actions + if (!isPlayer && doc.status === 'published') { + actions.push(TournamentActionKey.Join); + } + + if (isPlayer && doc.status === 'published') { + actions.push(TournamentActionKey.Leave); + } return actions; }; diff --git a/convex/_model/tournaments/table.ts b/convex/_model/tournaments/table.ts index f2c95522..a0a6af0e 100644 --- a/convex/_model/tournaments/table.ts +++ b/convex/_model/tournaments/table.ts @@ -55,12 +55,6 @@ export const editableFields = { currency: currencyCode, })), useNationalTeams: v.boolean(), - allowCompetitorGroupChoice: v.optional(v.boolean()), - groupBehavior: v.optional(v.union( - v.literal('hidden'), // Hidden from player - v.literal('optional'), // Player can select, otherwise auto assigned - v.literal('required'), // Player must select - )), // Format pairingMethod: tournamentPairingMethod, diff --git a/src/api.ts b/src/api.ts index 6dbbc154..174a1343 100644 --- a/src/api.ts +++ b/src/api.ts @@ -72,7 +72,6 @@ export { export { type TournamentRegistration, TournamentRegistrationActionKey, - type TournamentRegistrationFormData, type TournamentRegistrationId, } from '../convex/_model/tournamentRegistrations'; diff --git a/src/components/MatchResultForm/MatchResultForm.tsx b/src/components/MatchResultForm/MatchResultForm.tsx index bdc8ec1d..63f4210d 100644 --- a/src/components/MatchResultForm/MatchResultForm.tsx +++ b/src/components/MatchResultForm/MatchResultForm.tsx @@ -30,7 +30,6 @@ import { useAsyncState } from '~/hooks/useAsyncState'; import { useCreateMatchResult, useUpdateMatchResult } from '~/services/matchResults'; import { useGetActiveTournamentPairingsByUser } from '~/services/tournamentPairings'; import { useGetTournamentByTournamentPairing } from '~/services/tournaments'; -import { getTournamentPairingDisplayName } from '~/utils/common/getTournamentPairingDisplayName'; import { validateForm } from '~/utils/validateForm'; import { SingleMatchPlayerFields } from './components/SingleMatchPlayerFields'; import { TournamentPlayerFields } from './components/TournamentPlayerFields'; @@ -167,7 +166,7 @@ export const MatchResultForm = ({ { value: 'single', label: 'Single Match' }, ...(tournamentPairings || []).filter((pairing) => pairing.matchResultsProgress.submitted < pairing.matchResultsProgress.required).map((pairing) => ({ value: pairing._id, - label: getTournamentPairingDisplayName(pairing), + label: pairing.displayName, })), ]; diff --git a/src/components/TournamentProvider/actions/useDeleteAction.tsx b/src/components/TournamentProvider/actions/useDeleteAction.tsx index c18ef79b..c8d5944c 100644 --- a/src/components/TournamentProvider/actions/useDeleteAction.tsx +++ b/src/components/TournamentProvider/actions/useDeleteAction.tsx @@ -1,11 +1,14 @@ -import { Tournament, TournamentActionKey } from '~/api'; +import { + getTournamentDisplayName, + Tournament, + TournamentActionKey, +} from '~/api'; import { ActionDefinition } from '~/components/ContextMenu/ContextMenu.types'; import { toast } from '~/components/ToastProvider'; import { useDialogInstance } from '~/hooks/useDialogInstance'; import { useNavigateAway } from '~/hooks/useNavigateAway'; import { useDeleteTournament } from '~/services/tournaments'; import { PATHS } from '~/settings'; -import { getTournamentDisplayName } from '~/utils/common/getTournamentDisplayName'; const LABEL = 'Delete'; const KEY = TournamentActionKey.Delete; @@ -30,7 +33,7 @@ export const useDeleteAction = ( handler: () => open({ title: 'Warning!', content: ( - {`Are you sure you want to delete ${displayName}?`}This cannot be undone! + {`Are you sure you want to delete ${displayName}?`} This cannot be undone! ), actions: [ { diff --git a/src/components/TournamentRegistrationForm/TournamentRegistrationForm.schema.ts b/src/components/TournamentRegistrationForm/TournamentRegistrationForm.schema.ts index ab03ef46..edcb9f0e 100644 --- a/src/components/TournamentRegistrationForm/TournamentRegistrationForm.schema.ts +++ b/src/components/TournamentRegistrationForm/TournamentRegistrationForm.schema.ts @@ -11,11 +11,19 @@ const emptyToUndefined = (schema: T) => z.preprocess((va export const createSchema = () => z.object({ tournamentCompetitor: emptyToUndefined(z.object({ - teamName: emptyToUndefined(z.string().min(2).optional()), + teamName: emptyToUndefined(z.string({ + message: 'Please provide a team name.', + }).min(2).optional()), }).optional()), - tournamentCompetitorId: emptyToUndefined(z.string().transform((val) => val as TournamentCompetitorId).optional()), - tournamentId: z.string().transform((val) => val as TournamentId), - userId: z.string({ message: 'Please select a user.' }).transform((val) => val as UserId), + tournamentCompetitorId: emptyToUndefined(z.string({ + message: 'Please select a team.', + }).transform((val) => val as TournamentCompetitorId).optional()), + tournamentId: z.string({ + message: 'Please select a tournament.', + }).transform((val) => val as TournamentId), + userId: z.string({ + message: 'Please select a user.', + }).transform((val) => val as UserId), }); export type SubmitData = z.infer>; diff --git a/src/utils/common/getTournamentDisplayName.ts b/src/utils/common/getTournamentDisplayName.ts deleted file mode 100644 index cfe021bd..00000000 --- a/src/utils/common/getTournamentDisplayName.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Tournament } from '~/api'; - -export const getTournamentDisplayName = (tournament: Pick): string => { - if (tournament.editionYear) { - return `${tournament.title} ${tournament.editionYear}`; - } - return tournament.title; -}; diff --git a/src/utils/common/getTournamentPairingDisplayName.ts b/src/utils/common/getTournamentPairingDisplayName.ts deleted file mode 100644 index ce88f54b..00000000 --- a/src/utils/common/getTournamentPairingDisplayName.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { TournamentPairing } from '~/api'; - -/** - * Gets the display name for a tournament pairing. - * - * @param tournamentPairing - A (deep) tournament pairing. - * @returns - A display name string. - */ -export const getTournamentPairingDisplayName = ( - tournamentPairing: TournamentPairing, -): string => { - const { - tournamentCompetitor0, - tournamentCompetitor1, - } = tournamentPairing; - return `${tournamentCompetitor0.displayName} vs. ${tournamentCompetitor1?.displayName ?? 'Bye'}`; -}; From 227a8d64cbce63b87bccc1715f4dacce113bef16 Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Wed, 14 Jan 2026 09:27:27 +0100 Subject: [PATCH 4/7] Final tweaks --- .../_helpers/getAvailableActions.ts | 14 ++++----- .../_helpers/getAvailableActions.ts | 4 +-- .../_helpers/getAvailableActions.ts | 2 +- .../users/queries/internal/createIdFilter.ts | 30 ------------------- .../actions/useDeleteAction.tsx | 2 +- 5 files changed, 11 insertions(+), 41 deletions(-) delete mode 100644 convex/_model/users/queries/internal/createIdFilter.ts diff --git a/convex/_model/tournamentCompetitors/_helpers/getAvailableActions.ts b/convex/_model/tournamentCompetitors/_helpers/getAvailableActions.ts index e776f18c..ecc50149 100644 --- a/convex/_model/tournamentCompetitors/_helpers/getAvailableActions.ts +++ b/convex/_model/tournamentCompetitors/_helpers/getAvailableActions.ts @@ -8,25 +8,25 @@ import { checkUserIsRegistered } from '../../tournamentRegistrations/_helpers/ch export enum TournamentCompetitorActionKey { // ---- TO Actions ---- - /** Create a TournamentRegistration for a given TournamentCompetitor. */ + /** Create a TournamentRegistration for this TournamentCompetitor. */ AddPlayer = 'addPlayer', - /** Set a TournamentCompetitor's 'active' field to true or false. */ + /** Toggle this TournamentCompetitor's 'active' field to true or false. */ ToggleActive = 'toggleActive', // ---- TO or Captain Actions ---- - /** Edit a TournamentCompetitor. */ + /** Edit this TournamentCompetitor. */ Edit = 'edit', - /** Delete a TournamentCompetitor. */ + /** Delete this TournamentCompetitor. */ Delete = 'delete', // ---- Player Actions ---- - /** Create own TournamentRegistration. */ + /** Create own TournamentRegistration for this TournamentCompetitor. */ Join = 'join', - /** Delete own TournamentRegistration. */ + /** Delete own TournamentRegistration for this TournamentCompetitor. */ Leave = 'leave', } @@ -65,7 +65,7 @@ export const getAvailableActions = async ( } // TO Actions: - if (isOrganizer && isTeamTournament && !hasCurrentRound) { + if (isOrganizer && tournament.status !== 'draft' && isTeamTournament && !hasCurrentRound) { actions.push(TournamentCompetitorActionKey.AddPlayer); } diff --git a/convex/_model/tournamentRegistrations/_helpers/getAvailableActions.ts b/convex/_model/tournamentRegistrations/_helpers/getAvailableActions.ts index 2301d663..1c088b61 100644 --- a/convex/_model/tournamentRegistrations/_helpers/getAvailableActions.ts +++ b/convex/_model/tournamentRegistrations/_helpers/getAvailableActions.ts @@ -7,7 +7,7 @@ import { checkUserIsTournamentOrganizer } from '../../tournamentOrganizers'; export enum TournamentRegistrationActionKey { // ---- TO Actions ---- - /** Delete a TournamentRegistration. */ + /** Delete this TournamentRegistration (and TournamentCompetitor if no remaining players). */ Delete = 'delete', // TODO @@ -24,7 +24,7 @@ export enum TournamentRegistrationActionKey { // ---- Player Actions ---- - /** Delete own TournamentRegistration. */ + /** Delete own TournamentRegistration (and TournamentCompetitor if no remaining players). */ Leave = 'leave', // TODO diff --git a/convex/_model/tournaments/_helpers/getAvailableActions.ts b/convex/_model/tournaments/_helpers/getAvailableActions.ts index 1ca0923f..f9106c79 100644 --- a/convex/_model/tournaments/_helpers/getAvailableActions.ts +++ b/convex/_model/tournaments/_helpers/getAvailableActions.ts @@ -22,7 +22,7 @@ export enum TournamentActionKey { // TODO: UndoPublish /** Create a TournamentRegistration (+ TournamentCompetitor). */ - AddPlayer = 'addPlayer', // + AddPlayer = 'addPlayer', /** Set a (published) Tournament's status to 'archived', before it starts. */ Cancel = 'cancel', diff --git a/convex/_model/users/queries/internal/createIdFilter.ts b/convex/_model/users/queries/internal/createIdFilter.ts deleted file mode 100644 index e2784522..00000000 --- a/convex/_model/users/queries/internal/createIdFilter.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Expression, FilterBuilder } from 'convex/server'; - -import { DataModel, Id } from '../../../../_generated/dataModel'; - -type CreateIdFilterArgs = { - limitToIds?: Id<'users'>[]; - excludeIds?: Id<'users'>[]; -}; - -export const createIdFilter = (args: CreateIdFilterArgs): ((q: FilterBuilder) => Expression) | undefined => { - // If no filtering is needed, return undefined to skip the filter - if ((!args.limitToIds || args.limitToIds.length === 0) && - (!args.excludeIds || args.excludeIds.length === 0)) { - return undefined; - } - - return (q: FilterBuilder): Expression => { - if (args.limitToIds && args.limitToIds.length > 0) { - const isInLimitList = q.or(...args.limitToIds.map((id) => q.eq(q.field('_id'), id))); - if (args.excludeIds && args.excludeIds.length > 0) { - const isNotExcluded = q.not(q.or(...args.excludeIds.map((id) => q.eq(q.field('_id'), id)))); - return q.and(isInLimitList, isNotExcluded); - } - return isInLimitList; - } - - // This branch is only reached when excludeIds is provided but limitToIds is not - return q.not(q.or(...args.excludeIds!.map((id) => q.eq(q.field('_id'), id)))); - }; -}; diff --git a/src/components/TournamentRegistrationProvider/actions/useDeleteAction.tsx b/src/components/TournamentRegistrationProvider/actions/useDeleteAction.tsx index 567a37b4..47ed71b7 100644 --- a/src/components/TournamentRegistrationProvider/actions/useDeleteAction.tsx +++ b/src/components/TournamentRegistrationProvider/actions/useDeleteAction.tsx @@ -55,7 +55,7 @@ export const useDeleteAction = ( { intent: 'danger', onClick: () => mutation({ id: subject._id }), - text: 'Remove', + text: LABEL, }, ], }), From 804522bf17d23e44687c33d3e7e1e22ccabab55f Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Wed, 21 Jan 2026 10:07:51 +0100 Subject: [PATCH 5/7] WIP --- convex/_generated/api.d.ts | 4 +- convex/_model/common/errors.ts | 1 + convex/_model/common/types.ts | 14 ++ .../_helpers/getAvailableActions.ts | 8 +- .../_helpers/getDisplayName.ts | 8 +- .../_helpers/getAvailableActions.ts | 6 +- .../_helpers/getCreateSuccessMessage.ts | 38 ++++ .../mutations/createTournamentRegistration.ts | 50 +++--- .../tournaments/_helpers/getDisplayName.ts | 5 +- convex/_model/users/_helpers/redactUser.ts | 7 +- package-lock.json | 132 ++++++++++++-- package.json | 4 +- .../ContextMenu/ContextMenu.types.ts | 4 +- src/components/ContextMenu/index.ts | 2 + .../InputSingleFile/InputSingleFile.tsx | 3 - .../InputUser/InputUser.module.scss | 17 +- src/components/InputUser/InputUser.tsx | 169 +++++++++--------- .../TournamentCompetitorProvider.hooks.tsx | 48 +++-- .../actions/useAddPlayerAction.tsx | 51 +----- .../actions/useJoinAction.tsx | 41 ++--- .../actions/useLeaveAction.tsx | 76 -------- .../TournamentCompetitorProvider/index.ts | 1 - .../TournamentContextMenu.tsx | 14 +- .../TournamentProvider.hooks.tsx | 62 ++++--- .../actions/useAddPlayerAction.tsx | 46 +---- .../actions/useJoinAction.tsx | 53 ++---- .../actions/useLeaveAction.tsx | 75 -------- src/components/TournamentProvider/index.ts | 1 + .../utils/useCreateRegistrationAction.tsx | 69 +++++++ .../TournamentRegistrationForm.module.scss | 4 + .../TournamentRegistrationForm.schema.ts | 24 ++- .../TournamentRegistrationForm.tsx | 74 +++----- .../TournamentRegistrationForm.utils.ts | 16 ++ .../TournamentRegistrationForm/index.ts | 3 + .../TournamentRegistrationProvider.hooks.ts | 25 +-- .../TournamentRegistrationProvider.utils.ts | 22 +++ .../actions/useDeleteAction.tsx | 86 +++++---- .../actions/useLeaveAction.tsx | 69 +++++++ .../actions/useToggleActiveAction.tsx | 15 +- .../TournamentRegistrationProvider/index.ts | 2 +- src/components/generic/Checkbox/Checkbox.tsx | 3 +- src/components/generic/Form/FormField.tsx | 3 +- .../generic/InputCountry/InputCountry.tsx | 6 +- .../generic/InputCurrency/InputCurrency.tsx | 2 - .../generic/InputLocation/InputLocation.tsx | 2 - .../generic/InputNumber/InputNumber.scss | 8 +- .../generic/InputNumber/InputNumber.tsx | 4 +- .../generic/InputSelect/InputSelect.tsx | 6 +- .../generic/InputText/InputText.module.scss | 2 +- .../generic/InputText/InputText.tsx | 3 - .../generic/InputTextArea/InputTextArea.scss | 8 +- .../generic/InputTextArea/InputTextArea.tsx | 7 +- src/components/generic/Switch/Switch.tsx | 8 +- .../TournamentRegistrationsTable.tsx | 2 +- 54 files changed, 777 insertions(+), 636 deletions(-) create mode 100644 convex/_model/tournamentRegistrations/_helpers/getCreateSuccessMessage.ts delete mode 100644 src/components/TournamentCompetitorProvider/actions/useLeaveAction.tsx delete mode 100644 src/components/TournamentProvider/actions/useLeaveAction.tsx create mode 100644 src/components/TournamentProvider/utils/useCreateRegistrationAction.tsx create mode 100644 src/components/TournamentRegistrationForm/TournamentRegistrationForm.utils.ts create mode 100644 src/components/TournamentRegistrationProvider/TournamentRegistrationProvider.utils.ts create mode 100644 src/components/TournamentRegistrationProvider/actions/useLeaveAction.tsx diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index 77d0c772..8be28f8c 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -164,6 +164,7 @@ import type * as _model_tournamentPairings_table from "../_model/tournamentPairi import type * as _model_tournamentRegistrations__helpers_checkUserIsRegistered from "../_model/tournamentRegistrations/_helpers/checkUserIsRegistered.js"; import type * as _model_tournamentRegistrations__helpers_deepenTournamentRegistration from "../_model/tournamentRegistrations/_helpers/deepenTournamentRegistration.js"; import type * as _model_tournamentRegistrations__helpers_getAvailableActions from "../_model/tournamentRegistrations/_helpers/getAvailableActions.js"; +import type * as _model_tournamentRegistrations__helpers_getCreateSuccessMessage from "../_model/tournamentRegistrations/_helpers/getCreateSuccessMessage.js"; import type * as _model_tournamentRegistrations_index from "../_model/tournamentRegistrations/index.js"; import type * as _model_tournamentRegistrations_mutations_createTournamentRegistration from "../_model/tournamentRegistrations/mutations/createTournamentRegistration.js"; import type * as _model_tournamentRegistrations_mutations_deleteTournamentRegistration from "../_model/tournamentRegistrations/mutations/deleteTournamentRegistration.js"; @@ -249,7 +250,6 @@ import type * as _model_users_mutations_updateUserAvatarNoAuth from "../_model/u import type * as _model_users_queries_getCurrentUser from "../_model/users/queries/getCurrentUser.js"; import type * as _model_users_queries_getUser from "../_model/users/queries/getUser.js"; import type * as _model_users_queries_getUsers from "../_model/users/queries/getUsers.js"; -import type * as _model_users_queries_internal_createIdFilter from "../_model/users/queries/internal/createIdFilter.js"; import type * as _model_users_queries_internal_getUserByClaimToken from "../_model/users/queries/internal/getUserByClaimToken.js"; import type * as _model_users_queries_internal_getUserByEmail from "../_model/users/queries/internal/getUserByEmail.js"; import type * as _model_users_table from "../_model/users/table.js"; @@ -464,6 +464,7 @@ declare const fullApi: ApiFromModules<{ "_model/tournamentRegistrations/_helpers/checkUserIsRegistered": typeof _model_tournamentRegistrations__helpers_checkUserIsRegistered; "_model/tournamentRegistrations/_helpers/deepenTournamentRegistration": typeof _model_tournamentRegistrations__helpers_deepenTournamentRegistration; "_model/tournamentRegistrations/_helpers/getAvailableActions": typeof _model_tournamentRegistrations__helpers_getAvailableActions; + "_model/tournamentRegistrations/_helpers/getCreateSuccessMessage": typeof _model_tournamentRegistrations__helpers_getCreateSuccessMessage; "_model/tournamentRegistrations/index": typeof _model_tournamentRegistrations_index; "_model/tournamentRegistrations/mutations/createTournamentRegistration": typeof _model_tournamentRegistrations_mutations_createTournamentRegistration; "_model/tournamentRegistrations/mutations/deleteTournamentRegistration": typeof _model_tournamentRegistrations_mutations_deleteTournamentRegistration; @@ -549,7 +550,6 @@ declare const fullApi: ApiFromModules<{ "_model/users/queries/getCurrentUser": typeof _model_users_queries_getCurrentUser; "_model/users/queries/getUser": typeof _model_users_queries_getUser; "_model/users/queries/getUsers": typeof _model_users_queries_getUsers; - "_model/users/queries/internal/createIdFilter": typeof _model_users_queries_internal_createIdFilter; "_model/users/queries/internal/getUserByClaimToken": typeof _model_users_queries_internal_getUserByClaimToken; "_model/users/queries/internal/getUserByEmail": typeof _model_users_queries_internal_getUserByEmail; "_model/users/table": typeof _model_users_table; diff --git a/convex/_model/common/errors.ts b/convex/_model/common/errors.ts index 1db9d149..adbc62ce 100644 --- a/convex/_model/common/errors.ts +++ b/convex/_model/common/errors.ts @@ -20,6 +20,7 @@ export const errors = { COMPETITOR_ALREADY_HAS_MAX_PLAYERS: 'Team already has the maximum number of active players.', CANNOT_CREATE_REGISTRATION_WITHOUT_COMPETITOR: 'Cannot create a registration without a competitor ID or name.', CANNOT_CREATE_REGISTRATION_WITH_COMPETITOR_NAME_ID: 'Cannot create a registration with both a competitor ID and name.', + CANNOT_CREATE_REGISTRATION_WITHOUT_REAL_NAME: 'Cannot create a registration without revealing real name.', // Tournament Lifecycle CANNOT_CLOSE_ROUND_ON_ARCHIVED_TOURNAMENT: 'Cannot close a round on an archived tournament.', diff --git a/convex/_model/common/types.ts b/convex/_model/common/types.ts index dff437fd..837a2729 100644 --- a/convex/_model/common/types.ts +++ b/convex/_model/common/types.ts @@ -25,3 +25,17 @@ export type TriggerChange = { newDoc: Doc | null; oldDoc: Doc | null; }; + +export type MutationIssue = { + fieldPath: string; + message: string; +}; + +export type MutationResponse = { + success?: { + message: string; + } + error?: { + issues: MutationIssue[]; + } +}; diff --git a/convex/_model/tournamentCompetitors/_helpers/getAvailableActions.ts b/convex/_model/tournamentCompetitors/_helpers/getAvailableActions.ts index ecc50149..86036d29 100644 --- a/convex/_model/tournamentCompetitors/_helpers/getAvailableActions.ts +++ b/convex/_model/tournamentCompetitors/_helpers/getAvailableActions.ts @@ -47,6 +47,9 @@ export const getAvailableActions = async ( if (!tournament) { return []; } + const tournamentRegistrations = await ctx.db.query('tournamentRegistrations') + .withIndex('by_tournament_competitor', (q) => q.eq('tournamentCompetitorId', doc._id)) + .collect(); // --- CHECK AUTH ---- const userId = await getAuthUserId(ctx); @@ -55,6 +58,7 @@ export const getAvailableActions = async ( const isPlayer = await checkUserIsRegistered(ctx, tournament._id, userId); const isTeamTournament = tournament.competitorSize > 1; const isCaptain = isTeamTournament && userId && doc.captainUserId === userId; + const isTeamPlayer = tournamentRegistrations.find((r) => r.userId === userId); const hasCurrentRound = tournament.currentRound !== undefined; // ---- PRIMARY ACTIONS ---- @@ -78,7 +82,7 @@ export const getAvailableActions = async ( actions.push(TournamentCompetitorActionKey.Edit); } - if ((isOrganizer || isCaptain) && tournament.status !== 'active') { + if ((isOrganizer || isCaptain) && tournament.status === 'published') { actions.push(TournamentCompetitorActionKey.Delete); } @@ -87,7 +91,7 @@ export const getAvailableActions = async ( actions.push(TournamentCompetitorActionKey.Join); } - if (isPlayer && tournament.status === 'published' && isTeamTournament) { + if (isTeamPlayer && tournament.status === 'published') { actions.push(TournamentCompetitorActionKey.Leave); } diff --git a/convex/_model/tournamentCompetitors/_helpers/getDisplayName.ts b/convex/_model/tournamentCompetitors/_helpers/getDisplayName.ts index 46a61342..3bc78174 100644 --- a/convex/_model/tournamentCompetitors/_helpers/getDisplayName.ts +++ b/convex/_model/tournamentCompetitors/_helpers/getDisplayName.ts @@ -8,9 +8,13 @@ import { getTournamentRegistrationsByCompetitor } from '../../tournamentRegistra export const getDisplayName = async ( ctx: QueryCtx, - doc: Doc<'tournamentCompetitors'>, + doc?: Doc<'tournamentCompetitors'> | null, ): Promise => { - const fallBack = 'Unknown Competitor'; + const fallBack = 'Unknown Competitor'; // TODO: Language support + + if (!doc) { + return fallBack; + } const activeRegistrations = await getTournamentRegistrationsByCompetitor(ctx, { tournamentCompetitorId: doc._id, diff --git a/convex/_model/tournamentRegistrations/_helpers/getAvailableActions.ts b/convex/_model/tournamentRegistrations/_helpers/getAvailableActions.ts index 1c088b61..fe24172f 100644 --- a/convex/_model/tournamentRegistrations/_helpers/getAvailableActions.ts +++ b/convex/_model/tournamentRegistrations/_helpers/getAvailableActions.ts @@ -76,11 +76,11 @@ export const getAvailableActions = async ( // actions.push(TournamentRegistrationActionKey.ApproveList); // } - if ((isOrganizer || (isCaptain && !isSelf)) && tournament.status === 'published') { + if ((isOrganizer || isCaptain) && !isSelf && tournament.status === 'published' && isTeamTournament) { actions.push(TournamentRegistrationActionKey.Delete); } - if (isSelf && tournament.status === 'published') { + if (isSelf && tournament.status === 'published' && isTeamTournament) { actions.push(TournamentRegistrationActionKey.Leave); } @@ -92,7 +92,7 @@ export const getAvailableActions = async ( // actions.push(TournamentRegistrationActionKey.SubmitList); // } - if ((isOrganizer || isCaptain) && hasSparePlayers) { + if ((isOrganizer || isCaptain) && isTeamTournament && hasSparePlayers) { actions.push(TournamentRegistrationActionKey.ToggleActive); } diff --git a/convex/_model/tournamentRegistrations/_helpers/getCreateSuccessMessage.ts b/convex/_model/tournamentRegistrations/_helpers/getCreateSuccessMessage.ts new file mode 100644 index 00000000..e476da9c --- /dev/null +++ b/convex/_model/tournamentRegistrations/_helpers/getCreateSuccessMessage.ts @@ -0,0 +1,38 @@ +import { getAuthUserId } from '@convex-dev/auth/server'; + +import { Doc } from '../../../_generated/dataModel'; +import { MutationCtx } from '../../../_generated/server'; +import { getDisplayName as getTournamentCompetitorDisplayName } from '../../tournamentCompetitors'; +import { getDisplayName as getTournamentDisplayName } from '../../tournaments'; +import { getUser } from '../../users'; + +export const getCreateSuccessMessage = async ( + ctx: MutationCtx, + doc: Doc<'tournamentRegistrations'>, +): Promise => { + const currentUserId = await getAuthUserId(ctx); + const user = await getUser(ctx, { id: doc.userId }); + const tournament = await ctx.db.get(doc.tournamentId); + const tournamentCompetitor = await ctx.db.get(doc.tournamentCompetitorId); + + const isTeamTournament = (tournament?.competitorSize ?? 1) > 1; + const isSelf = currentUserId && currentUserId === user?._id; + + const tournamentName = getTournamentDisplayName(tournament); + const teamName = await getTournamentCompetitorDisplayName(ctx, tournamentCompetitor); + const userName = user?.displayName ?? 'Unknown User'; + + if (isSelf) { + if (isTeamTournament) { + return `You have joined ${tournamentName} on ${teamName}!`; + } else { + return `You have joined ${tournamentName}!`; + } + } else { + if (isTeamTournament) { + return `${userName} has been added to ${tournamentName} on ${teamName}!`; + } else { + return `${userName} has been added to ${tournamentName}!`; + } + } +}; diff --git a/convex/_model/tournamentRegistrations/mutations/createTournamentRegistration.ts b/convex/_model/tournamentRegistrations/mutations/createTournamentRegistration.ts index e85832ec..7475ecc3 100644 --- a/convex/_model/tournamentRegistrations/mutations/createTournamentRegistration.ts +++ b/convex/_model/tournamentRegistrations/mutations/createTournamentRegistration.ts @@ -7,34 +7,33 @@ import { import { MutationCtx } from '../../../_generated/server'; import { checkAuth } from '../../common/_helpers/checkAuth'; import { getErrorMessage } from '../../common/errors'; +import { MutationResponse } from '../../common/types'; import { VisibilityLevel } from '../../common/VisibilityLevel'; import { getTournamentOrganizersByTournament } from '../../tournamentOrganizers'; import { checkUserIsRegistered } from '../_helpers/checkUserIsRegistered'; -import { deepenTournamentRegistration } from '../_helpers/deepenTournamentRegistration'; -import { editableFields } from '../table'; -import { TournamentRegistration } from '../types'; - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const { tournamentCompetitorId, ...restFields } = editableFields; +import { getCreateSuccessMessage } from '../_helpers/getCreateSuccessMessage'; export const createTournamentRegistrationArgs = v.object({ - ...restFields, + userId: v.id('users'), + tournamentId: v.id('tournaments'), tournamentCompetitorId: v.optional(v.id('tournamentCompetitors')), tournamentCompetitor: v.optional(v.object({ teamName: v.optional(v.string()), })), + nameVisibilityConsent: v.optional(v.boolean()), }); export const createTournamentRegistration = async ( ctx: MutationCtx, args: Infer, -): Promise => { +): Promise => { // --- CHECK AUTH ---- /* These user IDs can make changes to this tournament registration: * - Tournament organizers; * - The user themselves; */ const currentUserId = await checkAuth(ctx); + const currentUser = await ctx.db.get(currentUserId); const tournamentOrganizers = await getTournamentOrganizersByTournament(ctx, { tournamentId: args.tournamentId, }); @@ -42,11 +41,15 @@ export const createTournamentRegistration = async ( ...tournamentOrganizers.map((r) => r.userId), args.userId, ]; - if (!authorizedUserIds.includes(currentUserId)) { + if (!currentUser || !authorizedUserIds.includes(currentUserId)) { throw new ConvexError(getErrorMessage('USER_DOES_NOT_HAVE_PERMISSION')); } // ---- VALIDATE ---- + const user = await ctx.db.get(args.userId); + if (!user) { + throw new ConvexError(getErrorMessage('USER_NOT_FOUND')); + } const tournament = await ctx.db.get(args.tournamentId); if (!tournament) { throw new ConvexError(getErrorMessage('TOURNAMENT_NOT_FOUND')); @@ -67,6 +70,9 @@ export const createTournamentRegistration = async ( if (args.tournamentCompetitorId && args.tournamentCompetitor?.teamName?.length) { throw new ConvexError(getErrorMessage('CANNOT_CREATE_REGISTRATION_WITH_COMPETITOR_NAME_ID')); } + if (tournament.requireRealNames && currentUser.nameVisibility < VisibilityLevel.Tournaments && !args.nameVisibilityConsent) { + throw new ConvexError(getErrorMessage('CANNOT_CREATE_REGISTRATION_WITHOUT_REAL_NAME')); + } // ---- PRIMARY ACTIONS ---- let tournamentCompetitorId = args.tournamentCompetitorId; @@ -94,18 +100,20 @@ export const createTournamentRegistration = async ( userId: args.userId, }); - // Force user's name visibility to match tournament requirement: - if (tournament.requireRealNames) { - const user = await ctx.db.get(args.userId); - if (!user) { - throw new ConvexError(getErrorMessage('USER_NOT_FOUND')); - } - if (user.nameVisibility < VisibilityLevel.Tournaments && currentUserId === args.userId) { - await ctx.db.patch(args.userId, { - nameVisibility: VisibilityLevel.Tournaments, - }); - } + // Update user's name visibility if consent given: + const consentRequired = tournament.requireRealNames && user.nameVisibility < VisibilityLevel.Tournaments; + const consentGranted = args.nameVisibilityConsent && currentUser._id === user._id; + if (consentRequired && consentGranted) { + await ctx.db.patch(args.userId, { + nameVisibility: VisibilityLevel.Tournaments, + }); } - return await deepenTournamentRegistration(ctx, (await ctx.db.get(tournamentRegistrationId))!); + const result = await ctx.db.get(tournamentRegistrationId); + const message = await getCreateSuccessMessage(ctx, result!); + return { + success: { + message, + }, + }; }; diff --git a/convex/_model/tournaments/_helpers/getDisplayName.ts b/convex/_model/tournaments/_helpers/getDisplayName.ts index 4910a7f5..aa14cdb6 100644 --- a/convex/_model/tournaments/_helpers/getDisplayName.ts +++ b/convex/_model/tournaments/_helpers/getDisplayName.ts @@ -1,8 +1,11 @@ import { Doc } from '../../../_generated/dataModel'; export const getDisplayName = ( - doc: Doc<'tournaments'>, + doc?: Doc<'tournaments'> | null, ): string => { + if (!doc) { + return 'Unknown Tournament'; // TODO: Language support + } if (doc.editionYear) { return `${doc.title} ${doc.editionYear}`; } diff --git a/convex/_model/users/_helpers/redactUser.ts b/convex/_model/users/_helpers/redactUser.ts index beed6fdd..ab9565c2 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 { VisibilityLevel } from '../../common/VisibilityLevel'; import { checkUserRelationshipLevel } from './checkUserRelationshipLevel'; import { formatUserRealName } from './formatUserRealName'; @@ -13,6 +14,8 @@ export type LimitedUser = Pick, '_id' | 'username'> & { avatarUrl?: string; countryCode?: string; displayName: string; + locationVisibility: VisibilityLevel; + nameVisibility: VisibilityLevel; }; /** @@ -53,7 +56,9 @@ export const redactUser = async ( return { ...restFields, avatarUrl, - displayName: nameVisible ? formatUserRealName(user) : user.username ?? 'Ghost', countryCode: locationVisible ? user.countryCode : undefined, + displayName: nameVisible ? formatUserRealName(user) : user.username ?? 'Ghost', + locationVisibility: user.locationVisibility, + nameVisibility: user.nameVisibility, }; }; diff --git a/package-lock.json b/package-lock.json index 6c00ec74..c8f49bea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@dnd-kit/sortable": "^10.0.0", "@fontsource/figtree": "^5.1.0", "@hookform/resolvers": "^3.9.0", - "@ianpaschal/combat-command-components": "^1.6.1", + "@ianpaschal/combat-command-components": "file:../combat-command-components/ianpaschal-combat-command-components-1.7.1.tgz", "@ianpaschal/combat-command-game-systems": "^1.1.4", "@mapbox/search-js-core": "^1.0.0-beta.25", "@radix-ui/colors": "^3.0.0", @@ -72,7 +72,9 @@ "@stylistic/eslint-plugin-ts": "^2.3.0", "@stylistic/stylelint-config": "^2.0.0", "@stylistic/stylelint-plugin": "^3.1.0", + "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.2.0", + "@testing-library/user-event": "^14.6.1", "@types/image-blob-reduce": "^4.1.4", "@types/luxon": "^3.4.2", "@types/node": "^22.13.5", @@ -108,6 +110,13 @@ "vitest": "^3.0.9" } }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -1486,9 +1495,9 @@ "license": "BSD-3-Clause" }, "node_modules/@ianpaschal/combat-command-components": { - "version": "1.6.1", - "resolved": "https://npm.pkg.github.com/download/@ianpaschal/combat-command-components/1.6.1/464caa4910f2c6cd9503846cf47eeb2cb0219b61", - "integrity": "sha512-wDGcisNn0uzjRD7A9LNNsaU3JRPqjq1UIsPr5zWEoC2wRsfBlDcF3oKljeGGqHh/1f4Ul+EBmaALKU/RbrX9oA==", + "version": "1.7.1", + "resolved": "file:../combat-command-components/ianpaschal-combat-command-components-1.7.1.tgz", + "integrity": "sha512-DG4TpIkDDtvdJf7lX6y/lt2Utoc4IQpJDC7H1M/LXrlz0vNLNM9ZX9AF52q66lSwnN8CuhfEDlR0Vy4RsvJqMg==", "license": "MIT", "dependencies": { "@base-ui/react": "^1.0.0", @@ -5027,6 +5036,33 @@ "node": ">=18" } }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, "node_modules/@testing-library/react": { "version": "16.3.0", "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", @@ -5055,6 +5091,20 @@ } } }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -6058,7 +6108,6 @@ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "dequal": "^2.0.3" } @@ -6867,6 +6916,13 @@ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -7110,7 +7166,6 @@ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -9152,6 +9207,16 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -10472,6 +10537,16 @@ "node": ">= 0.6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -11890,6 +11965,20 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -12630,6 +12719,19 @@ "node": ">=8" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -13043,6 +13145,7 @@ "integrity": "sha512-v3YCf31atbwJQIMtPNX8hcQ+okD4NQaTuKGUWfII8eaqn+3otrbttGL1zSMZAAtiPsBztQnujVBugg/cXFUpyg==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { "@adobe/css-tools": "~4.3.1", "debug": "^4.3.2", @@ -13065,14 +13168,16 @@ "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.3.tgz", "integrity": "sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/stylus/node_modules/sax": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==", "dev": true, - "license": "ISC" + "license": "ISC", + "optional": true }, "node_modules/stylus/node_modules/source-map": { "version": "0.7.4", @@ -13080,6 +13185,7 @@ "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", "dev": true, "license": "BSD-3-Clause", + "optional": true, "engines": { "node": ">= 8" } @@ -13597,9 +13703,9 @@ } }, "node_modules/typescript-plugin-css-modules": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/typescript-plugin-css-modules/-/typescript-plugin-css-modules-5.1.0.tgz", - "integrity": "sha512-6h+sLBa4l+XYSTn/31vZHd/1c3SvAbLpobY6FxDiUOHJQG1eD9Gh3eCs12+Eqc+TCOAdxcO+zAPvUq0jBfdciw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typescript-plugin-css-modules/-/typescript-plugin-css-modules-5.2.0.tgz", + "integrity": "sha512-c5pAU5d+m3GciDr/WhkFldz1NIEGBafuP/3xhFt9BEXS2gmn/LvjkoZ11vEBIuP8LkXfPNhOt1BUhM5efFuwOw==", "dev": true, "license": "MIT", "dependencies": { @@ -13617,9 +13723,11 @@ "reserved-words": "^0.1.2", "sass": "^1.70.0", "source-map-js": "^1.0.2", - "stylus": "^0.62.0", "tsconfig-paths": "^4.2.0" }, + "optionalDependencies": { + "stylus": "^0.62.0" + }, "peerDependencies": { "typescript": ">=4.0.0" } diff --git a/package.json b/package.json index b052db3e..cd16a7e6 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "@dnd-kit/sortable": "^10.0.0", "@fontsource/figtree": "^5.1.0", "@hookform/resolvers": "^3.9.0", - "@ianpaschal/combat-command-components": "^1.6.1", + "@ianpaschal/combat-command-components": "file:../combat-command-components/ianpaschal-combat-command-components-1.7.1.tgz", "@ianpaschal/combat-command-game-systems": "^1.1.4", "@mapbox/search-js-core": "^1.0.0-beta.25", "@radix-ui/colors": "^3.0.0", @@ -83,7 +83,9 @@ "@stylistic/eslint-plugin-ts": "^2.3.0", "@stylistic/stylelint-config": "^2.0.0", "@stylistic/stylelint-plugin": "^3.1.0", + "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.2.0", + "@testing-library/user-event": "^14.6.1", "@types/image-blob-reduce": "^4.1.4", "@types/luxon": "^3.4.2", "@types/node": "^22.13.5", diff --git a/src/components/ContextMenu/ContextMenu.types.ts b/src/components/ContextMenu/ContextMenu.types.ts index b699fd0b..89f5db29 100644 --- a/src/components/ContextMenu/ContextMenu.types.ts +++ b/src/components/ContextMenu/ContextMenu.types.ts @@ -16,6 +16,8 @@ export type Action = { export type ActionKey = TournamentActionKey | TournamentCompetitorActionKey | TournamentRegistrationActionKey; -export type ActionDefinition = Action & { +export type ActionDefinition = Action & { key: T; }; + +export type ActionOverride = Omit, 'handler'>; diff --git a/src/components/ContextMenu/index.ts b/src/components/ContextMenu/index.ts index 56946e75..6f8c955d 100644 --- a/src/components/ContextMenu/index.ts +++ b/src/components/ContextMenu/index.ts @@ -4,4 +4,6 @@ export { } from './ContextMenu'; export type { Action, + ActionDefinition, + ActionKey, } from './ContextMenu.types'; diff --git a/src/components/InputSingleFile/InputSingleFile.tsx b/src/components/InputSingleFile/InputSingleFile.tsx index 34f593ff..af20386a 100644 --- a/src/components/InputSingleFile/InputSingleFile.tsx +++ b/src/components/InputSingleFile/InputSingleFile.tsx @@ -16,7 +16,6 @@ interface InputSingleFileProps { onChange?: (value: StorageId) => void; onReset?: (name: string) => void; value?: StorageId; - hasError?: boolean; } export const InputSingleFile = forwardRef(({ @@ -26,8 +25,6 @@ export const InputSingleFile = forwardRef( onChange, onReset, value, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - hasError = false, ...props }, ref): JSX.Element => { const { data: fileUrl } = useGetFileUrl(value ? { id: value } : 'skip'); diff --git a/src/components/InputUser/InputUser.module.scss b/src/components/InputUser/InputUser.module.scss index 7b859e9b..9f42c36e 100644 --- a/src/components/InputUser/InputUser.module.scss +++ b/src/components/InputUser/InputUser.module.scss @@ -8,21 +8,36 @@ @use "/src/style/sizes"; .InputUser { + @include flex.row($gap: 0); + &_Trigger { @include flex.row; @include variants.outlined; @include corners.normal; cursor: pointer; + + flex: 1 auto; + padding: 0.75rem; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + // TODO: Add same focus as component library // &:focus { // } } &_Clear { - margin-left: auto; + align-self: stretch !important; + + height: auto !important; + max-height: 100% !important; + + border-left: none !important; + border-top-left-radius: 0; + border-bottom-left-radius: 0; } &_Popup { diff --git a/src/components/InputUser/InputUser.tsx b/src/components/InputUser/InputUser.tsx index 01f733b0..a9c62ea2 100644 --- a/src/components/InputUser/InputUser.tsx +++ b/src/components/InputUser/InputUser.tsx @@ -32,6 +32,7 @@ export interface InputUserProps { excludeIds?: UserId[]; loading?: boolean; disabled?: boolean; + id?: string; } export const InputUser = forwardRef(({ @@ -42,6 +43,7 @@ export const InputUser = forwardRef(({ value: controlledValue, loading = false, disabled = false, + id, }, ref): JSX.Element => { // Refs: @@ -129,89 +131,90 @@ export const InputUser = forwardRef(({ }; return ( - - } - nativeButton={false} - ref={ref} - > - {value ? ( - - ) : ( - , displayName: 'Select a user...' }} - /> - )} - {value && ( -