diff --git a/.eslintrc.cjs b/.eslintrc.cjs index e3234f9e..17bbd838 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -24,13 +24,14 @@ module.exports = { rules: { // Style - '@stylistic/js/arrow-parens': ["error", "always"], - '@stylistic/js/arrow-spacing': ["error", { "before": true, "after": true }], - '@stylistic/js/brace-style': "error", - '@stylistic/js/comma-dangle': ["error", "always-multiline"], - '@stylistic/js/eol-last': ["error", "always"], - '@stylistic/js/jsx-quotes': ["error", "prefer-double"], - '@stylistic/js/no-multi-spaces': "error", + '@stylistic/js/arrow-parens': ['error', 'always'], + '@stylistic/js/arrow-spacing': ['error', { 'before': true, 'after': true }], + '@stylistic/js/brace-style': 'error', + '@stylistic/js/comma-dangle': ['error', 'always-multiline'], + '@stylistic/js/eol-last': ['error', 'always'], + '@stylistic/js/implicit-arrow-linebreak': ['error', 'beside'], + '@stylistic/js/jsx-quotes': ['error', 'prefer-double'], + '@stylistic/js/no-multi-spaces': 'error', '@stylistic/js/no-multiple-empty-lines': ['error', { max: 1, maxBOF: 0, maxEOF: 0 }], '@stylistic/ts/indent': ['error', 2], '@stylistic/ts/object-curly-spacing': ['error', 'always'], @@ -42,20 +43,20 @@ module.exports = { 'quotes': 'off', '@typescript-eslint/quotes': ['error', 'single'], 'no-unused-vars': 'off', - '@typescript-eslint/no-unused-vars': ['warn', { 'argsIgnorePattern': '^_' }], + '@typescript-eslint/no-unused-vars': ['warn', { 'argsIgnorePattern': '^_', 'destructuredArrayIgnorePattern': '^_', 'varsIgnorePattern': '^_' }], // Misc. - "curly": ["error"], - "arrow-body-style": ["error", "as-needed"], - "no-console": ["warn", { allow: ["warn", "error", "info"] }], - "@typescript-eslint//explicit-function-return-type": "off", + 'curly': ['error'], + 'arrow-body-style': ['error', 'as-needed'], + 'no-console': ['warn', { allow: ['warn', 'error', 'info'] }], + '@typescript-eslint/explicit-function-return-type': 'off', // Plugin configurations 'import-newlines/enforce': ['error', 2], // ENABLE LATER // 'import-newlines/enforce': ['error', { // items: 2, - // "max-len": 100, + // 'max-len': 100, // }], 'react-refresh/only-export-components': [ 'warn', { allowConstantExport: true } ], 'simple-import-sort/exports': 'error', @@ -75,21 +76,21 @@ module.exports = { 'react/display-name': 'off', // 'react/jsx-sort-props': [ 'error', { // callbacksLast: true, - // multiline: "last", + // multiline: 'last', // reservedFirst: true, // ignoreCase: true, // }], - // "react/jsx-max-props-per-line": ["error", { + // 'react/jsx-max-props-per-line': ['error', { // maximum: 1, - // when: "multiline" + // when: 'multiline' // }] }, overrides: [ { // enable the rule specifically for TypeScript files - "files": ["convex/**/*.ts"], - "rules": { - "@typescript-eslint/explicit-function-return-type": "error", + 'files': ['convex/**/*.ts'], + 'rules': { + '@typescript-eslint/explicit-function-return-type': 'error', }, }, ], diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index 4a577346..ee3ec3af 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,9 @@ 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_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 +54,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 +79,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_listComments__helpers_deepenListComment from "../_model/listComments/_helpers/deepenListComment.js"; +import type * as _model_listComments_index from "../_model/listComments/index.js"; +import type * as _model_listComments_mutations_createListComment from "../_model/listComments/mutations/createListComment.js"; +import type * as _model_listComments_table from "../_model/listComments/table.js"; +import type * as _model_listComments_types from "../_model/listComments/types.js"; +import type * as _model_lists__helpers_checkListSubmittedOnTime from "../_model/lists/_helpers/checkListSubmittedOnTime.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 +133,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"; @@ -291,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 listComments from "../listComments.js"; import type * as lists from "../lists.js"; import type * as matchResultComments from "../matchResultComments.js"; import type * as matchResultLikes from "../matchResultLikes.js"; @@ -336,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; @@ -356,7 +371,9 @@ 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/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; @@ -366,10 +383,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; @@ -395,11 +408,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/listComments/_helpers/deepenListComment": typeof _model_listComments__helpers_deepenListComment; + "_model/listComments/index": typeof _model_listComments_index; + "_model/listComments/mutations/createListComment": typeof _model_listComments_mutations_createListComment; + "_model/listComments/table": typeof _model_listComments_table; + "_model/listComments/types": typeof _model_listComments_types; + "_model/lists/_helpers/checkListSubmittedOnTime": typeof _model_lists__helpers_checkListSubmittedOnTime; "_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; @@ -437,6 +462,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; @@ -606,6 +633,7 @@ declare const fullApi: ApiFromModules<{ http: typeof http; leagueRankings: typeof leagueRankings; leagues: typeof leagues; + listComments: typeof listComments; 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..7fcc0f7b --- /dev/null +++ b/convex/_model/files/actions/convertImageToPdf.ts @@ -0,0 +1,56 @@ +import { PDFDocument } from '@pdfme/pdf-lib'; +import { + ConvexError, + Infer, + v, +} from 'convex/values'; + +import { Id } from '../../../_generated/dataModel'; +import { ActionCtx } from '../../../_generated/server'; +import { getErrorMessage } from '../../common/errors'; + +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({ message: `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(getErrorMessage('FILE_NOT_FOUND')); + } + + 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/index.ts b/convex/_model/files/index.ts index 4ae03689..1185ffb8 100644 --- a/convex/_model/files/index.ts +++ b/convex/_model/files/index.ts @@ -1,3 +1,11 @@ +export { + convertImageToPdf, + convertImageToPdfArgs, +} from './actions/convertImageToPdf'; +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..130d21f6 --- /dev/null +++ b/convex/_model/files/queries/getFileMetadata.ts @@ -0,0 +1,28 @@ +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 => { + 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/listComments/_helpers/deepenListComment.ts b/convex/_model/listComments/_helpers/deepenListComment.ts new file mode 100644 index 00000000..6557268f --- /dev/null +++ b/convex/_model/listComments/_helpers/deepenListComment.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 deepenListComment = async ( + ctx: QueryCtx, + doc: Doc<'listComments'>, +) => { + 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/listComments/index.ts b/convex/_model/listComments/index.ts new file mode 100644 index 00000000..4a9f168d --- /dev/null +++ b/convex/_model/listComments/index.ts @@ -0,0 +1,7 @@ +export * from './types'; + +// Mutations +export { + createListComment, + createListCommentArgs, +} from './mutations/createListComment'; diff --git a/convex/_model/listComments/mutations/createListComment.ts b/convex/_model/listComments/mutations/createListComment.ts new file mode 100644 index 00000000..32dcb342 --- /dev/null +++ b/convex/_model/listComments/mutations/createListComment.ts @@ -0,0 +1,33 @@ +import { Infer, v } from 'convex/values'; + +import { Id } from '../../../_generated/dataModel'; +import { MutationCtx } from '../../../_generated/server'; +import { checkAuth } from '../../common/_helpers/checkAuth'; +import { getDocStrict } from '../../common/_helpers/getDocStrict'; +import { checkUserIsTournamentOrganizer } from '../../tournamentOrganizers'; +import { editableFields } from '../table'; + +export const createListCommentArgs = v.object({ + ...editableFields, +}); + +export const createListComment = async ( + ctx: MutationCtx, + args: Infer, +): Promise> => { + const userId = await checkAuth(ctx); + const { control, ...restArgs } = args; + const list = await getDocStrict(ctx, args.listId); + + let isOrganizer = false; + if (list.tournamentRegistrationId) { + const registration = await getDocStrict(ctx, list.tournamentRegistrationId); + isOrganizer = await checkUserIsTournamentOrganizer(ctx, registration.tournamentId, userId); + } + + return await ctx.db.insert('listComments', { + ...restArgs, + control: isOrganizer ? control : undefined, + userId, + }); +}; diff --git a/convex/_model/listComments/table.ts b/convex/_model/listComments/table.ts new file mode 100644 index 00000000..daff4c3a --- /dev/null +++ b/convex/_model/listComments/table.ts @@ -0,0 +1,24 @@ +import { defineTable } from 'convex/server'; +import { v } from 'convex/values'; + +export const editableFields = { + control: v.optional(v.union(v.literal('approved'), v.literal('rejected'))), + listId: v.id('lists'), + body: v.string(), + parentId: v.optional(v.id('listComments')), +}; + +/** + * 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/listComments/types.ts b/convex/_model/listComments/types.ts new file mode 100644 index 00000000..404dae9a --- /dev/null +++ b/convex/_model/listComments/types.ts @@ -0,0 +1,9 @@ +import { Infer } from 'convex/values'; + +import { deepenListComment } from './_helpers/deepenListComment'; +import { createListCommentArgs } from './mutations/createListComment'; +import { Id } from '../../_generated/dataModel'; + +export type ListComment = Awaited>; +export type ListCommentCreateArgs = Infer; +export type ListCommentId = Id<'listComments'>; diff --git a/convex/_model/lists/_helpers/checkListSubmittedOnTime.ts b/convex/_model/lists/_helpers/checkListSubmittedOnTime.ts new file mode 100644 index 00000000..668c1e67 --- /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/deepenList.ts b/convex/_model/lists/_helpers/deepenList.ts index e7a6daff..8a01eac3 100644 --- a/convex/_model/lists/_helpers/deepenList.ts +++ b/convex/_model/lists/_helpers/deepenList.ts @@ -1,40 +1,72 @@ -import { GameSystem } from '@ianpaschal/combat-command-game-systems/common'; +import { getAuthUserId } from '@convex-dev/auth/server'; 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 { FlamesOfWarV4 } from '../../gameSystems'; +import { getFileUrl } from '../../files'; +import { deepenListComment } from '../../listComments/_helpers/deepenListComment'; +import { checkUsersAreTeammates } from '../../tournamentCompetitors/_helpers/checkUsersAreTeammates'; +import { checkUserIsTournamentOrganizer } from '../../tournamentOrganizers'; +import { getUser } from '../../users'; +import { checkListSubmittedOnTime } from './checkListSubmittedOnTime'; +import { getAvailableActions } from './getAvailableActions'; /* eslint-disable @typescript-eslint/explicit-function-return-type */ /** * Deepens a list by joining additional relevant data and adding computed fields. - * + * Returns null if the list is not visible to the current user. + * * @remarks * This method's return type is, by nature, the definition of a deep list. - * + * * @param ctx - Convex query context - * @param tournament - Raw list document - * @returns A deep list + * @param doc - Raw list document + * @returns A deep list, or null if not visible */ 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), - }; + const user = await getUser(ctx, { id: doc.userId }); + if (!user) { + throw new ConvexError(getErrorMessage('USER_NOT_FOUND')); } - - // If no matcher found, throw an error: - throw new ConvexError(getErrorMessage('CANNOT_ADD_ANOTHER_PLAYER')); - -}; -/** - * Deep list with additional joined data and computed fields. - */ -export type DeepList = Awaited>; + const listCommentResults = await ctx.db.query('listComments') + .withIndex('by_list', (q) => q.eq('listId', doc._id)) + .collect(); + const comments = await Promise.all(listCommentResults.map((c) => deepenListComment(ctx, c))); + const lastControlComment = [...comments].reverse().find((c) => c.control !== undefined) ?? null; + + // Visibility check + if (doc.tournamentRegistrationId) { + const currentUserId = await getAuthUserId(ctx); + const tournamentRegistration = await getDocStrict(ctx, doc.tournamentRegistrationId); + const tournament = await getDocStrict(ctx, tournamentRegistration.tournamentId); + const authorizedRoles = await Promise.all([ + checkUserIsTournamentOrganizer(ctx, tournament._id, currentUserId), + checkUsersAreTeammates(ctx, tournamentRegistration.tournamentCompetitorId, tournamentRegistration.userId, currentUserId), + doc.userId === currentUserId, + ]); + const isAuthorized = authorizedRoles.some(Boolean); + const isApproved = lastControlComment?.control === 'approved'; + const visible = isAuthorized || (tournament.listsRevealed && isApproved); + if (!visible) { + return null; + } + } + + return { + ...doc, + availableActions: await getAvailableActions(ctx, doc), + lastControlComment, + displayName: undefined, // Future: Set displayName based on extracted list data + comments, + onTime: await checkListSubmittedOnTime(ctx, doc), + user, + fileUrl: await getFileUrl(ctx, { id: doc.storageId }), + }; +}; +// 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..658667c6 --- /dev/null +++ b/convex/_model/lists/_helpers/getAvailableActions.ts @@ -0,0 +1,74 @@ +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'; +import { checkUserIsRegistered } from '../../tournamentRegistrations'; + +export enum ListActionKey { + /** Delete this list. */ + Delete = 'delete', + /** Download this list's file. */ + Download = 'download', + /** Edit this list. */ + Update = 'update', + /** Add a comment to this list. */ + Comment = 'comment', + /** Approve or reject this list. */ + Approve = 'approve', +} + +/** + * 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; + let isPlayer = 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); + isPlayer = await checkUserIsRegistered(ctx, registration.tournamentId, 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.Update); + actions.push(ListActionKey.Delete); + } + + if (isOrganizer || isPlayer) { + actions.push(ListActionKey.Comment); + } + + if (isOrganizer) { + actions.push(ListActionKey.Approve); + } + + 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..4bd3c39e --- /dev/null +++ b/convex/_model/lists/mutations/createList.ts @@ -0,0 +1,34 @@ +import { Infer, v } from 'convex/values'; + +import { Id } from '../../../_generated/dataModel'; +import { MutationCtx } from '../../../_generated/server'; +import { checkAuth } from '../../common/_helpers/checkAuth'; +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> => { + const userId = await checkAuth(ctx); + + 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, + userId, + ...computedFields, + }); +}; diff --git a/convex/_model/lists/mutations/deleteList.ts b/convex/_model/lists/mutations/deleteList.ts new file mode 100644 index 00000000..e3013713 --- /dev/null +++ b/convex/_model/lists/mutations/deleteList.ts @@ -0,0 +1,27 @@ +import { + ConvexError, + Infer, + v, +} from 'convex/values'; + +import { MutationCtx } from '../../../_generated/server'; +import { getDocStrict } from '../../common/_helpers/getDocStrict'; +import { getErrorMessage } from '../../common/errors'; +import { getAvailableActions, ListActionKey } from '../_helpers/getAvailableActions'; + +export const deleteListArgs = v.object({ + id: v.id('lists'), +}); + +export const deleteList = async ( + ctx: MutationCtx, + args: Infer, +): Promise => { + const list = await getDocStrict(ctx, args.id); + const availableActions = await getAvailableActions(ctx, list); + if (!availableActions.includes(ListActionKey.Delete)) { + throw new ConvexError(getErrorMessage('USER_DOES_NOT_HAVE_PERMISSION')); + } + + 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..323bb1a3 --- /dev/null +++ b/convex/_model/lists/mutations/updateList.ts @@ -0,0 +1,39 @@ +import { + ConvexError, + Infer, + v, +} from 'convex/values'; + +import { MutationCtx } from '../../../_generated/server'; +import { getDocStrict } from '../../common/_helpers/getDocStrict'; +import { getErrorMessage } from '../../common/errors'; +import { getAvailableActions, ListActionKey } from '../_helpers/getAvailableActions'; +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, + userId: _userId, + tournamentRegistrationId: _tournamentRegistrationId, + ...updated + } = args; + + const list = await getDocStrict(ctx, id); + const availableActions = await getAvailableActions(ctx, list); + if (!availableActions.includes(ListActionKey.Update)) { + throw new ConvexError(getErrorMessage('USER_DOES_NOT_HAVE_PERMISSION')); + } + + 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..3678f00d 100644 --- a/convex/_model/lists/queries/getList.ts +++ b/convex/_model/lists/queries/getList.ts @@ -1,7 +1,8 @@ import { Infer, v } from 'convex/values'; import { QueryCtx } from '../../../_generated/server'; -import { deepenList, DeepList } from '../_helpers/deepenList'; +import { deepenList } from '../_helpers/deepenList'; +import { List } from '../types'; export const getListArgs = v.object({ id: v.id('lists'), @@ -9,7 +10,7 @@ export const getListArgs = v.object({ /** * Gets a list by ID. - * + * * @param ctx - Convex query context * @param args - Convex query args * @param args.id - ID of the list @@ -18,10 +19,10 @@ 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); + return await deepenList(ctx, result); }; diff --git a/convex/_model/lists/queries/getListsByTournamentRegistration.ts b/convex/_model/lists/queries/getListsByTournamentRegistration.ts new file mode 100644 index 00000000..82c899b7 --- /dev/null +++ b/convex/_model/lists/queries/getListsByTournamentRegistration.ts @@ -0,0 +1,21 @@ +import { Infer, v } from 'convex/values'; + +import { QueryCtx } from '../../../_generated/server'; +import { notNullOrUndefined } from '../../common/_helpers/notNullOrUndefined'; +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((r) => deepenList(ctx, r))); + 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..e2c4923a --- /dev/null +++ b/convex/_model/lists/queries/getListsByUser.ts @@ -0,0 +1,21 @@ +import { Infer, v } from 'convex/values'; + +import { QueryCtx } from '../../../_generated/server'; +import { notNullOrUndefined } from '../../common/_helpers/notNullOrUndefined'; +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((r) => deepenList(ctx, r))); + 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..6a15524a --- /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 = NonNullable>>; +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..4f50ecd7 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('tournamentPairings')), + tournamentCompetitorId: v.optional(v.id('tournamentCompetitors')), + })), }); 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..56c41b3c --- /dev/null +++ b/convex/_model/tournamentCompetitors/_helpers/checkUsersAreTeammates.ts @@ -0,0 +1,26 @@ +import { Id } from '../../../_generated/dataModel'; +import { QueryCtx } from '../../../_generated/server'; + +export const checkUsersAreTeammates = async ( + ctx: QueryCtx, + tournamentCompetitorId: Id<'tournamentCompetitors'>, + user0Id: Id<'users'> | null, + user1Id: Id<'users'> | null, +): Promise => { + if (!user0Id || !user1Id) { + return false; + } + const [record0, record1] = await Promise.all([ + ctx.db.query('tournamentRegistrations') + .withIndex('by_tournament_competitor_user', (q) => q + .eq('tournamentCompetitorId', tournamentCompetitorId) + .eq('userId', user0Id)) + .first(), + ctx.db.query('tournamentRegistrations') + .withIndex('by_tournament_competitor_user', (q) => q + .eq('tournamentCompetitorId', tournamentCompetitorId) + .eq('userId', user1Id)) + .first(), + ]); + return record0 !== null && record1 !== null; +}; 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..c2e3e1d5 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..2ae2ea91 100644 --- a/convex/_model/tournamentRegistrations/_helpers/getAvailableActions.ts +++ b/convex/_model/tournamentRegistrations/_helpers/getAvailableActions.ts @@ -13,11 +13,8 @@ export enum TournamentRegistrationActionKey { /** Delete this TournamentRegistration (and TournamentCompetitor if no remaining players). */ Delete = 'delete', - // TODO - // ApproveList = 'approveList', - - // TODO - // RejectList = 'rejectList', + /** Create a new list for this TournamentRegistration. */ + CreateList = 'createList', // TODO // Transfer = 'transfer', @@ -26,12 +23,8 @@ export enum TournamentRegistrationActionKey { ToggleActive = 'toggleActive', // ---- Player Actions ---- - /** Delete own TournamentRegistration (and TournamentCompetitor if no remaining players). */ Leave = 'leave', - - // TODO - // SubmitList = 'submitList', } /** @@ -75,9 +68,9 @@ export const getAvailableActions = async ( actions.push(TournamentRegistrationActionKey.Edit); } - // if (isOrganizer) { - // actions.push(TournamentRegistrationActionKey.ApproveList); - // } + if (isOrganizer || isCaptain || isSelf) { + actions.push(TournamentRegistrationActionKey.CreateList); + } if ((isOrganizer || isCaptain) && !isSelf && tournament.status === 'published' && isTeamTournament) { actions.push(TournamentRegistrationActionKey.Delete); @@ -87,14 +80,6 @@ export const getAvailableActions = async ( actions.push(TournamentRegistrationActionKey.Leave); } - // if (isOrganizer) { - // actions.push(TournamentRegistrationActionKey.RejectList); - // } - - // if (isOrganizer || ((isCaptain || isPlayer) && isListSubmissionOpen)) { - // actions.push(TournamentRegistrationActionKey.SubmitList); - // } - if ((isOrganizer || isCaptain) && isTeamTournament) { actions.push(TournamentRegistrationActionKey.ToggleActive); } 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/_model/utils/getTournamentRegistrationsCsv.ts b/convex/_model/utils/getTournamentRegistrationsCsv.ts index 3844b133..44058751 100644 --- a/convex/_model/utils/getTournamentRegistrationsCsv.ts +++ b/convex/_model/utils/getTournamentRegistrationsCsv.ts @@ -29,8 +29,7 @@ export const getTournamentRegistrationsCsv = async ( ); const header = 'givenName,familyName,email'; - const rows = users.map((u) => - `${u.givenName ?? ''},${u.familyName ?? ''},${u.email}`, + const rows = users.map((u) => `${u.givenName ?? ''},${u.familyName ?? ''},${u.email}`, ); return [header, ...rows].join('\n'); diff --git a/convex/files.ts b/convex/files.ts index 2f3aa268..cf11da61 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,17 @@ 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'), +}); diff --git a/convex/listComments.ts b/convex/listComments.ts new file mode 100644 index 00000000..1825ebf3 --- /dev/null +++ b/convex/listComments.ts @@ -0,0 +1,7 @@ +import { mutation } from './_generated/server'; +import * as model from './_model/listComments'; + +export const createListComment = mutation({ + args: model.createListCommentArgs, + handler: model.createListComment, +}); 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..9e8509a4 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 listComments from './_model/listComments/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, + listComments, lists, matchResultComments, matchResultLikes, diff --git a/package-lock.json b/package-lock.json index 6a84880d..c623ffe0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,9 +17,10 @@ "@dnd-kit/sortable": "^10.0.0", "@fontsource/figtree": "^5.1.0", "@hookform/resolvers": "^3.9.0", - "@ianpaschal/combat-command-components": "^1.8.4", + "@ianpaschal/combat-command-components": "^1.11.2", "@ianpaschal/combat-command-game-systems": "^1.4.1", "@mapbox/search-js-core": "^1.0.0-beta.25", + "@pdfme/pdf-lib": "^1.17.1", "@radix-ui/colors": "^3.0.0", "@react-hook/window-size": "^3.1.1", "@tanstack/react-query": "^5.59.15", @@ -42,11 +43,11 @@ "image-blob-reduce": "^4.1.0", "iso-3166-2": "^1.0.0", "jest-environment-jsdom": "^29.7.0", - "lucide-react": "^0.553.0", + "lucide-react": "^0.572.0", "nanoid": "^5.1.5", "nuqs": "^2.7.3", "oslo": "^1.2.1", - "pdfjs-dist": "^4.4.168", + "pdfjs-dist": "^5.4.624", "pica": "^9.0.1", "qs": "^6.14.0", "radix-ui": "^1.4.2", @@ -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" @@ -1126,6 +1126,23 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { "version": "0.25.2", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", @@ -1191,9 +1208,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1415,16 +1432,16 @@ } }, "node_modules/@gerrit0/mini-shiki": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.6.0.tgz", - "integrity": "sha512-KaeJvPNofTEZR9EzVNp/GQzbQqkGfjiu6k3CXKvhVTX+8OoAKSX/k7qxLKOX3B0yh2XqVAc93rsOu48CGt2Qug==", + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.22.0.tgz", + "integrity": "sha512-jMpciqEVUBKE1QwU64S4saNMzpsSza6diNCk4MWAeCxO2+LFi2FIFmL2S0VDLzEJCxuvCbU783xi8Hp/gkM5CQ==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/engine-oniguruma": "^3.6.0", - "@shikijs/langs": "^3.6.0", - "@shikijs/themes": "^3.6.0", - "@shikijs/types": "^3.6.0", + "@shikijs/engine-oniguruma": "^3.22.0", + "@shikijs/langs": "^3.22.0", + "@shikijs/themes": "^3.22.0", + "@shikijs/types": "^3.22.0", "@shikijs/vscode-textmate": "^10.0.2" } }, @@ -1500,12 +1517,12 @@ "license": "BSD-3-Clause" }, "node_modules/@ianpaschal/combat-command-components": { - "version": "1.8.4", - "resolved": "https://npm.pkg.github.com/download/@ianpaschal/combat-command-components/1.8.4/1707cf615f761a5d1151cb3c264901d57693eb4a", - "integrity": "sha512-D8K7TDE32YtfI2FSptFfrwWfs1A8G7Q+X/UqosxrCbE/104yTRE/0nOYfVpvlVe6V6iD+ApDyR01Fse7qSmV0A==", + "version": "1.11.2", + "resolved": "https://npm.pkg.github.com/download/@ianpaschal/combat-command-components/1.11.2/429ffe7b5687ce467dbb6129393c6537fc8bbd2a", + "integrity": "sha512-EKdz7CjyslsKIGI7/MHqt6SshmKxxGbWevCT+Nt/eRa9ul482WBIPtLuSdAGjhSa8iuPDzOE4NAj6sYF7Qzpiw==", "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", @@ -1515,18 +1532,20 @@ "radix-ui": "^1.4.3" }, "peerDependencies": { + "pdfjs-dist": "^5.4.0", "react": ">=18.0.0", "react-dom": ">=18.0.0", + "react-pdf": "^10.0.0", "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==", + "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 +1558,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 +1825,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 +1868,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 +1888,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 +1908,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 +1928,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 +1948,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 +1968,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 +1988,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 +2008,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 +2028,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 +2068,10 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@node-rs/argon2": { @@ -2889,6 +2976,37 @@ "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/@pdfme/pdf-lib": { + "version": "1.18.4", + "resolved": "https://registry.npmjs.org/@pdfme/pdf-lib/-/pdf-lib-1.18.4.tgz", + "integrity": "sha512-i9XGkKCCb2wHLfJC/wUZ221/58o4unG4dGvnS+Tk8kOQBVMf+DCXzIqs0SGDsF3DrEpXHIhaW2iscWNjM0qNtA==", + "license": "MIT", + "dependencies": { + "@pdf-lib/standard-fonts": "^1.0.0", + "@pdf-lib/upng": "^1.0.1", + "color": "^4.2.3", + "node-html-better-parser": "^1.4.0", + "pako": "^1.0.11" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -4813,40 +4931,40 @@ } }, "node_modules/@shikijs/engine-oniguruma": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.6.0.tgz", - "integrity": "sha512-nmOhIZ9yT3Grd+2plmW/d8+vZ2pcQmo/UnVwXMUXAKTXdi+LK0S08Ancrz5tQQPkxvjBalpMW2aKvwXfelauvA==", + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.22.0.tgz", + "integrity": "sha512-DyXsOG0vGtNtl7ygvabHd7Mt5EY8gCNqR9Y7Lpbbd/PbJvgWrqaKzH1JW6H6qFkuUa8aCxoiYVv8/YfFljiQxA==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.6.0", + "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "node_modules/@shikijs/langs": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.6.0.tgz", - "integrity": "sha512-IdZkQJaLBu1LCYCwkr30hNuSDfllOT8RWYVZK1tD2J03DkiagYKRxj/pDSl8Didml3xxuyzUjgtioInwEQM/TA==", + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.22.0.tgz", + "integrity": "sha512-x/42TfhWmp6H00T6uwVrdTJGKgNdFbrEdhaDwSR5fd5zhQ1Q46bHq9EO61SCEWJR0HY7z2HNDMaBZp8JRmKiIA==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.6.0" + "@shikijs/types": "3.22.0" } }, "node_modules/@shikijs/themes": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.6.0.tgz", - "integrity": "sha512-Fq2j4nWr1DF4drvmhqKq8x5vVQ27VncF8XZMBuHuQMZvUSS3NBgpqfwz/FoGe36+W6PvniZ1yDlg2d4kmYDU6w==", + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.22.0.tgz", + "integrity": "sha512-o+tlOKqsr6FE4+mYJG08tfCFDS+3CG20HbldXeVoyP+cYSUxDhrFf3GPjE60U55iOkkjbpY2uC3It/eeja35/g==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.6.0" + "@shikijs/types": "3.22.0" } }, "node_modules/@shikijs/types": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.6.0.tgz", - "integrity": "sha512-cLWFiToxYu0aAzJqhXTQsFiJRTFDAGl93IrMSBNaGSzs7ixkLfdG6pH11HipuWFGW5vyx4X47W8HDQ7eSrmBUg==", + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.22.0.tgz", + "integrity": "sha512-491iAekgKDBFE67z70Ok5a8KBMsQ2IJwOWw3us/7ffQkIBCyOQfm/aNwVMBUriP02QshIfgHCBSIYAl3u2eWjg==", "dev": true, "license": "MIT", "dependencies": { @@ -5517,6 +5635,42 @@ } } }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.0.tgz", + "integrity": "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.56.0", + "@typescript-eslint/types": "^8.56.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service/node_modules/@typescript-eslint/types": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", + "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@typescript-eslint/scope-manager": { "version": "7.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", @@ -5535,6 +5689,23 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz", + "integrity": "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, "node_modules/@typescript-eslint/type-utils": { "version": "7.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", @@ -5630,16 +5801,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.0.tgz", - "integrity": "sha512-8S9hXau6nQ/sYVtC3D6ISIDoJzS1NsCK+gluVhLN2YkBPX+/1wkwyUiDKnxRh15579WoOIyVWnoyIf3yGI9REw==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz", + "integrity": "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.32.0", - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/typescript-estree": "8.32.0" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5649,19 +5820,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.0.tgz", - "integrity": "sha512-jc/4IxGNedXkmG4mx4nJTILb6TMjL66D41vyeaPWvDUmeYQzF3lKtN15WsAeTr65ce4mPxwopPSo1yUUAWw0hQ==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz", + "integrity": "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0" + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5672,9 +5843,9 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.0.tgz", - "integrity": "sha512-O5Id6tGadAZEMThM6L9HmVf5hQUXNSxLVKeGJYWNhhVseps/0LddMkp7//VDkzwJ69lPL0UmZdcZwggj9akJaA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", + "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", "dev": true, "license": "MIT", "engines": { @@ -5686,20 +5857,21 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.0.tgz", - "integrity": "sha512-pU9VD7anSCOIoBFnhTGfOzlVFQIA1XXiQpH/CezqOBaDppRwTglJzCC6fUQGpfwey4T183NKhF1/mfatYmjRqQ==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz", + "integrity": "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" + "@typescript-eslint/project-service": "8.56.0", + "@typescript-eslint/tsconfig-utils": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5709,18 +5881,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.0.tgz", - "integrity": "sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz", + "integrity": "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.0", - "eslint-visitor-keys": "^4.2.0" + "@typescript-eslint/types": "8.56.0", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5730,10 +5902,23 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/utils/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@typescript-eslint/utils/node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", "dev": true, "license": "MIT", "engines": { @@ -6025,9 +6210,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -6139,18 +6324,20 @@ } }, "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -6684,6 +6871,19 @@ "node": ">=6" } }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -6702,6 +6902,16 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/colord": { "version": "2.9.3", "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", @@ -7176,7 +7386,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -7486,9 +7695,9 @@ } }, "node_modules/es-abstract": { - "version": "1.23.9", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", - "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", "dev": true, "license": "MIT", "dependencies": { @@ -7496,18 +7705,18 @@ "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", - "call-bound": "^1.0.3", + "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", + "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.0", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", @@ -7519,21 +7728,24 @@ "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", + "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.0", + "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.3", + "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.3", + "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", @@ -7542,7 +7754,7 @@ "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.18" + "which-typed-array": "^1.1.19" }, "engines": { "node": ">= 0.4" @@ -7827,9 +8039,9 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", - "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", "dev": true, "license": "MIT", "dependencies": { @@ -7855,30 +8067,30 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.31.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", - "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", "dependencies": { "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.8", - "array.prototype.findlastindex": "^1.2.5", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.0", + "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", - "is-core-module": "^2.15.1", + "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", - "object.values": "^1.2.0", + "object.values": "^1.2.1", "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.8", + "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "engines": { @@ -8985,6 +9197,22 @@ "node": ">=12" } }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -9492,6 +9720,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -10320,9 +10561,9 @@ } }, "node_modules/lucide-react": { - "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==", + "version": "0.572.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.572.0.tgz", + "integrity": "sha512-GsRHauaTBqk+WziW9mW+SGEcdpvld2f2VdbZCAOyFZbTEOdNVLJq1nEt2qAKJbWeBqDnv7y1XH+sWF6Q6bjgUw==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -10367,6 +10608,15 @@ "source-map-js": "^1.2.0" } }, + "node_modules/make-cancellable-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/make-cancellable-promise/-/make-cancellable-promise-2.0.0.tgz", + "integrity": "sha512-3SEQqTpV9oqVsIWqAcmDuaNeo7yBO3tqPtqGRcKkEo0lrzD3wqbKG9mkxO65KoOgXqj+zH2phJ2LiAsdzlogSw==", + "license": "MIT", + "funding": { + "url": "https://github.com/wojtekmaj/make-cancellable-promise?sponsor=1" + } + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -10390,10 +10640,19 @@ "dev": true, "license": "ISC" }, + "node_modules/make-event-props": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/make-event-props/-/make-event-props-2.0.0.tgz", + "integrity": "sha512-G/hncXrl4Qt7mauJEXSg3AcdYzmpkIITTNl5I+rH9sog5Yw0kK6vseJjCaPfOXqOqQuPUP89Rkhfz5kPS8ijtw==", + "license": "MIT", + "funding": { + "url": "https://github.com/wojtekmaj/make-event-props?sponsor=1" + } + }, "node_modules/markdown-it": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", - "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", "dev": true, "license": "MIT", "dependencies": { @@ -10491,6 +10750,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge-refs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-2.0.0.tgz", + "integrity": "sha512-3+B21mYK2IqUWnd2EivABLT7ueDhb0b8/dGK8LoFQPrU61YITeCMn14F7y7qZafWNZhUEKb24cJdiT5Wxs3prg==", + "license": "MIT", + "funding": { + "url": "https://github.com/wojtekmaj/merge-refs?sponsor=1" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -10675,6 +10951,22 @@ "license": "MIT", "optional": true }, + "node_modules/node-html-better-parser": { + "version": "1.5.8", + "resolved": "https://registry.npmjs.org/node-html-better-parser/-/node-html-better-parser-1.5.8.tgz", + "integrity": "sha512-t/wAKvaTSKco43X+yf9+76RiMt18MtMmzd4wc7rKj+fWav6DV4ajDEKdWlLzSE8USDF5zr/06uGj0Wr/dGAFtw==", + "license": "MIT", + "dependencies": { + "html-entities": "^2.3.2" + } + }, + "node_modules/node-readable-to-web-readable-stream": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/node-readable-to-web-readable-stream/-/node-readable-to-web-readable-stream-0.4.2.tgz", + "integrity": "sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ==", + "license": "MIT", + "optional": true + }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -10975,6 +11267,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", @@ -11134,15 +11432,17 @@ } }, "node_modules/pdfjs-dist": { - "version": "4.10.38", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.10.38.tgz", - "integrity": "sha512-/Y3fcFrXEAsMjJXeL9J8+ZG9U01LbuWaYypvDW2ycW1jL269L3js3DVBjDJ0Up9Np1uqDXsDrRihHANhZOlwdQ==", + "version": "5.4.624", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.624.tgz", + "integrity": "sha512-sm6TxKTtWv1Oh6n3C6J6a8odejb5uO4A4zo/2dgkHuC0iu8ZMAXOezEODkVaoVp8nX1Xzr+0WxFJJmUr45hQzg==", "license": "Apache-2.0", + "peer": true, "engines": { - "node": ">=20" + "node": ">=20.16.0 || >=22.3.0" }, "optionalDependencies": { - "@napi-rs/canvas": "^0.1.65" + "@napi-rs/canvas": "^0.1.88", + "node-readable-to-web-readable-stream": "^0.4.2" } }, "node_modules/peberminta": { @@ -11604,9 +11904,9 @@ } }, "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -11816,20 +12116,61 @@ "dev": true, "license": "MIT" }, - "node_modules/react-promise-suspense": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/react-promise-suspense/-/react-promise-suspense-0.3.4.tgz", - "integrity": "sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==", + "node_modules/react-pdf": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-10.4.0.tgz", + "integrity": "sha512-JAuxBX8IXHXuGPVy7d1HbZpAaZF0VIExV3oJByJQM6OWtzn8e2jm6GHRIq7mVY2PKIpYgoLxoglVWm2ITtQ9FQ==", "license": "MIT", "dependencies": { - "fast-deep-equal": "^2.0.1" - } - }, - "node_modules/react-promise-suspense/node_modules/fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", - "license": "MIT" + "clsx": "^2.0.0", + "dequal": "^2.0.3", + "make-cancellable-promise": "^2.0.0", + "make-event-props": "^2.0.0", + "merge-refs": "^2.0.0", + "pdfjs-dist": "5.4.296", + "tiny-invariant": "^1.0.0", + "warning": "^4.0.0" + }, + "funding": { + "url": "https://github.com/wojtekmaj/react-pdf?sponsor=1" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-pdf/node_modules/pdfjs-dist": { + "version": "5.4.296", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz", + "integrity": "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.16.0 || >=22.3.0" + }, + "optionalDependencies": { + "@napi-rs/canvas": "^0.1.80" + } + }, + "node_modules/react-promise-suspense": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/react-promise-suspense/-/react-promise-suspense-0.3.4.tgz", + "integrity": "sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^2.0.1" + } + }, + "node_modules/react-promise-suspense/node_modules/fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", + "license": "MIT" }, "node_modules/react-refresh": { "version": "0.17.0", @@ -12327,9 +12668,9 @@ } }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -12499,6 +12840,21 @@ "dev": true, "license": "ISC" }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT" + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -12581,6 +12937,20 @@ "dev": true, "license": "MIT" }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -13261,9 +13631,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": { @@ -13284,9 +13654,9 @@ } }, "node_modules/table/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", "dependencies": { @@ -13350,6 +13720,12 @@ "dev": true, "license": "MIT" }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -13683,17 +14059,17 @@ } }, "node_modules/typedoc": { - "version": "0.28.5", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.5.tgz", - "integrity": "sha512-5PzUddaA9FbaarUzIsEc4wNXCiO4Ot3bJNeMF2qKpYlTmM9TTaSHQ7162w756ERCkXER/+o2purRG6YOAv6EMA==", + "version": "0.28.17", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.17.tgz", + "integrity": "sha512-ZkJ2G7mZrbxrKxinTQMjFqsCoYY6a5Luwv2GKbTnBCEgV2ihYm5CflA9JnJAwH0pZWavqfYxmDkFHPt4yx2oDQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@gerrit0/mini-shiki": "^3.2.2", + "@gerrit0/mini-shiki": "^3.17.0", "lunr": "^2.3.9", "markdown-it": "^14.1.0", "minimatch": "^9.0.5", - "yaml": "^2.7.1" + "yaml": "^2.8.1" }, "bin": { "typedoc": "bin/typedoc" @@ -13703,7 +14079,7 @@ "pnpm": ">= 10" }, "peerDependencies": { - "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x" + "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x" } }, "node_modules/typescript": { @@ -13981,14 +14357,14 @@ "license": "MIT" }, "node_modules/vite": { - "version": "7.1.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz", - "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "esbuild": "^0.25.0", + "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", @@ -14079,6 +14455,473 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, "node_modules/vite/node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -14210,6 +15053,15 @@ "node": ">=14" } }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -14459,16 +15311,19 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", - "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", "dev": true, "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { - "node": ">= 14" + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" } }, "node_modules/yn": { diff --git a/package.json b/package.json index eda77ee9..70718df9 100644 --- a/package.json +++ b/package.json @@ -29,9 +29,10 @@ "@dnd-kit/sortable": "^10.0.0", "@fontsource/figtree": "^5.1.0", "@hookform/resolvers": "^3.9.0", - "@ianpaschal/combat-command-components": "^1.8.4", + "@ianpaschal/combat-command-components": "^1.11.2", "@ianpaschal/combat-command-game-systems": "^1.4.1", "@mapbox/search-js-core": "^1.0.0-beta.25", + "@pdfme/pdf-lib": "^1.17.1", "@radix-ui/colors": "^3.0.0", "@react-hook/window-size": "^3.1.1", "@tanstack/react-query": "^5.59.15", @@ -54,11 +55,11 @@ "image-blob-reduce": "^4.1.0", "iso-3166-2": "^1.0.0", "jest-environment-jsdom": "^29.7.0", - "lucide-react": "^0.553.0", + "lucide-react": "^0.572.0", "nanoid": "^5.1.5", "nuqs": "^2.7.3", "oslo": "^1.2.1", - "pdfjs-dist": "^4.4.168", + "pdfjs-dist": "^5.4.624", "pica": "^9.0.1", "qs": "^6.14.0", "radix-ui": "^1.4.2", diff --git a/src/api.ts b/src/api.ts index 41724490..200c4fc7 100644 --- a/src/api.ts +++ b/src/api.ts @@ -32,6 +32,18 @@ export { validateTournamentPairing, } from '../convex/_model/tournamentPairings'; +// Lists +export { + type ListComment, + type ListCommentCreateArgs, + type ListCommentId, +} from '../convex/_model/listComments'; +export { + type List, + ListActionKey, + type ListId, +} from '../convex/_model/lists'; + // Match Result Comments export { type DeepMatchResultComment as MatchResultComment, diff --git a/src/components/CommentThread/CommentThread.module.scss b/src/components/CommentThread/CommentThread.module.scss new file mode 100644 index 00000000..a77d65b7 --- /dev/null +++ b/src/components/CommentThread/CommentThread.module.scss @@ -0,0 +1,54 @@ +@use "/src/style/flex"; +@use "/src/style/text"; + +.CommentThread { + display: grid; + grid-template-areas: + "header header icon" + ". body body" + ". . reply"; + grid-template-columns: 1.5rem 1fr 1.5rem; + grid-template-rows: 1.5rem auto auto; + gap: 0.5rem; // Same as --avatar-spacing on small + + margin: -0.5rem; + padding: 0.5rem; + + &_Header { + @include flex.row($gap: 0.5rem); + + grid-area: header; + } + + &_Icon { + display: flex; + grid-area: icon; + align-items: center; + justify-content: center; + + aspect-ratio: 1; + width: 100%; + + border-radius: 100%; + + svg { + width: 1rem; + height: 1rem; + } + } + + &_Body { + @include text.ui($muted: true); + + grid-area: body; + color: inherit; + } +} + +.CommentThreadEvent { + display: grid; + grid-template-areas: "icon header"; + grid-template-columns: 1.5rem 1fr; + grid-template-rows: auto; + column-gap: 0.5rem; // Same as --avatar-spacing on small +} diff --git a/src/components/CommentThread/CommentThread.tsx b/src/components/CommentThread/CommentThread.tsx new file mode 100644 index 00000000..58ab34d9 --- /dev/null +++ b/src/components/CommentThread/CommentThread.tsx @@ -0,0 +1,51 @@ +import { ReactElement } from 'react'; +import { ElementIntent, getStyleClassNames } from '@ianpaschal/combat-command-components'; +import clsx from 'clsx'; + +import { ListCommentId, User } from '~/api'; +import { Timestamp } from '~/components/generic/Timestamp'; +import { IdentityBadge } from '~/components/IdentityBadge'; + +import styles from './CommentThread.module.scss'; + +export type Comment = { + _creationTime: number; + _id: ListCommentId; + body: string; + replies?: Comment[]; + user: User; + icon?: ReactElement; + intent?: ElementIntent; +}; + +export interface CommentThreadProps { + className?: string; + comment: Comment; + + // FUTURE: Add depth (replies) + // FUTURE: Add reply button +} + +export const CommentThread = ({ + className, + comment, +}: CommentThreadProps): JSX.Element => ( +
+
+ + +
+ {comment.icon && ( +
+ {comment.icon} +
+ )} +
+ {comment.body} +
+
+); diff --git a/src/components/CommentThread/CommentThreadEvent.tsx b/src/components/CommentThread/CommentThreadEvent.tsx new file mode 100644 index 00000000..87c2b8c0 --- /dev/null +++ b/src/components/CommentThread/CommentThreadEvent.tsx @@ -0,0 +1,42 @@ +import { ReactElement, ReactNode } from 'react'; +import { ElementIntent, getStyleClassNames } from '@ianpaschal/combat-command-components'; +import clsx from 'clsx'; + +import { Timestamp } from '~/components/generic/Timestamp'; +import { Comment } from './CommentThread'; + +import styles from './CommentThread.module.scss'; + +type EventData = { + _creationTime: number; + replies?: Comment[]; + content: ReactNode; + icon: ReactElement; + intent?: ElementIntent; +}; + +export interface CommentThreadEventProps { + className?: string; + event: EventData; + + // FUTURE: Add depth (replies) + // FUTURE: Add reply button +} + +export const CommentThreadEvent = ({ + className, + event, +}: CommentThreadEventProps): JSX.Element => ( +
+
+ {event.icon} +
+
+ {event.content} + +
+
+); diff --git a/src/components/CommentThread/index.ts b/src/components/CommentThread/index.ts new file mode 100644 index 00000000..07b20d5c --- /dev/null +++ b/src/components/CommentThread/index.ts @@ -0,0 +1,9 @@ +export { + type Comment, + CommentThread, + type CommentThreadProps, +} from './CommentThread'; +export { + CommentThreadEvent, + type CommentThreadEventProps, +} from './CommentThreadEvent'; diff --git a/src/components/ContextMenu/ContextMenu.tsx b/src/components/ContextMenu/ContextMenu.tsx index c09f6eab..b6b7f5e0 100644 --- a/src/components/ContextMenu/ContextMenu.tsx +++ b/src/components/ContextMenu/ContextMenu.tsx @@ -1,8 +1,7 @@ +import { Button, ElementSize } from '@ianpaschal/combat-command-components'; import { Ellipsis } from 'lucide-react'; -import { Button } from '~/components/generic/Button'; import { PopoverMenu } from '~/components/generic/PopoverMenu'; -import { ElementSize } from '~/types/componentLib'; import { Action } from './ContextMenu.types'; export interface ContextMenuProps { diff --git a/src/components/ContextMenu/ContextMenu.types.ts b/src/components/ContextMenu/ContextMenu.types.ts index 89f5db29..aedd82fa 100644 --- a/src/components/ContextMenu/ContextMenu.types.ts +++ b/src/components/ContextMenu/ContextMenu.types.ts @@ -1,11 +1,12 @@ import { MouseEvent } from 'react'; +import { ElementIntent } from '@ianpaschal/combat-command-components'; import { + ListActionKey, TournamentActionKey, TournamentCompetitorActionKey, TournamentRegistrationActionKey, } from '~/api'; -import { ElementIntent } from '~/types/componentLib'; export type Action = { handler: (e?: MouseEvent) => void; @@ -14,7 +15,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/ListCommentForm/ListCommentForm.module.scss b/src/components/ListCommentForm/ListCommentForm.module.scss new file mode 100644 index 00000000..646c61bb --- /dev/null +++ b/src/components/ListCommentForm/ListCommentForm.module.scss @@ -0,0 +1,13 @@ +@use "/src/style/flex"; + +.ListCommentForm { + @include flex.column; + + &_CommentInput { + height: 2rem; + } + + &_SubmitButtons { + @include flex.row($xAlign: right); + } +} diff --git a/src/components/ListCommentForm/ListCommentForm.schema.ts b/src/components/ListCommentForm/ListCommentForm.schema.ts new file mode 100644 index 00000000..374e67fd --- /dev/null +++ b/src/components/ListCommentForm/ListCommentForm.schema.ts @@ -0,0 +1,26 @@ +import { z } from 'zod'; + +import { ListId } from '~/api'; + +export const schema = z.object({ + body: z.string(), + listId: z.string({ message: 'List is required.' }).transform((val) => val as ListId), +}); + +/** + * The output of successful form validation. + */ +export type SubmitData = z.infer; + +/** + * The internal form state before validation (may contain missing or intermediate values). + */ +export type FormData = { + body: string; + listId: ListId | null; +}; + +export const defaultValues: FormData = { + body: '', + listId: null, +}; diff --git a/src/components/ListCommentForm/ListCommentForm.tsx b/src/components/ListCommentForm/ListCommentForm.tsx new file mode 100644 index 00000000..38f20b42 --- /dev/null +++ b/src/components/ListCommentForm/ListCommentForm.tsx @@ -0,0 +1,125 @@ +import { useEffect } from 'react'; +import { SubmitHandler, useForm } from 'react-hook-form'; +import { Button } from '@ianpaschal/combat-command-components'; +import clsx from 'clsx'; +import { + MessageCircle, + MessageCircleCheck, + MessageCircleX, +} from 'lucide-react'; + +import { + List, + ListActionKey, + ListComment, + ListCommentCreateArgs, +} from '~/api'; +import { useAuth } from '~/components/AuthProvider'; +import { Form, FormField } from '~/components/generic/Form'; +import { InputTextArea } from '~/components/generic/InputTextArea'; +import { validateForm } from '~/utils/validateForm'; +import { + defaultValues, + FormData, + schema, + SubmitData, +} from './ListCommentForm.schema'; + +import styles from './ListCommentForm.module.scss'; + +export interface ListCommentFormProps { + className?: string; + disabled?: boolean; + existingValues?: ListComment | null; + forcedValues: Partial; + id?: string; + list: List; + onSubmit: (data: ListCommentCreateArgs) => void; + setDirty?: (dirty: boolean) => void; +} + +export const ListCommentForm = ({ + className, + disabled = false, + existingValues, + forcedValues, + id, + list, + onSubmit, + setDirty, +}: ListCommentFormProps): JSX.Element => { + const user = useAuth(); + const form = useForm({ + defaultValues: { + ...defaultValues, + ...existingValues, + ...forcedValues, + }, + mode: 'onSubmit', + }); + + const { formState: { isDirty } } = form; + useEffect(() => { + setDirty?.(isDirty); + return () => setDirty?.(false); + }, [isDirty, setDirty]); + + const handleSubmit: SubmitHandler = async (formData): Promise => { + const validFormData = validateForm(schema, { + ...formData, + ...forcedValues, + userId: user?._id, + }, form.setError); + if (validFormData) { + onSubmit({ ...validFormData }); + form.reset(); + } + }; + + const handleApprove = (): void => { + const validFormData = validateForm(schema, { + ...form.watch(), + ...forcedValues, + userId: user?._id, + }, form.setError); + if (validFormData) { + onSubmit({ ...validFormData, control: 'approved' }); + form.reset(); + } + }; + + const handleReject = (): void => { + const validFormData = validateForm(schema, { + ...form.watch(), + ...forcedValues, + userId: user?._id, + }, form.setError); + if (validFormData) { + onSubmit({ ...validFormData, control: 'rejected' }); + form.reset(); + } + }; + + return ( +
+ + + +
+
+
+ ); +}; diff --git a/src/components/ListCommentForm/index.ts b/src/components/ListCommentForm/index.ts new file mode 100644 index 00000000..ecd97f90 --- /dev/null +++ b/src/components/ListCommentForm/index.ts @@ -0,0 +1,7 @@ +export { + ListCommentForm, + type ListCommentFormProps, +} from './ListCommentForm'; +export { + type SubmitData as ListCommentSubmitData, +} from './ListCommentForm.schema'; diff --git a/src/components/ListDetails/ListDetails.module.scss b/src/components/ListDetails/ListDetails.module.scss new file mode 100644 index 00000000..23323b6e --- /dev/null +++ b/src/components/ListDetails/ListDetails.module.scss @@ -0,0 +1,91 @@ +@use "/src/style/flex"; +@use "/src/style/borders"; +@use "/src/style/corners"; + +.ListDetails { + display: grid; + grid-template-areas: "viewer" "activity" "comments"; + grid-template-columns: minmax(0, 1fr); + grid-template-rows: minmax(0, 1fr) auto auto; + gap: 1rem; + + width: 100vw; + max-width: 100%; + height: 100%; + + &_Viewer { + grid-area: viewer; + } + + &_Comments { + display: grid; + grid-area: comments; + grid-template-columns: minmax(0, 1fr); + grid-template-rows: 1fr auto; + + max-height: 20rem; + + &_List { + @include flex.column($gap: 1.25rem); + + padding: 1rem; + + &[data-inset="true"] { + padding: var(--modal-padding) var(--modal-padding) 1rem; + } + } + + &_Form { + @include flex.column; + + padding: 1rem; + + &[data-inset="true"] { + padding: 1rem var(--modal-padding) var(--modal-padding); + } + } + } + + &[data-orientation="horizontal"] { + grid-template-areas: "viewer comments" "activity comments"; + grid-template-columns: minmax(0, 1fr) 20rem; + grid-template-rows: minmax(0, 1fr) auto; + + .ListDetails_Comments { + max-height: none; + } + } + + &[data-inset="true"] { + gap: 0; + } + + &_CommentList, + &_Activity { + @include flex.column; + + &[data-inset="true"] { + padding: 1rem var(--modal-padding) var(--modal-padding); + } + } + + &_Actions { + @include flex.row; + + margin-bottom: var(--modal-padding); + } + + &_OrganizerSection { + @include borders.normal($side: top); + + padding: 1rem var(--modal-padding) var(--modal-padding); + } +} + +.ListStatus { + padding: 1rem; + + &[data-inset="true"] { + padding: 1rem var(--modal-padding); + } +} diff --git a/src/components/ListDetails/ListDetails.tsx b/src/components/ListDetails/ListDetails.tsx new file mode 100644 index 00000000..e900eca8 --- /dev/null +++ b/src/components/ListDetails/ListDetails.tsx @@ -0,0 +1,127 @@ +import { + useEffect, + useRef, + useState, +} from 'react'; +import { + Button, + Drawer, + getStyleClassNames, + PdfViewer, +} from '@ianpaschal/combat-command-components'; +import clsx from 'clsx'; +import { + CheckCheck, + Download, + MessageCircle, + X, +} from 'lucide-react'; + +import { ListActionKey, ListId } from '~/api'; +import { CommentThread, CommentThreadEvent } from '~/components/CommentThread'; +import { ScrollArea } from '~/components/generic/ScrollArea'; +import { Spinner } from '~/components/generic/Spinner'; +import { ListCommentForm } from '~/components/ListCommentForm'; +import { useGetFileMetadata } from '~/services/files'; +import { useCreateListComment } from '~/services/listComments'; +import { useGetList } from '~/services/lists'; + +import styles from './ListDetails.module.scss'; + +export interface ListDetailsProps { + className?: string; + listId: ListId; + inset?: boolean; + orientation?: 'vertical' | 'horizontal'; +} + +export const ListDetails = ({ + className, + listId, + inset = false, + orientation, +}: ListDetailsProps): JSX.Element => { + const { data: list, loading: listLoading } = useGetList({ id: listId }); + const scrollAreaRef = useRef(null); + const [commentDrawerOpen, setCommentDrawerOpen] = useState(false); + const { mutation: createListComment } = useCreateListComment(); + const { data: file, loading } = useGetFileMetadata(list ? { id: list.storageId } : 'skip'); + + useEffect(() => { + const viewport = scrollAreaRef.current?.querySelector('[data-radix-scroll-area-viewport]'); + if (viewport) { + viewport.scrollTo({ top: viewport.scrollHeight, behavior: 'smooth' }); + } + }, [list?.comments.length]); + + if (listLoading || !list) { + return ; + } + + return ( +
+ +
+ +
+ , + }} + /> + {list.comments.map((comment) => ( + : ) : undefined, + intent: comment.control ? (comment.control === 'approved' ? 'success' : 'danger') : undefined, + }} + /> + ))} +
+
+ {list.availableActions.includes(ListActionKey.Comment) && ( +
+
+ )} +
+
+ ); +}; diff --git a/src/components/ListDetails/ListDetailsButton.tsx b/src/components/ListDetails/ListDetailsButton.tsx new file mode 100644 index 00000000..5c8208b8 --- /dev/null +++ b/src/components/ListDetails/ListDetailsButton.tsx @@ -0,0 +1,46 @@ +import { ReactElement, useState } from 'react'; +import { Drawer, DrawerProvider } from '@ianpaschal/combat-command-components'; + +import { + List, + ListId, + TournamentRegistration, +} from '~/api'; +import { ListDetails } from '~/components/ListDetails'; +import { DeviceSize, useDeviceSize } from '~/hooks/useDeviceSize'; +import { ListDetailsTrigger } from './ListDetailsTrigger'; + +export interface ListDetailsButtonProps { + className?: string; + lists: List[]; + tournamentRegistration: TournamentRegistration; +} + +export const ListDetailsButton = (props: ListDetailsButtonProps): ReactElement | null => { + const [selectedId, setSelectedId] = useState(null); + const [deviceSize] = useDeviceSize(); + const isMobile = deviceSize === DeviceSize.Mobile; + const isDesktop = deviceSize >= DeviceSize.Default; + return ( + + + setSelectedId(null)} + side={isMobile ? 'bottom' : 'right'} + disableScroll + disablePadding={isMobile} + fullSize + > + {selectedId && ( + + )} + + + ); +}; diff --git a/src/components/ListDetails/ListDetailsTrigger.tsx b/src/components/ListDetails/ListDetailsTrigger.tsx new file mode 100644 index 00000000..0b9c94bf --- /dev/null +++ b/src/components/ListDetails/ListDetailsTrigger.tsx @@ -0,0 +1,90 @@ +import { ReactElement } from 'react'; +import { + Badge, + BadgeProps, + Button, + ButtonProps, +} from '@ianpaschal/combat-command-components'; +import { + FileCheck, + FileText, + Upload, +} from 'lucide-react'; + +import { + List, + ListId, + TournamentRegistration, +} from '~/api'; +import { PopoverMenu } from '~/components/generic/PopoverMenu'; +import { useTournament } from '~/components/TournamentProvider'; +import { useCreateListAction } from '~/components/TournamentRegistrationProvider'; + +export interface ListDetailsTriggerProps { + className?: string; + lists: List[]; + tournamentRegistration: TournamentRegistration; + onSelect: (id: ListId) => void; +} + +export const ListDetailsTrigger = ({ + className, + lists, + tournamentRegistration, + onSelect, +}: ListDetailsTriggerProps): ReactElement | null => { + const tournament = useTournament(); + const createList = useCreateListAction(tournamentRegistration); + + // FUTURE: add support for manually closed list submission + + const isLate = lists.length === 0 ? ( + Date.now() > tournament.listSubmissionClosesAt + ) : ( + lists.some((l) => l._creationTime > tournament.listSubmissionClosesAt) + ); + + const triggerButtonProps: ButtonProps = { + className, + icon: , + variant: 'ghost', + }; + const badgeProps: Omit = { + value: isLate ? 'Late' : undefined, + intent: 'danger', + }; + + if (lists.length === 0 && createList) { + return ( + +