xpt-shared-types 1.3.1 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ import type { GameGenre } from "../generated/enums";
2
+ /**
3
+ * Every genre a game can carry, in the order the games page offers them as
4
+ * filter chips. Typed against the generated enum so a schema change that
5
+ * adds or renames a value fails here at compile time rather than leaving a
6
+ * chip that matches nothing.
7
+ */
8
+ export declare const GAME_GENRES: readonly GameGenre[];
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GAME_GENRES = void 0;
4
+ /**
5
+ * Every genre a game can carry, in the order the games page offers them as
6
+ * filter chips. Typed against the generated enum so a schema change that
7
+ * adds or renames a value fails here at compile time rather than leaving a
8
+ * chip that matches nothing.
9
+ */
10
+ exports.GAME_GENRES = [
11
+ "FPS",
12
+ "MOBA",
13
+ "Battle Royale",
14
+ "Fighting (FGC)",
15
+ "Sports",
16
+ "Strategy/RTS",
17
+ "Sim Racing",
18
+ "Card Game",
19
+ "Retro",
20
+ "Other",
21
+ ];
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Which platforms a game runs on, and which account networks a player needs
3
+ * on each of them.
4
+ *
5
+ * A tournament carries three catalogue relations — `game`, `platform` and
6
+ * `gameAccount` — and until these functions existed they were three
7
+ * unconnected choices. A host could put Super Smash Bros on PlayStation, or
8
+ * ask for a Steam ID in an Xbox tournament, and nothing anywhere objected.
9
+ *
10
+ * The chain is game -> platform -> account, each step narrowing the next:
11
+ *
12
+ * - a game is tagged with the platforms it runs on, and with the account
13
+ * networks it uses (`Valorant` -> Riot, `Counter-Strike 2` -> Steam);
14
+ * - a platform is tagged with the networks that exist on that box
15
+ * (PlayStation -> PSN, EA, Epic, Riot);
16
+ * - the accounts valid for a tournament are the *intersection* of the two.
17
+ *
18
+ * Neither list is derivable from the other. PC hosts {Steam, Epic, Battle.net,
19
+ * Riot, EA}, but Overwatch 2 on PC is only Battle.net or Steam — so the
20
+ * platform alone over-generates. And a game's platforms are not the union of
21
+ * its networks' platforms: 2XKO is PC-only, yet Riot must be tagged with
22
+ * PlayStation and Xbox for Valorant, which would wrongly put 2XKO on consoles.
23
+ * Both edges are real; the intersection is the answer.
24
+ *
25
+ * Lives here rather than in xpt-strapi because the client filters its radio
26
+ * groups with the identical rule. Two implementations of an intersection is
27
+ * two chances to disagree about what a host is allowed to pick.
28
+ *
29
+ * Pure on purpose: no Strapi, no I/O, so the rules are unit-tested directly
30
+ * (see xpt-strapi `tests/unit/utils/gameCatalogue.test.ts`).
31
+ */
32
+ /** The identifying fields of a populated Strapi relation, all optional. */
33
+ export interface CatalogueRef {
34
+ id?: number | string | null;
35
+ documentId?: string | null;
36
+ name?: string | null;
37
+ }
38
+ /** A platform, with the account networks that exist on it. */
39
+ export interface CataloguePlatformLike extends CatalogueRef {
40
+ game_accounts?: readonly CatalogueRef[] | null;
41
+ }
42
+ /** A game, with the platforms it runs on and the networks it uses. */
43
+ export interface CatalogueGameLike extends CatalogueRef {
44
+ title?: string | null;
45
+ platforms?: readonly CatalogueRef[] | null;
46
+ game_accounts?: readonly CatalogueRef[] | null;
47
+ }
48
+ /**
49
+ * Whether two relation references point at the same row.
50
+ *
51
+ * `documentId` wins when both sides carry one, because a numeric `id` differs
52
+ * between a draft and its published entry while the documentId does not. Two
53
+ * references that share *neither* field are never equal — the tempting
54
+ * shorthand `a.id === b.id` reads as true when both are `undefined`, which is
55
+ * how `hasGameAccountFor` used to admit any player to a tournament that had no
56
+ * account set at all.
57
+ */
58
+ export declare function sameRef(a?: CatalogueRef | null, b?: CatalogueRef | null): boolean;
59
+ /**
60
+ * The platforms a game can be played on.
61
+ *
62
+ * An untagged game returns the whole catalogue — see `isTagged`. Results keep
63
+ * the order of `allPlatforms` so the radio group a host sees is stable and
64
+ * matches the unfiltered list they saw before any tagging happened.
65
+ */
66
+ export declare function platformsForGame<P extends CataloguePlatformLike>(game: CatalogueGameLike | null | undefined, allPlatforms: readonly P[]): P[];
67
+ /**
68
+ * The account networks valid for a game on a platform.
69
+ *
70
+ * With no platform chosen yet the game's own list is returned unfiltered —
71
+ * that is what lets a single-network game (Fortnite, Valorant, Dota 2) resolve
72
+ * its account at creation time, before a platform exists.
73
+ *
74
+ * An empty result when both sides *are* tagged is returned as-is rather than
75
+ * falling back to the full catalogue. It means the two tag lists disagree,
76
+ * which is a data bug: surfacing it as "no account type available" gets it
77
+ * fixed, whereas quietly showing all ten hides it forever.
78
+ */
79
+ export declare function gameAccountsFor<A extends CatalogueRef>(game: CatalogueGameLike | null | undefined, platform: CataloguePlatformLike | null | undefined, allGameAccounts: readonly A[]): A[];
80
+ /**
81
+ * The one candidate, when there is exactly one.
82
+ *
83
+ * Drives auto-connect: a single valid option is not a choice, so the server
84
+ * connects it and the host is never shown a dialog with one radio in it.
85
+ */
86
+ export declare function soleCandidate<T>(candidates: readonly T[]): T | null;
87
+ /**
88
+ * The candidate platforms that carry this account network — the chain read
89
+ * backwards.
90
+ *
91
+ * Most networks answer the platform question by themselves: a PSN ID only
92
+ * exists on a PlayStation, a Steam ID only on PC. Pair that with
93
+ * `soleCandidate` and choosing the account fills the platform in, which is the
94
+ * other half of what makes these two cards feel like one decision.
95
+ *
96
+ * The four cross-platform networks (EA, Epic, Battle.net, Riot) match several
97
+ * and so infer nothing — correctly, because an EA ID really does not say which
98
+ * box you are on.
99
+ *
100
+ * An **untagged** platform is excluded rather than treated as permissive. That
101
+ * is the opposite of `gameAccountsFor`, and deliberate: not knowing what a
102
+ * platform carries is a reason to stay quiet, not a licence to claim it carries
103
+ * this.
104
+ */
105
+ export declare function platformsCarrying<P extends CataloguePlatformLike>(gameAccount: CatalogueRef | null | undefined, candidates: readonly P[]): P[];
106
+ /** Whether this platform is one the game actually runs on. */
107
+ export declare function isPlatformValidForGame(game: CatalogueGameLike | null | undefined, platform: CatalogueRef | null | undefined, allPlatforms: readonly CataloguePlatformLike[]): boolean;
108
+ /** Whether this account network is one the game uses on this platform. */
109
+ export declare function isGameAccountValidFor(game: CatalogueGameLike | null | undefined, platform: CataloguePlatformLike | null | undefined, gameAccount: CatalogueRef | null | undefined, allGameAccounts: readonly CatalogueRef[]): boolean;
@@ -0,0 +1,164 @@
1
+ "use strict";
2
+ /**
3
+ * Which platforms a game runs on, and which account networks a player needs
4
+ * on each of them.
5
+ *
6
+ * A tournament carries three catalogue relations — `game`, `platform` and
7
+ * `gameAccount` — and until these functions existed they were three
8
+ * unconnected choices. A host could put Super Smash Bros on PlayStation, or
9
+ * ask for a Steam ID in an Xbox tournament, and nothing anywhere objected.
10
+ *
11
+ * The chain is game -> platform -> account, each step narrowing the next:
12
+ *
13
+ * - a game is tagged with the platforms it runs on, and with the account
14
+ * networks it uses (`Valorant` -> Riot, `Counter-Strike 2` -> Steam);
15
+ * - a platform is tagged with the networks that exist on that box
16
+ * (PlayStation -> PSN, EA, Epic, Riot);
17
+ * - the accounts valid for a tournament are the *intersection* of the two.
18
+ *
19
+ * Neither list is derivable from the other. PC hosts {Steam, Epic, Battle.net,
20
+ * Riot, EA}, but Overwatch 2 on PC is only Battle.net or Steam — so the
21
+ * platform alone over-generates. And a game's platforms are not the union of
22
+ * its networks' platforms: 2XKO is PC-only, yet Riot must be tagged with
23
+ * PlayStation and Xbox for Valorant, which would wrongly put 2XKO on consoles.
24
+ * Both edges are real; the intersection is the answer.
25
+ *
26
+ * Lives here rather than in xpt-strapi because the client filters its radio
27
+ * groups with the identical rule. Two implementations of an intersection is
28
+ * two chances to disagree about what a host is allowed to pick.
29
+ *
30
+ * Pure on purpose: no Strapi, no I/O, so the rules are unit-tested directly
31
+ * (see xpt-strapi `tests/unit/utils/gameCatalogue.test.ts`).
32
+ */
33
+ Object.defineProperty(exports, "__esModule", { value: true });
34
+ exports.sameRef = sameRef;
35
+ exports.platformsForGame = platformsForGame;
36
+ exports.gameAccountsFor = gameAccountsFor;
37
+ exports.soleCandidate = soleCandidate;
38
+ exports.platformsCarrying = platformsCarrying;
39
+ exports.isPlatformValidForGame = isPlatformValidForGame;
40
+ exports.isGameAccountValidFor = isGameAccountValidFor;
41
+ /**
42
+ * Whether two relation references point at the same row.
43
+ *
44
+ * `documentId` wins when both sides carry one, because a numeric `id` differs
45
+ * between a draft and its published entry while the documentId does not. Two
46
+ * references that share *neither* field are never equal — the tempting
47
+ * shorthand `a.id === b.id` reads as true when both are `undefined`, which is
48
+ * how `hasGameAccountFor` used to admit any player to a tournament that had no
49
+ * account set at all.
50
+ */
51
+ function sameRef(a, b) {
52
+ if (!a || !b)
53
+ return false;
54
+ if (a.documentId && b.documentId)
55
+ return a.documentId === b.documentId;
56
+ const aId = a.id;
57
+ const bId = b.id;
58
+ if (aId === null || aId === undefined)
59
+ return false;
60
+ if (bId === null || bId === undefined)
61
+ return false;
62
+ return String(aId) === String(bId);
63
+ }
64
+ /**
65
+ * Whether a catalogue tag list says anything.
66
+ *
67
+ * An absent list and an empty one mean the same thing: nobody has tagged this
68
+ * row yet. Strapi serialises an unpopulated relation and a populated-but-empty
69
+ * one identically once it reaches the client, so there is no way to tell them
70
+ * apart and no reason to try — either way we have no opinion, and an untagged
71
+ * row must never lock a host out of a game we simply forgot to configure.
72
+ */
73
+ function isTagged(list) {
74
+ return Array.isArray(list) && list.length > 0;
75
+ }
76
+ /** The master list, minus any row that repeats one already seen. */
77
+ function dedupe(rows) {
78
+ const seen = [];
79
+ for (const row of rows) {
80
+ if (!seen.some((kept) => sameRef(kept, row)))
81
+ seen.push(row);
82
+ }
83
+ return seen;
84
+ }
85
+ /**
86
+ * The platforms a game can be played on.
87
+ *
88
+ * An untagged game returns the whole catalogue — see `isTagged`. Results keep
89
+ * the order of `allPlatforms` so the radio group a host sees is stable and
90
+ * matches the unfiltered list they saw before any tagging happened.
91
+ */
92
+ function platformsForGame(game, allPlatforms) {
93
+ const all = dedupe(allPlatforms);
94
+ if (!game || !isTagged(game.platforms))
95
+ return all;
96
+ return all.filter((platform) => game.platforms.some((tag) => sameRef(tag, platform)));
97
+ }
98
+ /**
99
+ * The account networks valid for a game on a platform.
100
+ *
101
+ * With no platform chosen yet the game's own list is returned unfiltered —
102
+ * that is what lets a single-network game (Fortnite, Valorant, Dota 2) resolve
103
+ * its account at creation time, before a platform exists.
104
+ *
105
+ * An empty result when both sides *are* tagged is returned as-is rather than
106
+ * falling back to the full catalogue. It means the two tag lists disagree,
107
+ * which is a data bug: surfacing it as "no account type available" gets it
108
+ * fixed, whereas quietly showing all ten hides it forever.
109
+ */
110
+ function gameAccountsFor(game, platform, allGameAccounts) {
111
+ const all = dedupe(allGameAccounts);
112
+ const byGame = !game || !isTagged(game.game_accounts)
113
+ ? all
114
+ : all.filter((account) => game.game_accounts.some((tag) => sameRef(tag, account)));
115
+ // No platform yet, or a platform nobody has tagged: the game is the only
116
+ // authority we have.
117
+ if (!platform || !isTagged(platform.game_accounts))
118
+ return byGame;
119
+ return byGame.filter((account) => platform.game_accounts.some((tag) => sameRef(tag, account)));
120
+ }
121
+ /**
122
+ * The one candidate, when there is exactly one.
123
+ *
124
+ * Drives auto-connect: a single valid option is not a choice, so the server
125
+ * connects it and the host is never shown a dialog with one radio in it.
126
+ */
127
+ function soleCandidate(candidates) {
128
+ return candidates.length === 1 ? candidates[0] : null;
129
+ }
130
+ /**
131
+ * The candidate platforms that carry this account network — the chain read
132
+ * backwards.
133
+ *
134
+ * Most networks answer the platform question by themselves: a PSN ID only
135
+ * exists on a PlayStation, a Steam ID only on PC. Pair that with
136
+ * `soleCandidate` and choosing the account fills the platform in, which is the
137
+ * other half of what makes these two cards feel like one decision.
138
+ *
139
+ * The four cross-platform networks (EA, Epic, Battle.net, Riot) match several
140
+ * and so infer nothing — correctly, because an EA ID really does not say which
141
+ * box you are on.
142
+ *
143
+ * An **untagged** platform is excluded rather than treated as permissive. That
144
+ * is the opposite of `gameAccountsFor`, and deliberate: not knowing what a
145
+ * platform carries is a reason to stay quiet, not a licence to claim it carries
146
+ * this.
147
+ */
148
+ function platformsCarrying(gameAccount, candidates) {
149
+ if (!gameAccount)
150
+ return [];
151
+ return dedupe(candidates).filter((platform) => { var _a; return ((_a = platform.game_accounts) !== null && _a !== void 0 ? _a : []).some((tag) => sameRef(tag, gameAccount)); });
152
+ }
153
+ /** Whether this platform is one the game actually runs on. */
154
+ function isPlatformValidForGame(game, platform, allPlatforms) {
155
+ if (!platform)
156
+ return false;
157
+ return platformsForGame(game, allPlatforms).some((valid) => sameRef(valid, platform));
158
+ }
159
+ /** Whether this account network is one the game uses on this platform. */
160
+ function isGameAccountValidFor(game, platform, gameAccount, allGameAccounts) {
161
+ if (!gameAccount)
162
+ return false;
163
+ return gameAccountsFor(game, platform, allGameAccounts).some((valid) => sameRef(valid, gameAccount));
164
+ }
@@ -1,19 +1,29 @@
1
1
  /** `Friendship.status` */
2
2
  export type FriendshipStatus = "pending" | "accepted";
3
+ /** `Game.genre` */
4
+ export type GameGenre = "FPS" | "MOBA" | "Battle Royale" | "Fighting (FGC)" | "Sports" | "Strategy/RTS" | "Sim Racing" | "Card Game" | "Retro" | "Other";
3
5
  /** `GameRequest.customLobbies` */
4
6
  export type GameRequestCustomLobbies = "Yes" | "No" | "Not sure";
5
7
  /** `GameRequest.genre` */
6
8
  export type GameRequestGenre = "FPS" | "MOBA" | "Battle Royale" | "Fighting (FGC)" | "Sports" | "Strategy/RTS" | "Sim Racing" | "Card Game" | "Other";
7
9
  /** `GameRequest.teamPlay` */
8
10
  export type GameRequestTeamPlay = "No (solo)" | "Yes, 2v2" | "Yes, 3v3" | "Yes, 4v4+" | "Don't know";
11
+ /** `Game.visibility` */
12
+ export type GameVisibility = "listed" | "searchOnly" | "archived";
13
+ /** `Match.disputeReason` */
14
+ export type MatchDisputeReason = "scoreMismatch" | "opponentNoShow" | "opponentLaggedOut" | "rulesViolation" | "other";
9
15
  /** `Match.lobbyStatus` */
10
16
  export type MatchLobbyStatus = "waiting" | "ready" | "disputed" | "completed";
17
+ /** `Match.resultType` */
18
+ export type MatchResultType = "reported" | "hostRecorded" | "bye" | "noShowHome" | "noShowAway" | "forfeitHome" | "forfeitAway" | "disputeResolved" | "adminOverride";
11
19
  /** `Match.round` */
12
20
  export type MatchRound = "Round 128" | "Round 64" | "Round 32" | "Round 16" | "Quarter Final" | "Semi-Final" | "Third Round" | "Finals" | "League";
13
21
  /** `Match.streamPlatform` */
14
22
  export type MatchStreamPlatform = "twitch" | "youtube" | "kick" | "other";
15
23
  /** `Match.winnerSlot` */
16
24
  export type MatchWinnerSlot = "home" | "away";
25
+ /** `Platform.code` */
26
+ export type PlatformCode = "pc" | "playstation" | "xbox" | "nintendo" | "mobile";
17
27
  /** `Referral.status` */
18
28
  export type ReferralStatus = "pending" | "completed";
19
29
  /** `Team.current_status` */
@@ -1,5 +1,5 @@
1
1
  import type { BlocksContent, MediaInput, RelationInput } from '../contracts';
2
- import type { FriendshipStatus, GameRequestCustomLobbies, GameRequestGenre, GameRequestTeamPlay, MatchLobbyStatus, MatchRound, MatchStreamPlatform, MatchWinnerSlot, ReferralStatus, TeamCurrentStatus, TeamInviteStatus, TeamInviteTeamRole, TeamPlayerTeamRole, TournamentCurrentStatus, TournamentParticipantEntryType, TournamentParticipantStatus, TournamentRoleRole, TournamentTeamSize, TournamentType, UserTransactionStripeStatus, UserTransactionType } from './enums';
2
+ import type { FriendshipStatus, GameGenre, GameRequestCustomLobbies, GameRequestGenre, GameRequestTeamPlay, GameVisibility, MatchDisputeReason, MatchLobbyStatus, MatchResultType, MatchRound, MatchStreamPlatform, MatchWinnerSlot, PlatformCode, ReferralStatus, TeamCurrentStatus, TeamInviteStatus, TeamInviteTeamRole, TeamPlayerTeamRole, TournamentCurrentStatus, TournamentParticipantEntryType, TournamentParticipantStatus, TournamentRoleRole, TournamentTeamSize, TournamentType, UserTransactionStripeStatus, UserTransactionType } from './enums';
3
3
  /** Write payload for `about`. */
4
4
  export interface AboutInput {
5
5
  heroTitle?: string;
@@ -44,10 +44,14 @@ export interface GameInput {
44
44
  imgThumb?: MediaInput;
45
45
  imgMain?: MediaInput;
46
46
  tournaments?: RelationInput | RelationInput[];
47
- isTrending?: boolean;
48
47
  user_game_stats?: RelationInput | RelationInput[];
49
48
  team_game_stats?: RelationInput | RelationInput[];
50
49
  imgMainPosition?: string;
50
+ visibility?: GameVisibility;
51
+ genre?: GameGenre;
52
+ sortOrder?: number;
53
+ platforms?: RelationInput | RelationInput[];
54
+ game_accounts?: RelationInput | RelationInput[];
51
55
  }
52
56
  /** Write payload for `game-account`. */
53
57
  export interface GameAccountInput {
@@ -55,6 +59,8 @@ export interface GameAccountInput {
55
59
  tournaments?: RelationInput | RelationInput[];
56
60
  user_game_accounts?: RelationInput | RelationInput[];
57
61
  imgThumb?: MediaInput;
62
+ games?: RelationInput | RelationInput[];
63
+ platforms?: RelationInput | RelationInput[];
58
64
  }
59
65
  /** Write payload for `game-request`. */
60
66
  export interface GameRequestInput {
@@ -126,6 +132,13 @@ export interface MatchInput {
126
132
  isStreaming?: boolean;
127
133
  streamHandle?: string;
128
134
  streamPlatform?: MatchStreamPlatform;
135
+ startedAt?: string;
136
+ endedAt?: string;
137
+ resultType?: MatchResultType;
138
+ resultNote?: string;
139
+ disputeReason?: MatchDisputeReason;
140
+ disputeNote?: string;
141
+ disputedBy?: RelationInput;
129
142
  }
130
143
  /** Write payload for `match-message`. */
131
144
  export interface MatchMessageInput {
@@ -148,7 +161,10 @@ export interface NotificationInput {
148
161
  /** Write payload for `platform`. */
149
162
  export interface PlatformInput {
150
163
  name?: string;
164
+ code?: PlatformCode;
151
165
  tournaments?: RelationInput | RelationInput[];
166
+ games?: RelationInput | RelationInput[];
167
+ game_accounts?: RelationInput | RelationInput[];
152
168
  }
153
169
  /** Write payload for `preset-avatar`. */
154
170
  export interface PresetAvatarInput {
@@ -1,5 +1,5 @@
1
1
  import type { BlocksContent, StrapiDocument, StrapiMedia, UserRole } from '../contracts';
2
- import type { FriendshipStatus, GameRequestCustomLobbies, GameRequestGenre, GameRequestTeamPlay, MatchLobbyStatus, MatchRound, MatchStreamPlatform, MatchWinnerSlot, ReferralStatus, TeamCurrentStatus, TeamInviteStatus, TeamInviteTeamRole, TeamPlayerTeamRole, TournamentCurrentStatus, TournamentParticipantEntryType, TournamentParticipantStatus, TournamentRoleRole, TournamentTeamSize, TournamentType, UserTransactionStripeStatus, UserTransactionType } from './enums';
2
+ import type { FriendshipStatus, GameGenre, GameRequestCustomLobbies, GameRequestGenre, GameRequestTeamPlay, GameVisibility, MatchDisputeReason, MatchLobbyStatus, MatchResultType, MatchRound, MatchStreamPlatform, MatchWinnerSlot, PlatformCode, ReferralStatus, TeamCurrentStatus, TeamInviteStatus, TeamInviteTeamRole, TeamPlayerTeamRole, TournamentCurrentStatus, TournamentParticipantEntryType, TournamentParticipantStatus, TournamentRoleRole, TournamentTeamSize, TournamentType, UserTransactionStripeStatus, UserTransactionType } from './enums';
3
3
  /** `about` */
4
4
  export interface About extends StrapiDocument {
5
5
  heroTitle?: string | null;
@@ -44,10 +44,14 @@ export interface Game extends StrapiDocument {
44
44
  imgThumb?: StrapiMedia | null;
45
45
  imgMain?: StrapiMedia | null;
46
46
  tournaments?: Tournament[];
47
- isTrending?: boolean | null;
48
47
  user_game_stats?: UserGameStat[];
49
48
  team_game_stats?: TeamGameStat[];
50
49
  imgMainPosition?: string | null;
50
+ visibility?: GameVisibility | null;
51
+ genre?: GameGenre | null;
52
+ sortOrder?: number | null;
53
+ platforms?: Platform[];
54
+ game_accounts?: GameAccount[];
51
55
  }
52
56
  /** `game-account` */
53
57
  export interface GameAccount extends StrapiDocument {
@@ -55,6 +59,8 @@ export interface GameAccount extends StrapiDocument {
55
59
  tournaments?: Tournament[];
56
60
  user_game_accounts?: UserGameAccount[];
57
61
  imgThumb?: StrapiMedia | null;
62
+ games?: Game[];
63
+ platforms?: Platform[];
58
64
  }
59
65
  /** `game-request` */
60
66
  export interface GameRequest extends StrapiDocument {
@@ -126,6 +132,13 @@ export interface Match extends StrapiDocument {
126
132
  isStreaming?: boolean | null;
127
133
  streamHandle?: string | null;
128
134
  streamPlatform?: MatchStreamPlatform | null;
135
+ startedAt?: string | null;
136
+ endedAt?: string | null;
137
+ resultType?: MatchResultType | null;
138
+ resultNote?: string | null;
139
+ disputeReason?: MatchDisputeReason | null;
140
+ disputeNote?: string | null;
141
+ disputedBy?: User | null;
129
142
  }
130
143
  /** `match-message` */
131
144
  export interface MatchMessage extends StrapiDocument {
@@ -148,7 +161,10 @@ export interface Notification extends StrapiDocument {
148
161
  /** `platform` */
149
162
  export interface Platform extends StrapiDocument {
150
163
  name?: string | null;
164
+ code?: PlatformCode | null;
151
165
  tournaments?: Tournament[];
166
+ games?: Game[];
167
+ game_accounts?: GameAccount[];
152
168
  }
153
169
  /** `preset-avatar` */
154
170
  export interface PresetAvatar extends StrapiDocument {
package/dist/index.d.ts CHANGED
@@ -2,4 +2,6 @@ export * from './generated';
2
2
  export * from './contracts';
3
3
  export * from './manual';
4
4
  export * from './bracket';
5
+ export * from './gameCatalogue';
5
6
  export * from './data/countriesList';
7
+ export * from './data/gameGenres';
package/dist/index.js CHANGED
@@ -22,5 +22,9 @@ __exportStar(require("./contracts"), exports);
22
22
  __exportStar(require("./manual"), exports);
23
23
  // Bracket shape — shared by the backend generator and Storybook fixtures
24
24
  __exportStar(require("./bracket"), exports);
25
+ // Game/platform/account catalogue — the game -> platform -> account chain,
26
+ // applied identically by the backend resolver and the client's pickers
27
+ __exportStar(require("./gameCatalogue"), exports);
25
28
  // Data
26
29
  __exportStar(require("./data/countriesList"), exports);
30
+ __exportStar(require("./data/gameGenres"), exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xpt-shared-types",
3
- "version": "1.3.1",
3
+ "version": "1.6.1",
4
4
  "description": "Shared types and data for XPT projects",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -0,0 +1,20 @@
1
+ import type { GameGenre } from "../generated/enums";
2
+
3
+ /**
4
+ * Every genre a game can carry, in the order the games page offers them as
5
+ * filter chips. Typed against the generated enum so a schema change that
6
+ * adds or renames a value fails here at compile time rather than leaving a
7
+ * chip that matches nothing.
8
+ */
9
+ export const GAME_GENRES: readonly GameGenre[] = [
10
+ "FPS",
11
+ "MOBA",
12
+ "Battle Royale",
13
+ "Fighting (FGC)",
14
+ "Sports",
15
+ "Strategy/RTS",
16
+ "Sim Racing",
17
+ "Card Game",
18
+ "Retro",
19
+ "Other",
20
+ ];
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Which platforms a game runs on, and which account networks a player needs
3
+ * on each of them.
4
+ *
5
+ * A tournament carries three catalogue relations — `game`, `platform` and
6
+ * `gameAccount` — and until these functions existed they were three
7
+ * unconnected choices. A host could put Super Smash Bros on PlayStation, or
8
+ * ask for a Steam ID in an Xbox tournament, and nothing anywhere objected.
9
+ *
10
+ * The chain is game -> platform -> account, each step narrowing the next:
11
+ *
12
+ * - a game is tagged with the platforms it runs on, and with the account
13
+ * networks it uses (`Valorant` -> Riot, `Counter-Strike 2` -> Steam);
14
+ * - a platform is tagged with the networks that exist on that box
15
+ * (PlayStation -> PSN, EA, Epic, Riot);
16
+ * - the accounts valid for a tournament are the *intersection* of the two.
17
+ *
18
+ * Neither list is derivable from the other. PC hosts {Steam, Epic, Battle.net,
19
+ * Riot, EA}, but Overwatch 2 on PC is only Battle.net or Steam — so the
20
+ * platform alone over-generates. And a game's platforms are not the union of
21
+ * its networks' platforms: 2XKO is PC-only, yet Riot must be tagged with
22
+ * PlayStation and Xbox for Valorant, which would wrongly put 2XKO on consoles.
23
+ * Both edges are real; the intersection is the answer.
24
+ *
25
+ * Lives here rather than in xpt-strapi because the client filters its radio
26
+ * groups with the identical rule. Two implementations of an intersection is
27
+ * two chances to disagree about what a host is allowed to pick.
28
+ *
29
+ * Pure on purpose: no Strapi, no I/O, so the rules are unit-tested directly
30
+ * (see xpt-strapi `tests/unit/utils/gameCatalogue.test.ts`).
31
+ */
32
+
33
+ /** The identifying fields of a populated Strapi relation, all optional. */
34
+ export interface CatalogueRef {
35
+ id?: number | string | null;
36
+ documentId?: string | null;
37
+ name?: string | null;
38
+ }
39
+
40
+ /** A platform, with the account networks that exist on it. */
41
+ export interface CataloguePlatformLike extends CatalogueRef {
42
+ game_accounts?: readonly CatalogueRef[] | null;
43
+ }
44
+
45
+ /** A game, with the platforms it runs on and the networks it uses. */
46
+ export interface CatalogueGameLike extends CatalogueRef {
47
+ title?: string | null;
48
+ platforms?: readonly CatalogueRef[] | null;
49
+ game_accounts?: readonly CatalogueRef[] | null;
50
+ }
51
+
52
+ /**
53
+ * Whether two relation references point at the same row.
54
+ *
55
+ * `documentId` wins when both sides carry one, because a numeric `id` differs
56
+ * between a draft and its published entry while the documentId does not. Two
57
+ * references that share *neither* field are never equal — the tempting
58
+ * shorthand `a.id === b.id` reads as true when both are `undefined`, which is
59
+ * how `hasGameAccountFor` used to admit any player to a tournament that had no
60
+ * account set at all.
61
+ */
62
+ export function sameRef(
63
+ a?: CatalogueRef | null,
64
+ b?: CatalogueRef | null
65
+ ): boolean {
66
+ if (!a || !b) return false;
67
+
68
+ if (a.documentId && b.documentId) return a.documentId === b.documentId;
69
+
70
+ const aId = a.id;
71
+ const bId = b.id;
72
+ if (aId === null || aId === undefined) return false;
73
+ if (bId === null || bId === undefined) return false;
74
+
75
+ return String(aId) === String(bId);
76
+ }
77
+
78
+ /**
79
+ * Whether a catalogue tag list says anything.
80
+ *
81
+ * An absent list and an empty one mean the same thing: nobody has tagged this
82
+ * row yet. Strapi serialises an unpopulated relation and a populated-but-empty
83
+ * one identically once it reaches the client, so there is no way to tell them
84
+ * apart and no reason to try — either way we have no opinion, and an untagged
85
+ * row must never lock a host out of a game we simply forgot to configure.
86
+ */
87
+ function isTagged(list?: readonly CatalogueRef[] | null): boolean {
88
+ return Array.isArray(list) && list.length > 0;
89
+ }
90
+
91
+ /** The master list, minus any row that repeats one already seen. */
92
+ function dedupe<T extends CatalogueRef>(rows: readonly T[]): T[] {
93
+ const seen: T[] = [];
94
+ for (const row of rows) {
95
+ if (!seen.some((kept) => sameRef(kept, row))) seen.push(row);
96
+ }
97
+ return seen;
98
+ }
99
+
100
+ /**
101
+ * The platforms a game can be played on.
102
+ *
103
+ * An untagged game returns the whole catalogue — see `isTagged`. Results keep
104
+ * the order of `allPlatforms` so the radio group a host sees is stable and
105
+ * matches the unfiltered list they saw before any tagging happened.
106
+ */
107
+ export function platformsForGame<P extends CataloguePlatformLike>(
108
+ game: CatalogueGameLike | null | undefined,
109
+ allPlatforms: readonly P[]
110
+ ): P[] {
111
+ const all = dedupe(allPlatforms);
112
+
113
+ if (!game || !isTagged(game.platforms)) return all;
114
+
115
+ return all.filter((platform) =>
116
+ game.platforms!.some((tag) => sameRef(tag, platform))
117
+ );
118
+ }
119
+
120
+ /**
121
+ * The account networks valid for a game on a platform.
122
+ *
123
+ * With no platform chosen yet the game's own list is returned unfiltered —
124
+ * that is what lets a single-network game (Fortnite, Valorant, Dota 2) resolve
125
+ * its account at creation time, before a platform exists.
126
+ *
127
+ * An empty result when both sides *are* tagged is returned as-is rather than
128
+ * falling back to the full catalogue. It means the two tag lists disagree,
129
+ * which is a data bug: surfacing it as "no account type available" gets it
130
+ * fixed, whereas quietly showing all ten hides it forever.
131
+ */
132
+ export function gameAccountsFor<A extends CatalogueRef>(
133
+ game: CatalogueGameLike | null | undefined,
134
+ platform: CataloguePlatformLike | null | undefined,
135
+ allGameAccounts: readonly A[]
136
+ ): A[] {
137
+ const all = dedupe(allGameAccounts);
138
+
139
+ const byGame =
140
+ !game || !isTagged(game.game_accounts)
141
+ ? all
142
+ : all.filter((account) =>
143
+ game.game_accounts!.some((tag) => sameRef(tag, account))
144
+ );
145
+
146
+ // No platform yet, or a platform nobody has tagged: the game is the only
147
+ // authority we have.
148
+ if (!platform || !isTagged(platform.game_accounts)) return byGame;
149
+
150
+ return byGame.filter((account) =>
151
+ platform.game_accounts!.some((tag) => sameRef(tag, account))
152
+ );
153
+ }
154
+
155
+ /**
156
+ * The one candidate, when there is exactly one.
157
+ *
158
+ * Drives auto-connect: a single valid option is not a choice, so the server
159
+ * connects it and the host is never shown a dialog with one radio in it.
160
+ */
161
+ export function soleCandidate<T>(candidates: readonly T[]): T | null {
162
+ return candidates.length === 1 ? candidates[0] : null;
163
+ }
164
+
165
+ /**
166
+ * The candidate platforms that carry this account network — the chain read
167
+ * backwards.
168
+ *
169
+ * Most networks answer the platform question by themselves: a PSN ID only
170
+ * exists on a PlayStation, a Steam ID only on PC. Pair that with
171
+ * `soleCandidate` and choosing the account fills the platform in, which is the
172
+ * other half of what makes these two cards feel like one decision.
173
+ *
174
+ * The four cross-platform networks (EA, Epic, Battle.net, Riot) match several
175
+ * and so infer nothing — correctly, because an EA ID really does not say which
176
+ * box you are on.
177
+ *
178
+ * An **untagged** platform is excluded rather than treated as permissive. That
179
+ * is the opposite of `gameAccountsFor`, and deliberate: not knowing what a
180
+ * platform carries is a reason to stay quiet, not a licence to claim it carries
181
+ * this.
182
+ */
183
+ export function platformsCarrying<P extends CataloguePlatformLike>(
184
+ gameAccount: CatalogueRef | null | undefined,
185
+ candidates: readonly P[]
186
+ ): P[] {
187
+ if (!gameAccount) return [];
188
+
189
+ return dedupe(candidates).filter((platform) =>
190
+ (platform.game_accounts ?? []).some((tag) => sameRef(tag, gameAccount))
191
+ );
192
+ }
193
+
194
+ /** Whether this platform is one the game actually runs on. */
195
+ export function isPlatformValidForGame(
196
+ game: CatalogueGameLike | null | undefined,
197
+ platform: CatalogueRef | null | undefined,
198
+ allPlatforms: readonly CataloguePlatformLike[]
199
+ ): boolean {
200
+ if (!platform) return false;
201
+ return platformsForGame(game, allPlatforms).some((valid) =>
202
+ sameRef(valid, platform)
203
+ );
204
+ }
205
+
206
+ /** Whether this account network is one the game uses on this platform. */
207
+ export function isGameAccountValidFor(
208
+ game: CatalogueGameLike | null | undefined,
209
+ platform: CataloguePlatformLike | null | undefined,
210
+ gameAccount: CatalogueRef | null | undefined,
211
+ allGameAccounts: readonly CatalogueRef[]
212
+ ): boolean {
213
+ if (!gameAccount) return false;
214
+ return gameAccountsFor(game, platform, allGameAccounts).some((valid) =>
215
+ sameRef(valid, gameAccount)
216
+ );
217
+ }
@@ -7,6 +7,19 @@ export type FriendshipStatus =
7
7
  | "pending"
8
8
  | "accepted";
9
9
 
10
+ /** `Game.genre` */
11
+ export type GameGenre =
12
+ | "FPS"
13
+ | "MOBA"
14
+ | "Battle Royale"
15
+ | "Fighting (FGC)"
16
+ | "Sports"
17
+ | "Strategy/RTS"
18
+ | "Sim Racing"
19
+ | "Card Game"
20
+ | "Retro"
21
+ | "Other";
22
+
10
23
  /** `GameRequest.customLobbies` */
11
24
  export type GameRequestCustomLobbies =
12
25
  | "Yes"
@@ -33,6 +46,20 @@ export type GameRequestTeamPlay =
33
46
  | "Yes, 4v4+"
34
47
  | "Don't know";
35
48
 
49
+ /** `Game.visibility` */
50
+ export type GameVisibility =
51
+ | "listed"
52
+ | "searchOnly"
53
+ | "archived";
54
+
55
+ /** `Match.disputeReason` */
56
+ export type MatchDisputeReason =
57
+ | "scoreMismatch"
58
+ | "opponentNoShow"
59
+ | "opponentLaggedOut"
60
+ | "rulesViolation"
61
+ | "other";
62
+
36
63
  /** `Match.lobbyStatus` */
37
64
  export type MatchLobbyStatus =
38
65
  | "waiting"
@@ -40,6 +67,18 @@ export type MatchLobbyStatus =
40
67
  | "disputed"
41
68
  | "completed";
42
69
 
70
+ /** `Match.resultType` */
71
+ export type MatchResultType =
72
+ | "reported"
73
+ | "hostRecorded"
74
+ | "bye"
75
+ | "noShowHome"
76
+ | "noShowAway"
77
+ | "forfeitHome"
78
+ | "forfeitAway"
79
+ | "disputeResolved"
80
+ | "adminOverride";
81
+
43
82
  /** `Match.round` */
44
83
  export type MatchRound =
45
84
  | "Round 128"
@@ -64,6 +103,14 @@ export type MatchWinnerSlot =
64
103
  | "home"
65
104
  | "away";
66
105
 
106
+ /** `Platform.code` */
107
+ export type PlatformCode =
108
+ | "pc"
109
+ | "playstation"
110
+ | "xbox"
111
+ | "nintendo"
112
+ | "mobile";
113
+
67
114
  /** `Referral.status` */
68
115
  export type ReferralStatus =
69
116
  | "pending"
@@ -9,13 +9,18 @@ import type {
9
9
  } from '../contracts';
10
10
  import type {
11
11
  FriendshipStatus,
12
+ GameGenre,
12
13
  GameRequestCustomLobbies,
13
14
  GameRequestGenre,
14
15
  GameRequestTeamPlay,
16
+ GameVisibility,
17
+ MatchDisputeReason,
15
18
  MatchLobbyStatus,
19
+ MatchResultType,
16
20
  MatchRound,
17
21
  MatchStreamPlatform,
18
22
  MatchWinnerSlot,
23
+ PlatformCode,
19
24
  ReferralStatus,
20
25
  TeamCurrentStatus,
21
26
  TeamInviteStatus,
@@ -81,10 +86,14 @@ export interface GameInput {
81
86
  imgThumb?: MediaInput;
82
87
  imgMain?: MediaInput;
83
88
  tournaments?: RelationInput | RelationInput[];
84
- isTrending?: boolean;
85
89
  user_game_stats?: RelationInput | RelationInput[];
86
90
  team_game_stats?: RelationInput | RelationInput[];
87
91
  imgMainPosition?: string;
92
+ visibility?: GameVisibility;
93
+ genre?: GameGenre;
94
+ sortOrder?: number;
95
+ platforms?: RelationInput | RelationInput[];
96
+ game_accounts?: RelationInput | RelationInput[];
88
97
  }
89
98
 
90
99
  /** Write payload for `game-account`. */
@@ -93,6 +102,8 @@ export interface GameAccountInput {
93
102
  tournaments?: RelationInput | RelationInput[];
94
103
  user_game_accounts?: RelationInput | RelationInput[];
95
104
  imgThumb?: MediaInput;
105
+ games?: RelationInput | RelationInput[];
106
+ platforms?: RelationInput | RelationInput[];
96
107
  }
97
108
 
98
109
  /** Write payload for `game-request`. */
@@ -169,6 +180,13 @@ export interface MatchInput {
169
180
  isStreaming?: boolean;
170
181
  streamHandle?: string;
171
182
  streamPlatform?: MatchStreamPlatform;
183
+ startedAt?: string;
184
+ endedAt?: string;
185
+ resultType?: MatchResultType;
186
+ resultNote?: string;
187
+ disputeReason?: MatchDisputeReason;
188
+ disputeNote?: string;
189
+ disputedBy?: RelationInput;
172
190
  }
173
191
 
174
192
  /** Write payload for `match-message`. */
@@ -194,7 +212,10 @@ export interface NotificationInput {
194
212
  /** Write payload for `platform`. */
195
213
  export interface PlatformInput {
196
214
  name?: string;
215
+ code?: PlatformCode;
197
216
  tournaments?: RelationInput | RelationInput[];
217
+ games?: RelationInput | RelationInput[];
218
+ game_accounts?: RelationInput | RelationInput[];
198
219
  }
199
220
 
200
221
  /** Write payload for `preset-avatar`. */
@@ -10,13 +10,18 @@ import type {
10
10
  } from '../contracts';
11
11
  import type {
12
12
  FriendshipStatus,
13
+ GameGenre,
13
14
  GameRequestCustomLobbies,
14
15
  GameRequestGenre,
15
16
  GameRequestTeamPlay,
17
+ GameVisibility,
18
+ MatchDisputeReason,
16
19
  MatchLobbyStatus,
20
+ MatchResultType,
17
21
  MatchRound,
18
22
  MatchStreamPlatform,
19
23
  MatchWinnerSlot,
24
+ PlatformCode,
20
25
  ReferralStatus,
21
26
  TeamCurrentStatus,
22
27
  TeamInviteStatus,
@@ -82,10 +87,14 @@ export interface Game extends StrapiDocument {
82
87
  imgThumb?: StrapiMedia | null;
83
88
  imgMain?: StrapiMedia | null;
84
89
  tournaments?: Tournament[];
85
- isTrending?: boolean | null;
86
90
  user_game_stats?: UserGameStat[];
87
91
  team_game_stats?: TeamGameStat[];
88
92
  imgMainPosition?: string | null;
93
+ visibility?: GameVisibility | null;
94
+ genre?: GameGenre | null;
95
+ sortOrder?: number | null;
96
+ platforms?: Platform[];
97
+ game_accounts?: GameAccount[];
89
98
  }
90
99
 
91
100
  /** `game-account` */
@@ -94,6 +103,8 @@ export interface GameAccount extends StrapiDocument {
94
103
  tournaments?: Tournament[];
95
104
  user_game_accounts?: UserGameAccount[];
96
105
  imgThumb?: StrapiMedia | null;
106
+ games?: Game[];
107
+ platforms?: Platform[];
97
108
  }
98
109
 
99
110
  /** `game-request` */
@@ -170,6 +181,13 @@ export interface Match extends StrapiDocument {
170
181
  isStreaming?: boolean | null;
171
182
  streamHandle?: string | null;
172
183
  streamPlatform?: MatchStreamPlatform | null;
184
+ startedAt?: string | null;
185
+ endedAt?: string | null;
186
+ resultType?: MatchResultType | null;
187
+ resultNote?: string | null;
188
+ disputeReason?: MatchDisputeReason | null;
189
+ disputeNote?: string | null;
190
+ disputedBy?: User | null;
173
191
  }
174
192
 
175
193
  /** `match-message` */
@@ -195,7 +213,10 @@ export interface Notification extends StrapiDocument {
195
213
  /** `platform` */
196
214
  export interface Platform extends StrapiDocument {
197
215
  name?: string | null;
216
+ code?: PlatformCode | null;
198
217
  tournaments?: Tournament[];
218
+ games?: Game[];
219
+ game_accounts?: GameAccount[];
199
220
  }
200
221
 
201
222
  /** `preset-avatar` */
package/src/index.ts CHANGED
@@ -1,14 +1,19 @@
1
- // Generated from the Strapi schemas — see scripts/sync-from-strapi.js
2
- export * from './generated';
3
-
4
- // Hand-written contract primitives (response envelopes, Populated, media)
5
- export * from './contracts';
6
-
7
- // Hand-written types with no Strapi content type behind them
8
- export * from './manual';
9
-
10
- // Bracket shape — shared by the backend generator and Storybook fixtures
11
- export * from './bracket';
12
-
13
- // Data
14
- export * from './data/countriesList';
1
+ // Generated from the Strapi schemas — see scripts/sync-from-strapi.js
2
+ export * from './generated';
3
+
4
+ // Hand-written contract primitives (response envelopes, Populated, media)
5
+ export * from './contracts';
6
+
7
+ // Hand-written types with no Strapi content type behind them
8
+ export * from './manual';
9
+
10
+ // Bracket shape — shared by the backend generator and Storybook fixtures
11
+ export * from './bracket';
12
+
13
+ // Game/platform/account catalogue — the game -> platform -> account chain,
14
+ // applied identically by the backend resolver and the client's pickers
15
+ export * from './gameCatalogue';
16
+
17
+ // Data
18
+ export * from './data/countriesList';
19
+ export * from './data/gameGenres';