From ff56b43c14bd129356fa0861b0d9e21c7e3ac790 Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Sun, 16 Nov 2025 17:09:50 +0100 Subject: [PATCH 01/16] WIP --- .claude/settings.json | 12 + .eslintrc.cjs | 1 + convex/_generated/api.d.ts | 50 +++- .../common/_helpers/filterWithSearchTerm.ts | 25 +- convex/_model/common/_helpers/getDocStrict.ts | 27 ++ .../_model/files/actions/convertImageToPdf.ts | 55 ++++ .../files/actions/getFileDownloadUrl.ts | 29 ++ convex/_model/files/index.ts | 12 + .../_model/files/queries/getFileMetadata.ts | 29 ++ .../flamesOfWarV4/_helpers/deepenListData.ts | 17 -- .../flamesOfWarV4/_model/listData.ts | 32 --- .../battlefront/flamesOfWarV4/index.ts | 9 - convex/_model/gameSystems/index.ts | 1 - .../listChecks/_helpers/deepenListCheck.ts | 21 ++ convex/_model/listChecks/index.ts | 5 + .../listChecks/mutations/createListCheck.ts | 18 ++ convex/_model/listChecks/table.ts | 25 ++ .../_helpers/checkListSubmittedOnTime.ts | 18 ++ .../lists/_helpers/checkListVisibility.ts | 32 +++ convex/_model/lists/_helpers/deepenList.ts | 36 ++- .../lists/_helpers/getAvailableActions.ts | 65 +++++ convex/_model/lists/index.ts | 32 ++- convex/_model/lists/mutations/createList.ts | 31 +++ convex/_model/lists/mutations/deleteList.ts | 14 + .../_model/lists/mutations/importListData.ts | 51 ---- convex/_model/lists/mutations/updateList.ts | 39 +++ convex/_model/lists/queries/getList.ts | 15 +- .../getListsByTournamentRegistration.ts | 27 ++ convex/_model/lists/queries/getListsByUser.ts | 27 ++ convex/_model/lists/table.ts | 17 +- convex/_model/lists/types.ts | 13 + .../matchResults/queries/getMatchResults.ts | 5 + .../_helpers/checkUserIsTeamCaptain.ts | 19 ++ .../_helpers/checkUsersAreTeammates.ts | 23 ++ .../_helpers/getAvailableActions.ts | 2 +- convex/_model/tournamentCompetitors/index.ts | 3 + .../_helpers/deepenTournamentRegistration.ts | 10 +- .../_helpers/getAvailableActions.ts | 9 +- .../_helpers/getAvailableActions.ts | 2 +- .../actions/exportFowV4TournamentMatchData.ts | 10 +- convex/_model/tournaments/table.ts | 1 + convex/files.ts | 25 +- convex/listChecks.ts | 7 + convex/lists.ts | 28 +- convex/scheduledTasks.ts | 6 + convex/schema.ts | 2 + package-lock.json | 254 +++++++++++++----- package.json | 3 +- src/api.ts | 7 + .../ContextMenu/ContextMenu.types.ts | 6 +- .../FileButton/FileButton.module.scss | 5 + src/components/FileButton/FileButton.tsx | 35 +++ src/components/FileButton/index.ts | 4 + .../ListCheckForm/ListCheckForm.module.scss | 56 ++++ .../ListCheckForm/ListCheckForm.schema.ts | 29 ++ .../ListCheckForm/ListCheckForm.tsx | 83 ++++++ src/components/ListCheckForm/index.ts | 7 + .../ListCreateDialog.hooks.tsx | 4 + .../ListCreateDialog.module.scss | 5 + .../ListCreateDialog/ListCreateDialog.tsx | 129 +++++++++ src/components/ListCreateDialog/index.ts | 2 + .../ListDetails/ListDetails.module.scss | 16 ++ src/components/ListDetails/ListDetails.tsx | 66 +++++ .../ListDetails/ListDetailsButton.tsx | 35 +++ .../ListDetails/ListDetailsTrigger.tsx | 86 ++++++ src/components/ListDetails/index.ts | 4 + src/components/ListForm/ListForm.module.scss | 56 ++++ src/components/ListForm/ListForm.schema.ts | 102 +++++++ src/components/ListForm/ListForm.tsx | 170 ++++++++++++ src/components/ListForm/index.ts | 7 + .../ListProvider/ListContextMenu.tsx | 22 ++ .../ListProvider/ListProvider.context.tsx | 7 + .../ListProvider/ListProvider.hooks.tsx | 29 ++ src/components/ListProvider/ListProvider.tsx | 18 ++ .../actions/useCreateListCheckAction.tsx | 46 ++++ .../ListProvider/actions/useDeleteAction.tsx | 41 +++ .../actions/useDownloadAction.tsx | 43 +++ .../ListProvider/actions/useEditAction.tsx | 57 ++++ src/components/ListProvider/index.ts | 16 ++ .../ListRenderer/ListRenderer.module.scss | 29 ++ src/components/ListRenderer/ListRenderer.tsx | 34 +++ src/components/ListRenderer/index.ts | 4 + .../ManageListButton/ManageListButton.tsx | 119 ++++++++ src/components/ManageListButton/index.ts | 1 + .../TournamentRegistrationForm.schema.ts | 9 +- .../TournamentRegistrationContextMenu.tsx | 1 + .../TournamentRegistrationProvider.hooks.ts | 2 + .../actions/useCreateListAction.tsx | 55 ++++ .../TournamentRegistrationProvider/index.ts | 1 + src/pages/ListDetailPage/ListDetailPage.tsx | 24 ++ src/pages/ListDetailPage/index.ts | 1 + ...urnamentRegistrationListButton.module.scss | 5 + .../TournamentRegistrationListButton.tsx | 41 +++ .../TournamentRegistrationListButton/index.ts | 2 + .../TournamentRegistrationsTable.tsx | 18 +- .../TournamentDetailPage.tsx | 3 +- src/services/files.ts | 64 ++++- src/services/listChecks.ts | 5 + src/services/lists.ts | 11 + 99 files changed, 2537 insertions(+), 279 deletions(-) create mode 100644 .claude/settings.json create mode 100644 convex/_model/common/_helpers/getDocStrict.ts create mode 100644 convex/_model/files/actions/convertImageToPdf.ts create mode 100644 convex/_model/files/actions/getFileDownloadUrl.ts create mode 100644 convex/_model/files/queries/getFileMetadata.ts delete mode 100644 convex/_model/gameSystems/battlefront/flamesOfWarV4/_helpers/deepenListData.ts delete mode 100644 convex/_model/gameSystems/battlefront/flamesOfWarV4/_model/listData.ts delete mode 100644 convex/_model/gameSystems/battlefront/flamesOfWarV4/index.ts delete mode 100644 convex/_model/gameSystems/index.ts create mode 100644 convex/_model/listChecks/_helpers/deepenListCheck.ts create mode 100644 convex/_model/listChecks/index.ts create mode 100644 convex/_model/listChecks/mutations/createListCheck.ts create mode 100644 convex/_model/listChecks/table.ts create mode 100644 convex/_model/lists/_helpers/checkListSubmittedOnTime.ts create mode 100644 convex/_model/lists/_helpers/checkListVisibility.ts create mode 100644 convex/_model/lists/_helpers/getAvailableActions.ts create mode 100644 convex/_model/lists/mutations/createList.ts create mode 100644 convex/_model/lists/mutations/deleteList.ts delete mode 100644 convex/_model/lists/mutations/importListData.ts create mode 100644 convex/_model/lists/mutations/updateList.ts create mode 100644 convex/_model/lists/queries/getListsByTournamentRegistration.ts create mode 100644 convex/_model/lists/queries/getListsByUser.ts create mode 100644 convex/_model/lists/types.ts create mode 100644 convex/_model/tournamentCompetitors/_helpers/checkUserIsTeamCaptain.ts create mode 100644 convex/_model/tournamentCompetitors/_helpers/checkUsersAreTeammates.ts create mode 100644 convex/listChecks.ts create mode 100644 src/components/FileButton/FileButton.module.scss create mode 100644 src/components/FileButton/FileButton.tsx create mode 100644 src/components/FileButton/index.ts create mode 100644 src/components/ListCheckForm/ListCheckForm.module.scss create mode 100644 src/components/ListCheckForm/ListCheckForm.schema.ts create mode 100644 src/components/ListCheckForm/ListCheckForm.tsx create mode 100644 src/components/ListCheckForm/index.ts create mode 100644 src/components/ListCreateDialog/ListCreateDialog.hooks.tsx create mode 100644 src/components/ListCreateDialog/ListCreateDialog.module.scss create mode 100644 src/components/ListCreateDialog/ListCreateDialog.tsx create mode 100644 src/components/ListCreateDialog/index.ts create mode 100644 src/components/ListDetails/ListDetails.module.scss create mode 100644 src/components/ListDetails/ListDetails.tsx create mode 100644 src/components/ListDetails/ListDetailsButton.tsx create mode 100644 src/components/ListDetails/ListDetailsTrigger.tsx create mode 100644 src/components/ListDetails/index.ts create mode 100644 src/components/ListForm/ListForm.module.scss create mode 100644 src/components/ListForm/ListForm.schema.ts create mode 100644 src/components/ListForm/ListForm.tsx create mode 100644 src/components/ListForm/index.ts create mode 100644 src/components/ListProvider/ListContextMenu.tsx create mode 100644 src/components/ListProvider/ListProvider.context.tsx create mode 100644 src/components/ListProvider/ListProvider.hooks.tsx create mode 100644 src/components/ListProvider/ListProvider.tsx create mode 100644 src/components/ListProvider/actions/useCreateListCheckAction.tsx create mode 100644 src/components/ListProvider/actions/useDeleteAction.tsx create mode 100644 src/components/ListProvider/actions/useDownloadAction.tsx create mode 100644 src/components/ListProvider/actions/useEditAction.tsx create mode 100644 src/components/ListProvider/index.ts create mode 100644 src/components/ListRenderer/ListRenderer.module.scss create mode 100644 src/components/ListRenderer/ListRenderer.tsx create mode 100644 src/components/ListRenderer/index.ts create mode 100644 src/components/ManageListButton/ManageListButton.tsx create mode 100644 src/components/ManageListButton/index.ts create mode 100644 src/components/TournamentRegistrationProvider/actions/useCreateListAction.tsx create mode 100644 src/pages/ListDetailPage/ListDetailPage.tsx create mode 100644 src/pages/ListDetailPage/index.ts create mode 100644 src/pages/TournamentCompetitorDetailPage/components/TournamentRegistrationListButton/TournamentRegistrationListButton.module.scss create mode 100644 src/pages/TournamentCompetitorDetailPage/components/TournamentRegistrationListButton/TournamentRegistrationListButton.tsx create mode 100644 src/pages/TournamentCompetitorDetailPage/components/TournamentRegistrationListButton/index.ts create mode 100644 src/services/listChecks.ts create mode 100644 src/services/lists.ts diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000..58b94b31 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,12 @@ +{ + "permissions": { + "allow": [ + "Read(//Users/ian/Code/combat-command-components/src/components/Wizard/**)", + "Read(//Users/ian/Code/combat-command-components/**)", + "Bash(npm run lint:*)" + ], + "additionalDirectories": [ + "/Users/ian/Code/combat-command-components/src/components/Wizard" + ] + } +} diff --git a/.eslintrc.cjs b/.eslintrc.cjs index e3234f9e..6c8ac094 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -49,6 +49,7 @@ module.exports = { "arrow-body-style": ["error", "as-needed"], "no-console": ["warn", { allow: ["warn", "error", "info"] }], "@typescript-eslint//explicit-function-return-type": "off", + "implicit-arrow-linebreak": ["error", "beside"], // Plugin configurations 'import-newlines/enforce': ['error', 2], diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index 5d6f6e61..a125de80 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -21,6 +21,7 @@ import type * as _model_common__helpers_gameSystem_createSortByRanking from "../ 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_getDocStrict from "../_model/common/_helpers/getDocStrict.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"; @@ -41,7 +42,10 @@ import type * as _model_common_themes from "../_model/common/themes.js"; import type * as _model_common_tournamentPairingConfig from "../_model/common/tournamentPairingConfig.js"; import type * as _model_common_tournamentStatus from "../_model/common/tournamentStatus.js"; import type * as _model_common_types from "../_model/common/types.js"; +import type * as _model_files_actions_convertImageToPdf from "../_model/files/actions/convertImageToPdf.js"; +import type * as _model_files_actions_getFileDownloadUrl from "../_model/files/actions/getFileDownloadUrl.js"; import type * as _model_files_index from "../_model/files/index.js"; +import type * as _model_files_queries_getFileMetadata from "../_model/files/queries/getFileMetadata.js"; import type * as _model_files_queries_getFileUrl from "../_model/files/queries/getFileUrl.js"; import type * as _model_friendships__helpers_deepenFriendship from "../_model/friendships/_helpers/deepenFriendship.js"; import type * as _model_friendships_index from "../_model/friendships/index.js"; @@ -51,10 +55,6 @@ import type * as _model_friendships_mutations_deleteFriendship from "../_model/f import type * as _model_friendships_queries_getFriendship from "../_model/friendships/queries/getFriendship.js"; import type * as _model_friendships_queries_getFriendshipsByUser from "../_model/friendships/queries/getFriendshipsByUser.js"; import type * as _model_friendships_table from "../_model/friendships/table.js"; -import type * as _model_gameSystems_battlefront_flamesOfWarV4__helpers_deepenListData from "../_model/gameSystems/battlefront/flamesOfWarV4/_helpers/deepenListData.js"; -import type * as _model_gameSystems_battlefront_flamesOfWarV4__model_listData from "../_model/gameSystems/battlefront/flamesOfWarV4/_model/listData.js"; -import type * as _model_gameSystems_battlefront_flamesOfWarV4_index from "../_model/gameSystems/battlefront/flamesOfWarV4/index.js"; -import type * as _model_gameSystems_index from "../_model/gameSystems/index.js"; import type * as _model_leagueOrganizers__helpers_checkUserIsLeagueOrganizer from "../_model/leagueOrganizers/_helpers/checkUserIsLeagueOrganizer.js"; import type * as _model_leagueOrganizers__helpers_deepenLeagueOrganizer from "../_model/leagueOrganizers/_helpers/deepenLeagueOrganizer.js"; import type * as _model_leagueOrganizers_index from "../_model/leagueOrganizers/index.js"; @@ -80,11 +80,23 @@ import type * as _model_leagues_queries_getLeague from "../_model/leagues/querie import type * as _model_leagues_queries_getLeagues from "../_model/leagues/queries/getLeagues.js"; import type * as _model_leagues_table from "../_model/leagues/table.js"; import type * as _model_leagues_types from "../_model/leagues/types.js"; +import type * as _model_listChecks__helpers_deepenListCheck from "../_model/listChecks/_helpers/deepenListCheck.js"; +import type * as _model_listChecks_index from "../_model/listChecks/index.js"; +import type * as _model_listChecks_mutations_createListCheck from "../_model/listChecks/mutations/createListCheck.js"; +import type * as _model_listChecks_table from "../_model/listChecks/table.js"; +import type * as _model_lists__helpers_checkListSubmittedOnTime from "../_model/lists/_helpers/checkListSubmittedOnTime.js"; +import type * as _model_lists__helpers_checkListVisibility from "../_model/lists/_helpers/checkListVisibility.js"; import type * as _model_lists__helpers_deepenList from "../_model/lists/_helpers/deepenList.js"; +import type * as _model_lists__helpers_getAvailableActions from "../_model/lists/_helpers/getAvailableActions.js"; import type * as _model_lists_index from "../_model/lists/index.js"; -import type * as _model_lists_mutations_importListData from "../_model/lists/mutations/importListData.js"; +import type * as _model_lists_mutations_createList from "../_model/lists/mutations/createList.js"; +import type * as _model_lists_mutations_deleteList from "../_model/lists/mutations/deleteList.js"; +import type * as _model_lists_mutations_updateList from "../_model/lists/mutations/updateList.js"; import type * as _model_lists_queries_getList from "../_model/lists/queries/getList.js"; +import type * as _model_lists_queries_getListsByTournamentRegistration from "../_model/lists/queries/getListsByTournamentRegistration.js"; +import type * as _model_lists_queries_getListsByUser from "../_model/lists/queries/getListsByUser.js"; import type * as _model_lists_table from "../_model/lists/table.js"; +import type * as _model_lists_types from "../_model/lists/types.js"; import type * as _model_matchResultComments__helpers_deepenMatchResultComment from "../_model/matchResultComments/_helpers/deepenMatchResultComment.js"; import type * as _model_matchResultComments_index from "../_model/matchResultComments/index.js"; import type * as _model_matchResultComments_mutations_addMatchResultComment from "../_model/matchResultComments/mutations/addMatchResultComment.js"; @@ -122,6 +134,8 @@ import type * as _model_photos_index from "../_model/photos/index.js"; import type * as _model_photos_mutations_createPhoto from "../_model/photos/mutations/createPhoto.js"; 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_checkUserIsTeamCaptain from "../_model/tournamentCompetitors/_helpers/checkUserIsTeamCaptain.js"; +import type * as _model_tournamentCompetitors__helpers_checkUsersAreTeammates from "../_model/tournamentCompetitors/_helpers/checkUsersAreTeammates.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_getDetails from "../_model/tournamentCompetitors/_helpers/getDetails.js"; @@ -290,6 +304,7 @@ import type * as generateFileUploadUrl from "../generateFileUploadUrl.js"; import type * as http from "../http.js"; import type * as leagueRankings from "../leagueRankings.js"; import type * as leagues from "../leagues.js"; +import type * as listChecks from "../listChecks.js"; import type * as lists from "../lists.js"; import type * as matchResultComments from "../matchResultComments.js"; import type * as matchResultLikes from "../matchResultLikes.js"; @@ -335,6 +350,7 @@ declare const fullApi: ApiFromModules<{ "_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/getDocStrict": typeof _model_common__helpers_getDocStrict; "_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; @@ -355,7 +371,10 @@ declare const fullApi: ApiFromModules<{ "_model/common/tournamentPairingConfig": typeof _model_common_tournamentPairingConfig; "_model/common/tournamentStatus": typeof _model_common_tournamentStatus; "_model/common/types": typeof _model_common_types; + "_model/files/actions/convertImageToPdf": typeof _model_files_actions_convertImageToPdf; + "_model/files/actions/getFileDownloadUrl": typeof _model_files_actions_getFileDownloadUrl; "_model/files/index": typeof _model_files_index; + "_model/files/queries/getFileMetadata": typeof _model_files_queries_getFileMetadata; "_model/files/queries/getFileUrl": typeof _model_files_queries_getFileUrl; "_model/friendships/_helpers/deepenFriendship": typeof _model_friendships__helpers_deepenFriendship; "_model/friendships/index": typeof _model_friendships_index; @@ -365,10 +384,6 @@ declare const fullApi: ApiFromModules<{ "_model/friendships/queries/getFriendship": typeof _model_friendships_queries_getFriendship; "_model/friendships/queries/getFriendshipsByUser": typeof _model_friendships_queries_getFriendshipsByUser; "_model/friendships/table": typeof _model_friendships_table; - "_model/gameSystems/battlefront/flamesOfWarV4/_helpers/deepenListData": typeof _model_gameSystems_battlefront_flamesOfWarV4__helpers_deepenListData; - "_model/gameSystems/battlefront/flamesOfWarV4/_model/listData": typeof _model_gameSystems_battlefront_flamesOfWarV4__model_listData; - "_model/gameSystems/battlefront/flamesOfWarV4/index": typeof _model_gameSystems_battlefront_flamesOfWarV4_index; - "_model/gameSystems/index": typeof _model_gameSystems_index; "_model/leagueOrganizers/_helpers/checkUserIsLeagueOrganizer": typeof _model_leagueOrganizers__helpers_checkUserIsLeagueOrganizer; "_model/leagueOrganizers/_helpers/deepenLeagueOrganizer": typeof _model_leagueOrganizers__helpers_deepenLeagueOrganizer; "_model/leagueOrganizers/index": typeof _model_leagueOrganizers_index; @@ -394,11 +409,23 @@ declare const fullApi: ApiFromModules<{ "_model/leagues/queries/getLeagues": typeof _model_leagues_queries_getLeagues; "_model/leagues/table": typeof _model_leagues_table; "_model/leagues/types": typeof _model_leagues_types; + "_model/listChecks/_helpers/deepenListCheck": typeof _model_listChecks__helpers_deepenListCheck; + "_model/listChecks/index": typeof _model_listChecks_index; + "_model/listChecks/mutations/createListCheck": typeof _model_listChecks_mutations_createListCheck; + "_model/listChecks/table": typeof _model_listChecks_table; + "_model/lists/_helpers/checkListSubmittedOnTime": typeof _model_lists__helpers_checkListSubmittedOnTime; + "_model/lists/_helpers/checkListVisibility": typeof _model_lists__helpers_checkListVisibility; "_model/lists/_helpers/deepenList": typeof _model_lists__helpers_deepenList; + "_model/lists/_helpers/getAvailableActions": typeof _model_lists__helpers_getAvailableActions; "_model/lists/index": typeof _model_lists_index; - "_model/lists/mutations/importListData": typeof _model_lists_mutations_importListData; + "_model/lists/mutations/createList": typeof _model_lists_mutations_createList; + "_model/lists/mutations/deleteList": typeof _model_lists_mutations_deleteList; + "_model/lists/mutations/updateList": typeof _model_lists_mutations_updateList; "_model/lists/queries/getList": typeof _model_lists_queries_getList; + "_model/lists/queries/getListsByTournamentRegistration": typeof _model_lists_queries_getListsByTournamentRegistration; + "_model/lists/queries/getListsByUser": typeof _model_lists_queries_getListsByUser; "_model/lists/table": typeof _model_lists_table; + "_model/lists/types": typeof _model_lists_types; "_model/matchResultComments/_helpers/deepenMatchResultComment": typeof _model_matchResultComments__helpers_deepenMatchResultComment; "_model/matchResultComments/index": typeof _model_matchResultComments_index; "_model/matchResultComments/mutations/addMatchResultComment": typeof _model_matchResultComments_mutations_addMatchResultComment; @@ -436,6 +463,8 @@ declare const fullApi: ApiFromModules<{ "_model/photos/mutations/createPhoto": typeof _model_photos_mutations_createPhoto; "_model/photos/queries/getPhoto": typeof _model_photos_queries_getPhoto; "_model/photos/table": typeof _model_photos_table; + "_model/tournamentCompetitors/_helpers/checkUserIsTeamCaptain": typeof _model_tournamentCompetitors__helpers_checkUserIsTeamCaptain; + "_model/tournamentCompetitors/_helpers/checkUsersAreTeammates": typeof _model_tournamentCompetitors__helpers_checkUsersAreTeammates; "_model/tournamentCompetitors/_helpers/deepenTournamentCompetitor": typeof _model_tournamentCompetitors__helpers_deepenTournamentCompetitor; "_model/tournamentCompetitors/_helpers/getAvailableActions": typeof _model_tournamentCompetitors__helpers_getAvailableActions; "_model/tournamentCompetitors/_helpers/getDetails": typeof _model_tournamentCompetitors__helpers_getDetails; @@ -604,6 +633,7 @@ declare const fullApi: ApiFromModules<{ http: typeof http; leagueRankings: typeof leagueRankings; leagues: typeof leagues; + listChecks: typeof listChecks; lists: typeof lists; matchResultComments: typeof matchResultComments; matchResultLikes: typeof matchResultLikes; diff --git a/convex/_model/common/_helpers/filterWithSearchTerm.ts b/convex/_model/common/_helpers/filterWithSearchTerm.ts index b5ff922f..c94dc0fe 100644 --- a/convex/_model/common/_helpers/filterWithSearchTerm.ts +++ b/convex/_model/common/_helpers/filterWithSearchTerm.ts @@ -7,18 +7,17 @@ export const filterWithSearchTerm = ( return items; } const tokens = searchTerm.trim().toLowerCase().split(' '); - return items.filter((item) => - fields.some((field) => { - const value = item[field]; - return tokens.some((token) => { - if (typeof value === 'string') { - return value.toLowerCase().includes(token); - } - if (typeof value === 'number') { - return value.toString().includes(token); - } - return false; - }); - }), + return items.filter((item) => fields.some((field) => { + const value = item[field]; + return tokens.some((token) => { + if (typeof value === 'string') { + return value.toLowerCase().includes(token); + } + if (typeof value === 'number') { + return value.toString().includes(token); + } + return false; + }); + }), ); }; diff --git a/convex/_model/common/_helpers/getDocStrict.ts b/convex/_model/common/_helpers/getDocStrict.ts new file mode 100644 index 00000000..55938320 --- /dev/null +++ b/convex/_model/common/_helpers/getDocStrict.ts @@ -0,0 +1,27 @@ +import { GenericDatabaseReader } from 'convex/server'; +import { ConvexError, GenericId } from 'convex/values'; + +import { + DataModel, + Doc, + TableNames, +} from '../../../_generated/dataModel'; + +/** + * Fetches a document by ID, throwing a `ConvexError` if it doesn't exist. + * + * @param ctx - A context object with a database reader. + * @param id - The ID of the document to fetch. + * @returns The document. + * @throws {ConvexError} If no document exists with the given ID. + */ +export const getDocStrict = async ( + ctx: { db: GenericDatabaseReader }, + id: GenericId, +): Promise> => { + const doc = await ctx.db.get(id); + if (!doc) { + throw new ConvexError({ message: `Document not found: ${id}`, code: 'DOCUMENT_NOT_FOUND' }); + } + return doc; +}; diff --git a/convex/_model/files/actions/convertImageToPdf.ts b/convex/_model/files/actions/convertImageToPdf.ts new file mode 100644 index 00000000..c6ea4745 --- /dev/null +++ b/convex/_model/files/actions/convertImageToPdf.ts @@ -0,0 +1,55 @@ +import { + ConvexError, + Infer, + v, +} from 'convex/values'; +import { PDFDocument } from 'pdf-lib'; + +import { Id } from '../../../_generated/dataModel'; +import { ActionCtx } from '../../../_generated/server'; + +const IMAGE_MIME_TYPES = ['image/png', 'image/jpeg', 'image/jpg'] as const; +type ImageMimeType = (typeof IMAGE_MIME_TYPES)[number]; + +const isImageMimeType = (mimeType: string): mimeType is ImageMimeType => (IMAGE_MIME_TYPES as ReadonlyArray).includes(mimeType); + +export const convertImageToPdfArgs = v.object({ + storageId: v.id('_storage'), + mimeType: v.string(), +}); + +export const convertImageToPdf = async ( + ctx: ActionCtx, + args: Infer, +): Promise> => { + if (!isImageMimeType(args.mimeType)) { + throw new ConvexError(`Unsupported MIME type: ${args.mimeType}. Expected one of: ${IMAGE_MIME_TYPES.join(', ')}`); + } + + const blob = await ctx.storage.get(args.storageId); + if (!blob) { + throw new ConvexError('File not found in storage'); + } + + const imageBytes = new Uint8Array(await blob.arrayBuffer()); + const pdfDoc = await PDFDocument.create(); + + const image = args.mimeType === 'image/png' ? await pdfDoc.embedPng(imageBytes) : await pdfDoc.embedJpg(imageBytes); + + const page = pdfDoc.addPage([image.width, image.height]); + page.drawImage(image, { + x: 0, + y: 0, + width: image.width, + height: image.height, + }); + + const pdfBytes = await pdfDoc.save(); + const pdfBuffer = pdfBytes.buffer as ArrayBuffer; + const pdfBlob = new Blob([pdfBuffer], { type: 'application/pdf' }); + const pdfStorageId = await ctx.storage.store(pdfBlob); + + await ctx.storage.delete(args.storageId); + + return pdfStorageId; +}; diff --git a/convex/_model/files/actions/getFileDownloadUrl.ts b/convex/_model/files/actions/getFileDownloadUrl.ts new file mode 100644 index 00000000..879118c5 --- /dev/null +++ b/convex/_model/files/actions/getFileDownloadUrl.ts @@ -0,0 +1,29 @@ +import { + ConvexError, + Infer, + v, +} from 'convex/values'; + +import { ActionCtx } from '../../../_generated/server'; +import { getErrorMessage } from '../../common/errors'; + +export const getFileDownloadDataArgs = v.object({ + id: v.id('_storage'), +}); + +export const getFileDownloadData = async ( + ctx: ActionCtx, + args: Infer, +): Promise => { + const blob = await ctx.storage.get(args.id); + if (!blob) { + throw new ConvexError(getErrorMessage('FILE_NOT_FOUND')); + } + const arrayBuffer = await blob.arrayBuffer(); + const bytes = new Uint8Array(arrayBuffer); + let binary = ''; + for (let i = 0; i < bytes.length; i++) { + binary += String.fromCharCode(bytes[i]); + } + return btoa(binary); +}; diff --git a/convex/_model/files/index.ts b/convex/_model/files/index.ts index 4ae03689..478aefd0 100644 --- a/convex/_model/files/index.ts +++ b/convex/_model/files/index.ts @@ -1,3 +1,15 @@ +export { + convertImageToPdf, + convertImageToPdfArgs, +} from './actions/convertImageToPdf'; +export { + getFileDownloadData, + getFileDownloadDataArgs, +} from './actions/getFileDownloadUrl'; +export { + getFileMetadata, + getFileMetadataArgs, +} from './queries/getFileMetadata'; export { getFileUrl, getFileUrlArgs, diff --git a/convex/_model/files/queries/getFileMetadata.ts b/convex/_model/files/queries/getFileMetadata.ts new file mode 100644 index 00000000..43f2fdc7 --- /dev/null +++ b/convex/_model/files/queries/getFileMetadata.ts @@ -0,0 +1,29 @@ +import { GenericDoc } from '@convex-dev/auth/server'; +import { SystemDataModel } from 'convex/server'; +import { + ConvexError, + Infer, + v, +} from 'convex/values'; + +import { QueryCtx } from '../../../_generated/server'; +import { getErrorMessage } from '../../common/errors'; + +export const getFileMetadataArgs = v.object({ + id: v.id('_storage'), +}); + +export const getFileMetadata = async ( + ctx: QueryCtx, + args: Infer, +): Promise & { url: string } | null> => { + const fileUrl = await ctx.storage.getUrl(args.id); + const fileMetadata = await ctx.db.system.get(args.id); + if (!fileMetadata || !fileUrl) { + throw new ConvexError(getErrorMessage('FILE_NOT_FOUND')); + } + return { + ...fileMetadata, + url: fileUrl, + }; +}; diff --git a/convex/_model/gameSystems/battlefront/flamesOfWarV4/_helpers/deepenListData.ts b/convex/_model/gameSystems/battlefront/flamesOfWarV4/_helpers/deepenListData.ts deleted file mode 100644 index 663cb67d..00000000 --- a/convex/_model/gameSystems/battlefront/flamesOfWarV4/_helpers/deepenListData.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { factions, forceDiagrams } from '@ianpaschal/combat-command-game-systems/flamesOfWarV4'; - -import { Doc } from '../../../../../_generated/dataModel'; - -export type DeepFowV4ListData = ReturnType; - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -export const deepenListData = ( - data: Doc<'lists'>['data'], -) => ({ - ...data, - meta: { - ...data.meta, - faction: forceDiagrams[data.meta.forceDiagram].faction, - alignment: factions[forceDiagrams[data.meta.forceDiagram].faction].alignment, - }, -}); diff --git a/convex/_model/gameSystems/battlefront/flamesOfWarV4/_model/listData.ts b/convex/_model/gameSystems/battlefront/flamesOfWarV4/_model/listData.ts deleted file mode 100644 index 3f9aef03..00000000 --- a/convex/_model/gameSystems/battlefront/flamesOfWarV4/_model/listData.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { ForceDiagram, Unit } from '@ianpaschal/combat-command-game-systems/flamesOfWarV4'; -import { Infer, v } from 'convex/values'; - -import { getStaticEnumConvexValidator } from '../../../../common/_helpers/getStaticEnumConvexValidator'; - -const forceDiagram = getStaticEnumConvexValidator(ForceDiagram); -const unit = getStaticEnumConvexValidator(Unit); - -export const listData = v.object({ - tournamentRegistrationId: v.optional(v.id('tournamentRegistrations')), - meta: v.object({ - forceDiagram, - pointsLimit: v.number(), - }), - formations: v.array(v.object({ - id: v.string(), // NanoId - sourceId: unit, - })), - units: v.array(v.object({ - id: v.string(), // NanoId - sourceId: unit, - formationId: v.string(), // Formation NanoId or 'support' - slotId: v.string(), // e.g. Armour 1 - })), - commandCards: v.array(v.object({ - id: v.string(), - sourceId: v.string(), - appliedTo: v.string(), // Formation ID or unit ID - })), -}); - -export type ListData = Infer; diff --git a/convex/_model/gameSystems/battlefront/flamesOfWarV4/index.ts b/convex/_model/gameSystems/battlefront/flamesOfWarV4/index.ts deleted file mode 100644 index 4291370a..00000000 --- a/convex/_model/gameSystems/battlefront/flamesOfWarV4/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -// TODO-250: These should all be moved to combat-command-game-systems eventually. - -export { - deepenListData, -} from './_helpers/deepenListData'; -export { - type ListData, - listData, -} from './_model/listData'; diff --git a/convex/_model/gameSystems/index.ts b/convex/_model/gameSystems/index.ts deleted file mode 100644 index 3dd23fe2..00000000 --- a/convex/_model/gameSystems/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * as FlamesOfWarV4 from './battlefront/flamesOfWarV4'; diff --git a/convex/_model/listChecks/_helpers/deepenListCheck.ts b/convex/_model/listChecks/_helpers/deepenListCheck.ts new file mode 100644 index 00000000..48595b00 --- /dev/null +++ b/convex/_model/listChecks/_helpers/deepenListCheck.ts @@ -0,0 +1,21 @@ +import { ConvexError } from 'convex/values'; + +import { Doc } from '../../../_generated/dataModel'; +import { QueryCtx } from '../../../_generated/server'; +import { getErrorMessage } from '../../common/errors'; +import { getUser } from '../../users/queries/getUser'; + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +export const deepenListCheck = async ( + ctx: QueryCtx, + doc: Doc<'listChecks'>, +) => { + const user = await getUser(ctx, { id: doc.userId }); + if (!user) { + throw new ConvexError(getErrorMessage('USER_NOT_FOUND')); + } + return { + ...doc, + user, + }; +}; diff --git a/convex/_model/listChecks/index.ts b/convex/_model/listChecks/index.ts new file mode 100644 index 00000000..5a35b5ce --- /dev/null +++ b/convex/_model/listChecks/index.ts @@ -0,0 +1,5 @@ +// Mutations +export { + createListCheck, + createListCheckArgs, +} from './mutations/createListCheck'; diff --git a/convex/_model/listChecks/mutations/createListCheck.ts b/convex/_model/listChecks/mutations/createListCheck.ts new file mode 100644 index 00000000..1a997179 --- /dev/null +++ b/convex/_model/listChecks/mutations/createListCheck.ts @@ -0,0 +1,18 @@ +import { Infer, v } from 'convex/values'; + +import { Id } from '../../../_generated/dataModel'; +import { MutationCtx } from '../../../_generated/server'; +import { checkAuth } from '../../common/_helpers/checkAuth'; +import { editableFields } from '../table'; + +export const createListCheckArgs = v.object({ + ...editableFields, +}); + +export const createListCheck = async ( + ctx: MutationCtx, + args: Infer, +): Promise> => await ctx.db.insert('listChecks', { + ...args, + userId: await checkAuth(ctx), +}); diff --git a/convex/_model/listChecks/table.ts b/convex/_model/listChecks/table.ts new file mode 100644 index 00000000..263d6d29 --- /dev/null +++ b/convex/_model/listChecks/table.ts @@ -0,0 +1,25 @@ +import { defineTable } from 'convex/server'; +import { v } from 'convex/values'; + +export const reviewStatus = v.union(v.literal('approved'), v.literal('rejected')); + +export const editableFields = { + approved: v.boolean(), + listId: v.id('lists'), + comment: v.optional(v.string()), +}; + +/** + * Fields which can only be edited using special mutations, or which are set programmatically. + */ +export const computedFields = { + modifiedAt: v.optional(v.number()), + userId: v.id('users'), +}; + +export default defineTable({ + ...editableFields, + ...computedFields, +}) + .index('by_list', ['listId']) + .index('by_user', ['userId']); diff --git a/convex/_model/lists/_helpers/checkListSubmittedOnTime.ts b/convex/_model/lists/_helpers/checkListSubmittedOnTime.ts new file mode 100644 index 00000000..3fda90c0 --- /dev/null +++ b/convex/_model/lists/_helpers/checkListSubmittedOnTime.ts @@ -0,0 +1,18 @@ +import { Doc } from '../../../_generated/dataModel'; +import { QueryCtx } from '../../../_generated/server'; +import { getDocStrict } from '../../common/_helpers/getDocStrict'; + +export const checkListSubmittedOnTime = async ( + ctx: QueryCtx, + doc: Doc<'lists'>, +): Promise => { + if (doc.tournamentRegistrationId) { + const tournamentRegistration = await getDocStrict(ctx, doc.tournamentRegistrationId); + const tournament = await getDocStrict(ctx, tournamentRegistration.tournamentId); + if (doc._creationTime < tournament.listSubmissionClosesAt) { + return true; + } + return false; + } + return true; +}; diff --git a/convex/_model/lists/_helpers/checkListVisibility.ts b/convex/_model/lists/_helpers/checkListVisibility.ts new file mode 100644 index 00000000..5dbc66e5 --- /dev/null +++ b/convex/_model/lists/_helpers/checkListVisibility.ts @@ -0,0 +1,32 @@ +import { getAuthUserId } from '@convex-dev/auth/server'; + +import { Doc } from '../../../_generated/dataModel'; +import { QueryCtx } from '../../../_generated/server'; +import { getDocStrict } from '../../common/_helpers/getDocStrict'; +import { checkUsersAreTeammates } from '../../tournamentCompetitors/_helpers/checkUsersAreTeammates'; +import { checkUserIsTournamentOrganizer } from '../../tournamentOrganizers'; + +export const checkListVisibility = async ( + ctx: QueryCtx, + doc: Doc<'lists'>, +): Promise => { + const userId = await getAuthUserId(ctx); + + if (doc.tournamentRegistrationId) { + const tournamentRegistration = await getDocStrict(ctx, doc.tournamentRegistrationId); + const tournament = await getDocStrict(ctx, tournamentRegistration.tournamentId); + const isOrganizer = await checkUserIsTournamentOrganizer(ctx, tournament._id, userId); + const isTeammate = await checkUsersAreTeammates(ctx, tournamentRegistration.userId, userId); + const latestCheck = await ctx.db.query('listChecks') + .withIndex('by_list', (q) => q.eq('listId', doc._id)) + .first(); + + if (isOrganizer || isTeammate || (tournament.listsRevealed && latestCheck?.approved)) { + return true; + } + + return false; + } + + return true; +}; diff --git a/convex/_model/lists/_helpers/deepenList.ts b/convex/_model/lists/_helpers/deepenList.ts index e7a6daff..070c5ecd 100644 --- a/convex/_model/lists/_helpers/deepenList.ts +++ b/convex/_model/lists/_helpers/deepenList.ts @@ -1,10 +1,7 @@ -import { GameSystem } from '@ianpaschal/combat-command-game-systems/common'; -import { ConvexError } from 'convex/values'; - import { Doc } from '../../../_generated/dataModel'; import { QueryCtx } from '../../../_generated/server'; -import { getErrorMessage } from '../../common/errors'; -import { FlamesOfWarV4 } from '../../gameSystems'; +import { checkListSubmittedOnTime } from './checkListSubmittedOnTime'; +import { getAvailableActions } from './getAvailableActions'; /* eslint-disable @typescript-eslint/explicit-function-return-type */ /** @@ -19,22 +16,19 @@ import { FlamesOfWarV4 } from '../../gameSystems'; */ export const deepenList = async ( ctx: QueryCtx, - list: Doc<'lists'>, + doc: Doc<'lists'>, ) => { - // TODO-250: Add Team Yankee support here. - if (list.gameSystem === GameSystem.FlamesOfWarV4) { - return { - ...list, - data: FlamesOfWarV4.deepenListData(list.data), - }; - } - - // If no matcher found, throw an error: - throw new ConvexError(getErrorMessage('CANNOT_ADD_ANOTHER_PLAYER')); + + const listChecks = await ctx.db.query('listChecks') + .withIndex('by_list', (q) => q.eq('listId', doc._id)) + .collect(); + return { + ...doc, + displayName: undefined, // Future: Set displayName based on extracted list data + availableActions: await getAvailableActions(ctx, doc), + onTime: await checkListSubmittedOnTime(ctx, doc), + listChecks, + }; }; - -/** - * Deep list with additional joined data and computed fields. - */ -export type DeepList = Awaited>; +// FUTURE: Deepen list based on game system diff --git a/convex/_model/lists/_helpers/getAvailableActions.ts b/convex/_model/lists/_helpers/getAvailableActions.ts new file mode 100644 index 00000000..7775ab3a --- /dev/null +++ b/convex/_model/lists/_helpers/getAvailableActions.ts @@ -0,0 +1,65 @@ +import { getAuthUserId } from '@convex-dev/auth/server'; + +import { Doc } from '../../../_generated/dataModel'; +import { QueryCtx } from '../../../_generated/server'; +import { getDocStrict } from '../../common/_helpers/getDocStrict'; +import { checkUserIsTeamCaptain } from '../../tournamentCompetitors'; +import { checkUserIsTournamentOrganizer } from '../../tournamentOrganizers'; + +export enum ListActionKey { + /** Delete this list. */ + Delete = 'delete', + /** Download this list's file. */ + Download = 'download', + /** Edit this list. */ + Manage = 'manage', + /** Approve or reject this list. */ + CreateListCheck = 'createListCheck', +} + +/** + * Gets a list of list actions which are available to the current user. + * + * @param ctx - Convex query context + * @param doc - Raw list document + * @returns An array of ListActionKey(s) + */ +export const getAvailableActions = async ( + ctx: QueryCtx, + doc: Doc<'lists'>, +): Promise => { + const userId = await getAuthUserId(ctx); + + const isSelf = userId && doc.userId === userId; + + let isArchived = false; + let isOrganizer = false; + let isCaptain = false; + + if (doc.tournamentRegistrationId) { + const registration = await getDocStrict(ctx, doc.tournamentRegistrationId); + const tournament = await getDocStrict(ctx, registration.tournamentId); + + isOrganizer = await checkUserIsTournamentOrganizer(ctx, registration.tournamentId, userId); + isCaptain = await checkUserIsTeamCaptain(ctx, registration.tournamentCompetitorId, userId); + isArchived = tournament.status === 'archived'; + } + + // ---- PRIMARY ACTIONS ---- + const actions: ListActionKey[] = [ListActionKey.Download]; + + if (isArchived) { + return actions; + } + + if (isOrganizer || ((isSelf || isCaptain) && !doc.locked)) { + actions.push(ListActionKey.Delete); + actions.push(ListActionKey.Manage); + } + + if (isOrganizer) { + actions.push(ListActionKey.CreateListCheck); + } + + return actions; +}; diff --git a/convex/_model/lists/index.ts b/convex/_model/lists/index.ts index 22089660..ca89f1d0 100644 --- a/convex/_model/lists/index.ts +++ b/convex/_model/lists/index.ts @@ -1,12 +1,32 @@ -import { Id } from '../../_generated/dataModel'; - -export type ListId = Id<'lists'>; +export { + ListActionKey, +} from './_helpers/getAvailableActions'; +export * from './types'; +// Mutations export { - importListData, - importListDataArgs, -} from './mutations/importListData'; + createList, + createListArgs, +} from './mutations/createList'; +export { + deleteList, + deleteListArgs, +} from './mutations/deleteList'; +export { + updateList, + updateListArgs, +} from './mutations/updateList'; + +// Queries export { getList, getListArgs, } from './queries/getList'; +export { + getListsByTournamentRegistration, + getListsByTournamentRegistrationArgs, +} from './queries/getListsByTournamentRegistration'; +export { + getListsByUser, + getListsByUserArgs, +} from './queries/getListsByUser'; diff --git a/convex/_model/lists/mutations/createList.ts b/convex/_model/lists/mutations/createList.ts new file mode 100644 index 00000000..ef4d635c --- /dev/null +++ b/convex/_model/lists/mutations/createList.ts @@ -0,0 +1,31 @@ +import { Infer, v } from 'convex/values'; + +import { Id } from '../../../_generated/dataModel'; +import { MutationCtx } from '../../../_generated/server'; +import { getDocStrict } from '../../common/_helpers/getDocStrict'; +import { editableFields } from '../table'; + +export const createListArgs = v.object(editableFields); + +export const createList = async ( + ctx: MutationCtx, + args: Infer, +): Promise> => { + + let computedFields = { + locked: false, + }; + + if (args.tournamentRegistrationId) { + const tournamentRegistration = await getDocStrict(ctx, args.tournamentRegistrationId); + const tournament = await getDocStrict(ctx, tournamentRegistration.tournamentId); + computedFields = { + locked: Date.now() > tournament.listSubmissionClosesAt, + }; + } + + return await ctx.db.insert('lists', { + ...args, + ...computedFields, + }); +}; diff --git a/convex/_model/lists/mutations/deleteList.ts b/convex/_model/lists/mutations/deleteList.ts new file mode 100644 index 00000000..660f370b --- /dev/null +++ b/convex/_model/lists/mutations/deleteList.ts @@ -0,0 +1,14 @@ +import { Infer, v } from 'convex/values'; + +import { MutationCtx } from '../../../_generated/server'; + +export const deleteListArgs = v.object({ + id: v.id('lists'), +}); + +export const deleteList = async ( + ctx: MutationCtx, + args: Infer, +): Promise => { + await ctx.db.delete(args.id); +}; diff --git a/convex/_model/lists/mutations/importListData.ts b/convex/_model/lists/mutations/importListData.ts deleted file mode 100644 index 3aa6d69c..00000000 --- a/convex/_model/lists/mutations/importListData.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { GameSystem } from '@ianpaschal/combat-command-game-systems/common'; -import { ForceDiagram, Unit } from '@ianpaschal/combat-command-game-systems/flamesOfWarV4'; -import { Infer, v } from 'convex/values'; -import { customAlphabet } from 'nanoid'; - -import { MutationCtx } from '../../../_generated/server'; -import { getStaticEnumConvexValidator } from '../../common/_helpers/getStaticEnumConvexValidator'; - -const forceDiagram = getStaticEnumConvexValidator(ForceDiagram); -const formation = getStaticEnumConvexValidator(Unit); - -const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz', 6); - -export const importListDataArgs = v.object({ - pointsLimit: v.number(), - data: v.array(v.object({ - displayName: v.optional(v.string()), // Not used, but allowed because its handy in the raw data. - forceDiagram, - formations: v.array(formation), - playerUserId: v.id('users'), - tournamentRegistrationId: v.id('tournamentRegistrations'), - })), - locked: v.boolean(), -}); - -export const importListData = async ( - ctx: MutationCtx, - args: Infer, -): Promise => { - for (const row of args.data) { - const listId = await ctx.db.insert('lists', { - gameSystem: GameSystem.FlamesOfWarV4, - ownerUserId: row.playerUserId, - data: { - meta: { - forceDiagram: row.forceDiagram, - pointsLimit: args.pointsLimit, - }, - formations: row.formations.map((sourceId) => ({ - id: nanoid(), - sourceId, - })), - units: [], - commandCards: [], - }, - locked: args.locked, - }); - - await ctx.db.patch(row.tournamentRegistrationId, { listId }); - } -}; diff --git a/convex/_model/lists/mutations/updateList.ts b/convex/_model/lists/mutations/updateList.ts new file mode 100644 index 00000000..0da06cdd --- /dev/null +++ b/convex/_model/lists/mutations/updateList.ts @@ -0,0 +1,39 @@ +import { Infer, v } from 'convex/values'; + +import { MutationCtx } from '../../../_generated/server'; +import { editableFields } from '../table'; + +export const updateListArgs = v.object({ + _id: v.id('lists'), + ...editableFields, +}); + +export const updateList = async ( + ctx: MutationCtx, + args: Infer, +): Promise => { + + const { _id: id, ...updated } = args; + + // let computedFields = { + // locked: false, + // approved: true, + // }; + + // if (args.tournamentRegistrationId) { + // const tournamentRegistration = await getDocStrict(ctx, args.tournamentRegistrationId); + // const tournament = await getDocStrict(ctx, tournamentRegistration.tournamentId); + // computedFields = { + // locked: Date.now() > tournament.listSubmissionClosesAt, + // approved: false, + // }; + // } + + // TODO: Do not allow approved change if not a TO + // TODO: Do not allow any changes if locked + + return await ctx.db.patch(id, { + ...updated, + modifiedAt: Date.now(), + }); +}; diff --git a/convex/_model/lists/queries/getList.ts b/convex/_model/lists/queries/getList.ts index 31f1bda2..0076b5b7 100644 --- a/convex/_model/lists/queries/getList.ts +++ b/convex/_model/lists/queries/getList.ts @@ -1,7 +1,9 @@ import { Infer, v } from 'convex/values'; import { QueryCtx } from '../../../_generated/server'; -import { deepenList, DeepList } from '../_helpers/deepenList'; +import { checkListVisibility } from '../_helpers/checkListVisibility'; +import { deepenList } from '../_helpers/deepenList'; +import { List } from '../types'; export const getListArgs = v.object({ id: v.id('lists'), @@ -18,10 +20,13 @@ export const getListArgs = v.object({ export const getList = async ( ctx: QueryCtx, args: Infer, -): Promise => { - const list = await ctx.db.get(args.id); - if (!list) { +): Promise => { + const result = await ctx.db.get(args.id); + if (!result) { return null; } - return await deepenList(ctx, list); + if (await checkListVisibility(ctx, result)) { + return await deepenList(ctx, result); + } + return null; }; diff --git a/convex/_model/lists/queries/getListsByTournamentRegistration.ts b/convex/_model/lists/queries/getListsByTournamentRegistration.ts new file mode 100644 index 00000000..2e46e431 --- /dev/null +++ b/convex/_model/lists/queries/getListsByTournamentRegistration.ts @@ -0,0 +1,27 @@ +import { Infer, v } from 'convex/values'; + +import { QueryCtx } from '../../../_generated/server'; +import { notNullOrUndefined } from '../../common/_helpers/notNullOrUndefined'; +import { checkListVisibility } from '../_helpers/checkListVisibility'; +import { deepenList } from '../_helpers/deepenList'; +import { List } from '../types'; + +export const getListsByTournamentRegistrationArgs = v.object({ + tournamentRegistrationId: v.id('tournamentRegistrations'), +}); + +export const getListsByTournamentRegistration = async ( + ctx: QueryCtx, + args: Infer, +): Promise => { + const results = await ctx.db.query('lists') + .withIndex('by_tournament_registration', (q) => q.eq('tournamentRegistrationId', args.tournamentRegistrationId)) + .collect(); + const deepResults = await Promise.all(results.map(async (r) => { + if (await checkListVisibility(ctx, r)) { + return await deepenList(ctx, r); + } + return null; + })); + return deepResults.filter(notNullOrUndefined); +}; diff --git a/convex/_model/lists/queries/getListsByUser.ts b/convex/_model/lists/queries/getListsByUser.ts new file mode 100644 index 00000000..4dcbc07b --- /dev/null +++ b/convex/_model/lists/queries/getListsByUser.ts @@ -0,0 +1,27 @@ +import { Infer, v } from 'convex/values'; + +import { QueryCtx } from '../../../_generated/server'; +import { notNullOrUndefined } from '../../common/_helpers/notNullOrUndefined'; +import { checkListVisibility } from '../_helpers/checkListVisibility'; +import { deepenList } from '../_helpers/deepenList'; +import { List } from '../types'; + +export const getListsByUserArgs = v.object({ + userId: v.id('users'), +}); + +export const getListsByUser = async ( + ctx: QueryCtx, + args: Infer, +): Promise => { + const results = await ctx.db.query('lists') + .withIndex('by_user', (q) => q.eq('userId', args.userId)) + .collect(); + const deepResults = await Promise.all(results.map(async (r) => { + if (await checkListVisibility(ctx, r)) { + return await deepenList(ctx, r); + } + return null; + })); + return deepResults.filter(notNullOrUndefined); +}; diff --git a/convex/_model/lists/table.ts b/convex/_model/lists/table.ts index c98f4884..50bd7431 100644 --- a/convex/_model/lists/table.ts +++ b/convex/_model/lists/table.ts @@ -3,14 +3,17 @@ import { defineTable } from 'convex/server'; import { v } from 'convex/values'; import { getStaticEnumConvexValidator } from '../common/_helpers/getStaticEnumConvexValidator'; -import { FlamesOfWarV4 } from '../gameSystems'; const gameSystem = getStaticEnumConvexValidator(GameSystem); export const editableFields = { - gameSystem: gameSystem, - data: FlamesOfWarV4.listData, - ownerUserId: v.id('users'), + gameSystem, + storageId: v.id('_storage'), + userId: v.id('users'), + tournamentRegistrationId: v.optional(v.id('tournamentRegistrations')), + + // FUTURE: + // data: listData, }; /** @@ -18,11 +21,13 @@ export const editableFields = { */ export const computedFields = { modifiedAt: v.optional(v.number()), - locked: v.optional(v.boolean()), + locked: v.boolean(), }; export default defineTable({ ...editableFields, ...computedFields, }) - .index('by_owner_user_id', ['ownerUserId']); + .index('by_game_system', ['gameSystem']) + .index('by_tournament_registration', ['tournamentRegistrationId']) + .index('by_user', ['userId']); diff --git a/convex/_model/lists/types.ts b/convex/_model/lists/types.ts new file mode 100644 index 00000000..4b4ddfa2 --- /dev/null +++ b/convex/_model/lists/types.ts @@ -0,0 +1,13 @@ +import { Infer } from 'convex/values'; + +import { deepenList } from './_helpers/deepenList'; +import { createListArgs } from './mutations/createList'; +import { deleteListArgs } from './mutations/deleteList'; +import { updateListArgs } from './mutations/updateList'; +import { Id } from '../../_generated/dataModel'; + +export type List = Awaited>; +export type ListCreateArgs = Infer; +export type ListDeleteArgs = Infer; +export type ListId = Id<'lists'>; +export type ListUpdateArgs = Infer; diff --git a/convex/_model/matchResults/queries/getMatchResults.ts b/convex/_model/matchResults/queries/getMatchResults.ts index bc43fce5..76f1e5fe 100644 --- a/convex/_model/matchResults/queries/getMatchResults.ts +++ b/convex/_model/matchResults/queries/getMatchResults.ts @@ -6,6 +6,11 @@ import { deepenMatchResult, DeepMatchResult } from '../_helpers/deepenMatchResul export const getMatchResultsArgs = v.object({ paginationOpts: paginationOptsValidator, + filter: v.optional(v.object({ + tournamentId: v.optional(v.id('tournaments')), + tournamentPairingId: v.optional(v.id('tournaments')), + tournamentCompetitorId: v.optional(v.id('tournaments')), + })), }); export const getMatchResults = async ( diff --git a/convex/_model/tournamentCompetitors/_helpers/checkUserIsTeamCaptain.ts b/convex/_model/tournamentCompetitors/_helpers/checkUserIsTeamCaptain.ts new file mode 100644 index 00000000..c9def003 --- /dev/null +++ b/convex/_model/tournamentCompetitors/_helpers/checkUserIsTeamCaptain.ts @@ -0,0 +1,19 @@ +import { Id } from '../../../_generated/dataModel'; +import { QueryCtx } from '../../../_generated/server'; +import { getDocStrict } from '../../common/_helpers/getDocStrict'; + +export const checkUserIsTeamCaptain = async ( + ctx: QueryCtx, + tournamentCompetitorId: Id<'tournamentCompetitors'>, + userId: Id<'users'> | null, +): Promise => { + if (!userId) { + return false; + } + const { tournamentId, captainUserId } = await getDocStrict(ctx, tournamentCompetitorId); + const { competitorSize } = await getDocStrict(ctx, tournamentId); + if (competitorSize <= 1) { + return false; + } + return captainUserId === userId; +}; diff --git a/convex/_model/tournamentCompetitors/_helpers/checkUsersAreTeammates.ts b/convex/_model/tournamentCompetitors/_helpers/checkUsersAreTeammates.ts new file mode 100644 index 00000000..e61668cb --- /dev/null +++ b/convex/_model/tournamentCompetitors/_helpers/checkUsersAreTeammates.ts @@ -0,0 +1,23 @@ +import { Id } from '../../../_generated/dataModel'; +import { QueryCtx } from '../../../_generated/server'; + +export const checkUsersAreTeammates = async ( + ctx: QueryCtx, + user0Id: Id<'users'> | null, + user1Id: Id<'users'> | null, +): Promise => { + if (!user0Id || !user1Id) { + return false; + } + const player0Records = await ctx.db.query('tournamentRegistrations') + .withIndex('by_user', (q) => q.eq('userId', user0Id)) + .collect(); + const player1Records = await ctx.db.query('tournamentRegistrations') + .withIndex('by_user', (q) => q.eq('userId', user1Id)) + .collect(); + + const player0CompetitorIds = new Set(player0Records.map((r) => r.tournamentCompetitorId)); + + return player1Records.some((r) => player0CompetitorIds.has(r.tournamentCompetitorId), + ); +}; diff --git a/convex/_model/tournamentCompetitors/_helpers/getAvailableActions.ts b/convex/_model/tournamentCompetitors/_helpers/getAvailableActions.ts index f966c4b9..e22c0b95 100644 --- a/convex/_model/tournamentCompetitors/_helpers/getAvailableActions.ts +++ b/convex/_model/tournamentCompetitors/_helpers/getAvailableActions.ts @@ -87,7 +87,7 @@ export const getAvailableActions = async ( } // Player Actions: - if (!isPlayer && tournament.status === 'published' && isTeamTournament) { + if (userId && !isPlayer && tournament.status === 'published' && isTeamTournament) { actions.push(TournamentCompetitorActionKey.Join); } diff --git a/convex/_model/tournamentCompetitors/index.ts b/convex/_model/tournamentCompetitors/index.ts index 5a110e90..9eb2ade5 100644 --- a/convex/_model/tournamentCompetitors/index.ts +++ b/convex/_model/tournamentCompetitors/index.ts @@ -7,6 +7,9 @@ export type TournamentCompetitorId = Id<'tournamentCompetitors'>; export type ScoreAdjustment = Infer; // Helpers +export { + checkUserIsTeamCaptain, +} from './_helpers/checkUserIsTeamCaptain'; export { deepenTournamentCompetitor, type DeepTournamentCompetitor, diff --git a/convex/_model/tournamentRegistrations/_helpers/deepenTournamentRegistration.ts b/convex/_model/tournamentRegistrations/_helpers/deepenTournamentRegistration.ts index 51e6f51f..1769a799 100644 --- a/convex/_model/tournamentRegistrations/_helpers/deepenTournamentRegistration.ts +++ b/convex/_model/tournamentRegistrations/_helpers/deepenTournamentRegistration.ts @@ -3,7 +3,9 @@ import { ConvexError } from 'convex/values'; import { Doc } from '../../../_generated/dataModel'; import { QueryCtx } from '../../../_generated/server'; +import { getDocStrict } from '../../common/_helpers/getDocStrict'; import { getErrorMessage } from '../../common/errors'; +import { getListsByTournamentRegistration } from '../../lists'; import { checkUserIsTournamentOrganizer } from '../../tournamentOrganizers'; import { getUser } from '../../users'; import { getAvailableActions } from './getAvailableActions'; @@ -20,10 +22,8 @@ export const deepenTournamentRegistration = async ( if (!user) { throw new ConvexError(getErrorMessage('USER_NOT_FOUND')); } - const tournament = await ctx.db.get(doc.tournamentId); - if (!tournament) { - throw new ConvexError(getErrorMessage('TOURNAMENT_NOT_FOUND')); - } + + const tournament = await getDocStrict(ctx, doc.tournamentId); const availableActions = await getAvailableActions(ctx, doc); @@ -32,6 +32,7 @@ export const deepenTournamentRegistration = async ( const factionsVisible = isOrganizer || tournament.factionsRevealed; // TODO: Use lists if they are present. getDetails() + const lists = await getListsByTournamentRegistration(ctx, { tournamentRegistrationId: doc._id }); const alignments = Array.from(new Set(alignmentsVisible && details?.alignment ? [details.alignment] : [])); const factions = Array.from(new Set(factionsVisible && details?.faction ? [details.faction] : [])); @@ -48,6 +49,7 @@ export const deepenTournamentRegistration = async ( displayName: user.displayName, alignments, factions, + lists, }; }; diff --git a/convex/_model/tournamentRegistrations/_helpers/getAvailableActions.ts b/convex/_model/tournamentRegistrations/_helpers/getAvailableActions.ts index d195021e..aef33f95 100644 --- a/convex/_model/tournamentRegistrations/_helpers/getAvailableActions.ts +++ b/convex/_model/tournamentRegistrations/_helpers/getAvailableActions.ts @@ -13,9 +13,12 @@ export enum TournamentRegistrationActionKey { /** Delete this TournamentRegistration (and TournamentCompetitor if no remaining players). */ Delete = 'delete', + /** Create a new list for this TournamentRegistration. */ + CreateList = 'createList', + // TODO // ApproveList = 'approveList', - + // TODO // RejectList = 'rejectList', @@ -75,6 +78,10 @@ export const getAvailableActions = async ( actions.push(TournamentRegistrationActionKey.Edit); } + if (isOrganizer || isCaptain || isSelf) { + actions.push(TournamentRegistrationActionKey.CreateList); + } + // if (isOrganizer) { // actions.push(TournamentRegistrationActionKey.ApproveList); // } diff --git a/convex/_model/tournaments/_helpers/getAvailableActions.ts b/convex/_model/tournaments/_helpers/getAvailableActions.ts index df995202..1adf1ac1 100644 --- a/convex/_model/tournaments/_helpers/getAvailableActions.ts +++ b/convex/_model/tournaments/_helpers/getAvailableActions.ts @@ -162,7 +162,7 @@ export const getAvailableActions = async ( } // Player Actions - if (!isPlayer && doc.status === 'published') { + if (userId && !isPlayer && doc.status === 'published') { actions.push(TournamentActionKey.Join); } diff --git a/convex/_model/tournaments/actions/exportFowV4TournamentMatchData.ts b/convex/_model/tournaments/actions/exportFowV4TournamentMatchData.ts index 30df2181..16c555a1 100644 --- a/convex/_model/tournaments/actions/exportFowV4TournamentMatchData.ts +++ b/convex/_model/tournaments/actions/exportFowV4TournamentMatchData.ts @@ -42,17 +42,17 @@ export const exportFowV4TournamentMatchData = async ( type GeneralKey = keyof typeof matchResult; type DetailsKey = keyof typeof matchResult.details; const playerUser = i === 0 ? matchResult.player0User : matchResult.player1User; - const playerList = i === 0 ? matchResult.player0List : matchResult.player1List; + // const playerList = i === 0 ? matchResult.player0List : matchResult.player1List; const playerTeam = tournamentCompetitors.find((c) => c.registrations.find((r) => r.user?._id === playerUser?._id)); return { ...acc, [`player_${letter}_team`]: playerTeam?.teamName ?? '', [`player_${letter}_user_id`]: playerUser?._id ?? '', [`player_${letter}_name`]: playerUser?.displayName ?? matchResult[`player${i}Placeholder` as GeneralKey], - [`player_${letter}_force_diagram`]: playerList?.data.meta.forceDiagram ?? '', - [`player_${letter}_faction`]: playerList?.data.meta.faction ?? '', - [`player_${letter}_formation_0`]: playerList?.data.formations[0]?.sourceId ?? '', - [`player_${letter}_formation_1`]: playerList?.data.formations[1]?.sourceId ?? '', + // [`player_${letter}_force_diagram`]: playerList?.data.meta.forceDiagram ?? '', + // [`player_${letter}_faction`]: playerList?.data.meta.faction ?? '', + // [`player_${letter}_formation_0`]: playerList?.data.formations[0]?.sourceId ?? '', + // [`player_${letter}_formation_1`]: playerList?.data.formations[1]?.sourceId ?? '', [`player_${letter}_battle_plan`]: details[`player${i}BattlePlan` as DetailsKey], [`player_${letter}_units_lost`]: details[`player${i}UnitsLost` as DetailsKey], [`player_${letter}_score`]: matchResult[`player${i}Score` as DetailsKey], diff --git a/convex/_model/tournaments/table.ts b/convex/_model/tournaments/table.ts index 19cb31f6..7f372e1a 100644 --- a/convex/_model/tournaments/table.ts +++ b/convex/_model/tournaments/table.ts @@ -60,6 +60,7 @@ export const editableFields = { alignmentsRevealed: v.optional(v.boolean()), factionsRevealed: v.optional(v.boolean()), + listsRevealed: v.optional(v.boolean()), // Format pairingConfig: tournamentPairingConfig, diff --git a/convex/files.ts b/convex/files.ts index 2f3aa268..c2e47a59 100644 --- a/convex/files.ts +++ b/convex/files.ts @@ -1,4 +1,10 @@ -import { mutation, query } from './_generated/server'; +import { v } from 'convex/values'; + +import { + action, + mutation, + query, +} from './_generated/server'; import * as model from './_model/files'; export const getFileUrl = query({ @@ -6,6 +12,23 @@ export const getFileUrl = query({ handler: model.getFileUrl, }); +export const getFileMetadata = query({ + args: model.getFileMetadataArgs, + handler: model.getFileMetadata, +}); + export const generateFileUploadUrl = mutation({ handler: async (ctx) => await ctx.storage.generateUploadUrl(), }); + +export const convertImageToPdf = action({ + args: model.convertImageToPdfArgs, + handler: model.convertImageToPdf, + returns: v.id('_storage'), +}); + +export const getFileDownloadData = action({ + args: model.getFileDownloadDataArgs, + handler: model.getFileDownloadData, + returns: v.string(), +}); diff --git a/convex/listChecks.ts b/convex/listChecks.ts new file mode 100644 index 00000000..19a05082 --- /dev/null +++ b/convex/listChecks.ts @@ -0,0 +1,7 @@ +import { mutation } from './_generated/server'; +import * as model from './_model/listChecks'; + +export const createListCheck = mutation({ + args: model.createListCheckArgs, + handler: model.createListCheck, +}); diff --git a/convex/lists.ts b/convex/lists.ts index 1068c1e5..5ba4bd48 100644 --- a/convex/lists.ts +++ b/convex/lists.ts @@ -1,7 +1,27 @@ -import { mutation } from './_generated/server'; +import { mutation, query } from './_generated/server'; import * as model from './_model/lists'; -export const importListData = mutation({ - args: model.importListDataArgs, - handler: model.importListData, +export const createList = mutation({ + args: model.createListArgs, + handler: model.createList, +}); + +export const deleteList = mutation({ + args: model.deleteListArgs, + handler: model.deleteList, +}); + +export const updateList = mutation({ + args: model.updateListArgs, + handler: model.updateList, +}); + +export const getList = query({ + args: model.getListArgs, + handler: model.getList, +}); + +export const getListsByTournamentRegistration = query({ + args: model.getListsByTournamentRegistrationArgs, + handler: model.getListsByTournamentRegistration, }); diff --git a/convex/scheduledTasks.ts b/convex/scheduledTasks.ts index 5dc88e3d..a14b8ee4 100644 --- a/convex/scheduledTasks.ts +++ b/convex/scheduledTasks.ts @@ -38,6 +38,12 @@ export const cleanUpFileStorage = internalMutation(async (ctx) => { referencedIds.add(u.avatarStorageId); } } + const lists = await ctx.db.query('lists').collect(); + for (const l of lists) { + if (l.storageId) { + referencedIds.add(l.storageId); + } + } const deletedIds = new Set(); diff --git a/convex/schema.ts b/convex/schema.ts index 9b1f42bc..98b42190 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -5,6 +5,7 @@ import friendships from './_model/friendships/table'; import leagueOrganizers from './_model/leagueOrganizers/table'; import leagueRankings from './_model/leagueRankings/table'; import leagues from './_model/leagues/table'; +import listChecks from './_model/listChecks/table'; import lists from './_model/lists/table'; import matchResultComments from './_model/matchResultComments/table'; import matchResultLikes from './_model/matchResultLikes/table'; @@ -26,6 +27,7 @@ export default defineSchema({ leagueOrganizers, leagueRankings, leagues, + listChecks, lists, matchResultComments, matchResultLikes, diff --git a/package-lock.json b/package-lock.json index 6ecf1750..7adaf9e1 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.8.3", + "@ianpaschal/combat-command-components": "file:../combat-command-components/ianpaschal-combat-command-components-1.9.0.tgz", "@ianpaschal/combat-command-game-systems": "^1.4.0", "@mapbox/search-js-core": "^1.0.0-beta.25", "@radix-ui/colors": "^3.0.0", @@ -46,6 +46,7 @@ "nanoid": "^5.1.5", "nuqs": "^2.7.3", "oslo": "^1.2.1", + "pdf-lib": "^1.17.1", "pdfjs-dist": "^4.4.168", "pica": "^9.0.1", "qs": "^6.14.0", @@ -415,9 +416,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -472,17 +473,16 @@ } }, "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==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@base-ui/react/-/react-1.2.0.tgz", + "integrity": "sha512-O6aEQHcm+QyGTFY28xuwRD3SEJGZOBDpyjN2WvpfWYFVhg+3zfXPysAILqtM0C1kWC82MccOE/v1j+GHXE4qIw==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.28.4", - "@base-ui/utils": "0.2.3", + "@babel/runtime": "^7.28.6", + "@base-ui/utils": "0.2.5", "@floating-ui/react-dom": "^2.1.6", "@floating-ui/utils": "^0.2.10", - "reselect": "^5.1.1", - "tabbable": "^6.3.0", + "tabbable": "^6.4.0", "use-sync-external-store": "^1.6.0" }, "engines": { @@ -504,12 +504,12 @@ } }, "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==", + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@base-ui/utils/-/utils-0.2.5.tgz", + "integrity": "sha512-oYC7w0gp76RI5MxprlGLV0wze0SErZaRl3AAkeP3OnNB/UBMb6RqNf6ZSIlxOc9Qp68Ab3C2VOcJQyRs7Xc7Vw==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.28.4", + "@babel/runtime": "^7.28.6", "@floating-ui/utils": "^0.2.10", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" @@ -1500,12 +1500,12 @@ "license": "BSD-3-Clause" }, "node_modules/@ianpaschal/combat-command-components": { - "version": "1.8.3", - "resolved": "https://npm.pkg.github.com/download/@ianpaschal/combat-command-components/1.8.3/79bc9bee5052e13cc94b32d715e54d321e798cd1", - "integrity": "sha512-gT71Lo9NRTZvg5O+mbidtXLhvGRCwG2GXHFy/hrSlHBFhSwldUmti901WwnyCS9aP91Zyon6sfkgbuTAdfHLXw==", + "version": "1.9.0", + "resolved": "file:../combat-command-components/ianpaschal-combat-command-components-1.9.0.tgz", + "integrity": "sha512-UWyTFzlTGFzBwe6IAPkK9nohdx7IDdoC1ikBwDpBEEBK8D8TUaFyBDxhq3cv6y7Z4ws/tPhNMCns1gDhMxsing==", "license": "MIT", "dependencies": { - "@base-ui/react": "^1.0.0", + "@base-ui/react": "^1.2.0", "@fontsource/figtree": "^5.2.10", "@radix-ui/colors": "^3.0.0", "@tanstack/react-store": "^0.8.0", @@ -1521,12 +1521,12 @@ } }, "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==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.8.1.tgz", + "integrity": "sha512-XItJt+rG8c5Wn/2L/bnxys85rBpm0BfMbhb4zmPVLXAKY9POrp1xd6IbU4PKoOI+jSEGc3vntPRfLGSgXfE2Ig==", "license": "MIT", "dependencies": { - "@tanstack/store": "0.8.0", + "@tanstack/store": "0.8.1", "use-sync-external-store": "^1.6.0" }, "funding": { @@ -1539,9 +1539,9 @@ } }, "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==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.8.1.tgz", + "integrity": "sha512-PtOisLjUZPz5VyPRSCGjNOlwTvabdTBQ2K80DpVL1chGVr35WRxfeavAPdNq6pm/t7F8GhoR2qtmkkqtCEtHYw==", "license": "MIT", "funding": { "type": "github", @@ -1806,31 +1806,39 @@ } }, "node_modules/@napi-rs/canvas": { - "version": "0.1.70", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.70.tgz", - "integrity": "sha512-nD6NGa4JbNYSZYsTnLGrqe9Kn/lCkA4ybXt8sx5ojDqZjr2i0TWAHxx/vhgfjX+i3hCdKWufxYwi7CfXqtITSA==", + "version": "0.1.92", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.92.tgz", + "integrity": "sha512-q7ZaUCJkEU5BeOdE7fBx1XWRd2T5Ady65nxq4brMf5L4cE1VV/ACq5w9Z5b/IVJs8CwSSIwc30nlthH0gFo4Ig==", "license": "MIT", "optional": true, + "workspaces": [ + "e2e/*" + ], "engines": { "node": ">= 10" }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, "optionalDependencies": { - "@napi-rs/canvas-android-arm64": "0.1.70", - "@napi-rs/canvas-darwin-arm64": "0.1.70", - "@napi-rs/canvas-darwin-x64": "0.1.70", - "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.70", - "@napi-rs/canvas-linux-arm64-gnu": "0.1.70", - "@napi-rs/canvas-linux-arm64-musl": "0.1.70", - "@napi-rs/canvas-linux-riscv64-gnu": "0.1.70", - "@napi-rs/canvas-linux-x64-gnu": "0.1.70", - "@napi-rs/canvas-linux-x64-musl": "0.1.70", - "@napi-rs/canvas-win32-x64-msvc": "0.1.70" + "@napi-rs/canvas-android-arm64": "0.1.92", + "@napi-rs/canvas-darwin-arm64": "0.1.92", + "@napi-rs/canvas-darwin-x64": "0.1.92", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.92", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.92", + "@napi-rs/canvas-linux-arm64-musl": "0.1.92", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.92", + "@napi-rs/canvas-linux-x64-gnu": "0.1.92", + "@napi-rs/canvas-linux-x64-musl": "0.1.92", + "@napi-rs/canvas-win32-arm64-msvc": "0.1.92", + "@napi-rs/canvas-win32-x64-msvc": "0.1.92" } }, "node_modules/@napi-rs/canvas-android-arm64": { - "version": "0.1.70", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.70.tgz", - "integrity": "sha512-I/YOuQ0wbkVYxVaYtCgN42WKTYxNqFA0gTcTrHIGG1jfpDSyZWII/uHcjOo4nzd19io6Y4+/BqP8E5hJgf9OmQ==", + "version": "0.1.92", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.92.tgz", + "integrity": "sha512-rDOtq53ujfOuevD5taxAuIFALuf1QsQWZe1yS/N4MtT+tNiDBEdjufvQRPWZ11FubL2uwgP8ApYU3YOaNu1ZsQ==", "cpu": [ "arm64" ], @@ -1841,12 +1849,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-darwin-arm64": { - "version": "0.1.70", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.70.tgz", - "integrity": "sha512-4pPGyXetHIHkw2TOJHujt3mkCP8LdDu8+CT15ld9Id39c752RcI0amDHSuMLMQfAjvusA9B5kKxazwjMGjEJpQ==", + "version": "0.1.92", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.92.tgz", + "integrity": "sha512-4PT6GRGCr7yMRehp42x0LJb1V0IEy1cDZDDayv7eKbFUIGbPFkV7CRC9Bee5MPkjg1EB4ZPXXUyy3gjQm7mR8Q==", "cpu": [ "arm64" ], @@ -1857,12 +1869,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-darwin-x64": { - "version": "0.1.70", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.70.tgz", - "integrity": "sha512-+2N6Os9LbkmDMHL+raknrUcLQhsXzc5CSXRbXws9C3pv/mjHRVszQ9dhFUUe9FjfPhCJznO6USVdwOtu7pOrzQ==", + "version": "0.1.92", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.92.tgz", + "integrity": "sha512-5e/3ZapP7CqPtDcZPtmowCsjoyQwuNMMD7c0GKPtZQ8pgQhLkeq/3fmk0HqNSD1i227FyJN/9pDrhw/UMTkaWA==", "cpu": [ "x64" ], @@ -1873,12 +1889,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { - "version": "0.1.70", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.70.tgz", - "integrity": "sha512-QjscX9OaKq/990sVhSMj581xuqLgiaPVMjjYvWaCmAJRkNQ004QfoSMEm3FoTqM4DRoquP8jvuEXScVJsc1rqQ==", + "version": "0.1.92", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.92.tgz", + "integrity": "sha512-j6KaLL9iir68lwpzzY+aBGag1PZp3+gJE2mQ3ar4VJVmyLRVOh+1qsdNK1gfWoAVy5w6U7OEYFrLzN2vOFUSng==", "cpu": [ "arm" ], @@ -1889,12 +1909,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-linux-arm64-gnu": { - "version": "0.1.70", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.70.tgz", - "integrity": "sha512-LNakMOwwqwiHIwMpnMAbFRczQMQ7TkkMyATqFCOtUJNlE6LPP/QiUj/mlFrNbUn/hctqShJ60gWEb52ZTALbVw==", + "version": "0.1.92", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.92.tgz", + "integrity": "sha512-s3NlnJMHOSotUYVoTCoC1OcomaChFdKmZg0VsHFeIkeHbwX0uPHP4eCX1irjSfMykyvsGHTQDfBAtGYuqxCxhQ==", "cpu": [ "arm64" ], @@ -1905,12 +1929,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-linux-arm64-musl": { - "version": "0.1.70", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.70.tgz", - "integrity": "sha512-wBTOllEYNfJCHOdZj9v8gLzZ4oY3oyPX8MSRvaxPm/s7RfEXxCyZ8OhJ5xAyicsDdbE5YBZqdmaaeP5+xKxvtg==", + "version": "0.1.92", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.92.tgz", + "integrity": "sha512-xV0GQnukYq5qY+ebkAwHjnP2OrSGBxS3vSi1zQNQj0bkXU6Ou+Tw7JjCM7pZcQ28MUyEBS1yKfo7rc7ip2IPFQ==", "cpu": [ "arm64" ], @@ -1921,12 +1949,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { - "version": "0.1.70", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.70.tgz", - "integrity": "sha512-GVUUPC8TuuFqHip0rxHkUqArQnlzmlXmTEBuXAWdgCv85zTCFH8nOHk/YCF5yo0Z2eOm8nOi90aWs0leJ4OE5Q==", + "version": "0.1.92", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.92.tgz", + "integrity": "sha512-+GKvIFbQ74eB/TopEdH6XIXcvOGcuKvCITLGXy7WLJAyNp3Kdn1ncjxg91ihatBaPR+t63QOE99yHuIWn3UQ9w==", "cpu": [ "riscv64" ], @@ -1937,12 +1969,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-linux-x64-gnu": { - "version": "0.1.70", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.70.tgz", - "integrity": "sha512-/kvUa2lZRwGNyfznSn5t1ShWJnr/m5acSlhTV3eXECafObjl0VBuA1HJw0QrilLpb4Fe0VLywkpD1NsMoVDROQ==", + "version": "0.1.92", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.92.tgz", + "integrity": "sha512-tFd6MwbEhZ1g64iVY2asV+dOJC+GT3Yd6UH4G3Hp0/VHQ6qikB+nvXEULskFYZ0+wFqlGPtXjG1Jmv7sJy+3Ww==", "cpu": [ "x64" ], @@ -1953,12 +1989,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-linux-x64-musl": { - "version": "0.1.70", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.70.tgz", - "integrity": "sha512-aqlv8MLpycoMKRmds7JWCfVwNf1fiZxaU7JwJs9/ExjTD8lX2KjsO7CTeAj5Cl4aEuzxUWbJPUUE2Qu9cZ1vfg==", + "version": "0.1.92", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.92.tgz", + "integrity": "sha512-uSuqeSveB/ZGd72VfNbHCSXO9sArpZTvznMVsb42nqPP7gBGEH6NJQ0+hmF+w24unEmxBhPYakP/Wiosm16KkA==", "cpu": [ "x64" ], @@ -1969,12 +2009,36 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-arm64-msvc": { + "version": "0.1.92", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.92.tgz", + "integrity": "sha512-20SK5AU/OUNz9ZuoAPj5ekWai45EIBDh/XsdrVZ8le/pJVlhjFU3olbumSQUXRFn7lBRS+qwM8kA//uLaDx6iQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-win32-x64-msvc": { - "version": "0.1.70", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.70.tgz", - "integrity": "sha512-Q9QU3WIpwBTVHk4cPfBjGHGU4U0llQYRXgJtFtYqqGNEOKVN4OT6PQ+ve63xwIPODMpZ0HHyj/KLGc9CWc3EtQ==", + "version": "0.1.92", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.92.tgz", + "integrity": "sha512-KEhyZLzq1MXCNlXybz4k25MJmHFp+uK1SIb8yJB0xfrQjz5aogAMhyseSzewo+XxAq3OAOdyKvfHGNzT3w1RPg==", "cpu": [ "x64" ], @@ -1985,6 +2049,10 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@node-rs/argon2": { @@ -2889,6 +2957,24 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@pdf-lib/standard-fonts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", + "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.6" + } + }, + "node_modules/@pdf-lib/upng": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz", + "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.10" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -10975,6 +11061,12 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -11133,6 +11225,24 @@ "node": ">= 14.16" } }, + "node_modules/pdf-lib": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", + "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==", + "license": "MIT", + "dependencies": { + "@pdf-lib/standard-fonts": "^1.0.0", + "@pdf-lib/upng": "^1.0.1", + "pako": "^1.0.11", + "tslib": "^1.11.1" + } + }, + "node_modules/pdf-lib/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, "node_modules/pdfjs-dist": { "version": "4.10.38", "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.10.38.tgz", @@ -13261,9 +13371,9 @@ "license": "MIT" }, "node_modules/tabbable": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.3.0.tgz", - "integrity": "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", "license": "MIT" }, "node_modules/table": { diff --git a/package.json b/package.json index 541711a3..51e4644e 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "@dnd-kit/sortable": "^10.0.0", "@fontsource/figtree": "^5.1.0", "@hookform/resolvers": "^3.9.0", - "@ianpaschal/combat-command-components": "^1.8.3", + "@ianpaschal/combat-command-components": "file:../combat-command-components/ianpaschal-combat-command-components-1.9.0.tgz", "@ianpaschal/combat-command-game-systems": "^1.4.0", "@mapbox/search-js-core": "^1.0.0-beta.25", "@radix-ui/colors": "^3.0.0", @@ -58,6 +58,7 @@ "nanoid": "^5.1.5", "nuqs": "^2.7.3", "oslo": "^1.2.1", + "pdf-lib": "^1.17.1", "pdfjs-dist": "^4.4.168", "pica": "^9.0.1", "qs": "^6.14.0", diff --git a/src/api.ts b/src/api.ts index 41724490..bc75c47b 100644 --- a/src/api.ts +++ b/src/api.ts @@ -32,6 +32,13 @@ export { validateTournamentPairing, } from '../convex/_model/tournamentPairings'; +// Lists +export { + type List, + ListActionKey, + type ListId, +} from '../convex/_model/lists'; + // Match Result Comments export { type DeepMatchResultComment as MatchResultComment, diff --git a/src/components/ContextMenu/ContextMenu.types.ts b/src/components/ContextMenu/ContextMenu.types.ts index 89f5db29..ebbd7b1b 100644 --- a/src/components/ContextMenu/ContextMenu.types.ts +++ b/src/components/ContextMenu/ContextMenu.types.ts @@ -1,11 +1,13 @@ import { MouseEvent } from 'react'; +import { ElementIntent } from 'node_modules/@ianpaschal/combat-command-components/dist/types'; import { + ListActionKey, TournamentActionKey, TournamentCompetitorActionKey, TournamentRegistrationActionKey, } from '~/api'; -import { ElementIntent } from '~/types/componentLib'; +// import { ElementIntent } from '~/types/componentLib'; export type Action = { handler: (e?: MouseEvent) => void; @@ -14,7 +16,7 @@ export type Action = { intent?: ElementIntent; }; -export type ActionKey = TournamentActionKey | TournamentCompetitorActionKey | TournamentRegistrationActionKey; +export type ActionKey = ListActionKey | TournamentActionKey | TournamentCompetitorActionKey | TournamentRegistrationActionKey; export type ActionDefinition = Action & { key: T; diff --git a/src/components/FileButton/FileButton.module.scss b/src/components/FileButton/FileButton.module.scss new file mode 100644 index 00000000..faf32294 --- /dev/null +++ b/src/components/FileButton/FileButton.module.scss @@ -0,0 +1,5 @@ +@use "/src/style/flex"; + +.FileButton { + @include flex.column; +} diff --git a/src/components/FileButton/FileButton.tsx b/src/components/FileButton/FileButton.tsx new file mode 100644 index 00000000..9f2f6754 --- /dev/null +++ b/src/components/FileButton/FileButton.tsx @@ -0,0 +1,35 @@ +import { ChangeEvent, useId } from 'react'; +import { Button, ButtonProps } from '@ianpaschal/combat-command-components'; + +export interface FileButtonProps extends Omit { + accept?: string[]; + onChange?: (files: FileList) => void; +} + +export const FileButton = ({ + accept = [], + onChange, + ...props +}: FileButtonProps): JSX.Element => { + const id = useId(); + const handleChange = async (event: ChangeEvent): Promise => { + if (!event.target.files || event.target.files.length === 0) { + return; + } + onChange?.(event.target.files); + }; + return ( + <> +