xpt-shared-types 1.8.0 → 1.11.0
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.
- package/dist/generated/enums.d.ts +12 -0
- package/dist/generated/inputs.d.ts +32 -2
- package/dist/generated/models.d.ts +32 -2
- package/dist/manual/index.d.ts +3 -0
- package/dist/manual/index.js +3 -0
- package/dist/manual/tournamentEntry.d.ts +36 -0
- package/dist/manual/tournamentEntry.js +23 -0
- package/dist/manual/tournamentInvite.d.ts +114 -0
- package/dist/manual/tournamentInvite.js +26 -0
- package/dist/manual/tournamentJoinRequest.d.ts +79 -0
- package/dist/manual/tournamentJoinRequest.js +47 -0
- package/package.json +1 -1
- package/src/generated/enums.ts +37 -0
- package/src/generated/inputs.ts +39 -1
- package/src/generated/models.ts +39 -1
- package/src/manual/index.ts +3 -0
- package/src/manual/tournamentEntry.ts +54 -0
- package/src/manual/tournamentInvite.ts +133 -0
- package/src/manual/tournamentJoinRequest.ts +118 -0
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** `Faq.category` */
|
|
2
|
+
export type FaqCategory = "gettingStarted" | "tournamentsMatches" | "hosting" | "walletCoins" | "account";
|
|
1
3
|
/** `Friendship.status` */
|
|
2
4
|
export type FriendshipStatus = "pending" | "accepted";
|
|
3
5
|
/** `Game.genre` */
|
|
@@ -34,8 +36,16 @@ export type TeamInviteStatus = "pending" | "accepted" | "declined";
|
|
|
34
36
|
export type TeamInviteTeamRole = "captain" | "co_captain" | "player" | "substitute" | "coach";
|
|
35
37
|
/** `TeamPlayer.team_role` */
|
|
36
38
|
export type TeamPlayerTeamRole = "captain" | "co_captain" | "player" | "substitute" | "coach";
|
|
39
|
+
/** `Tournament.access` */
|
|
40
|
+
export type TournamentAccess = "open" | "inviteOnly" | "approval";
|
|
37
41
|
/** `Tournament.currentStatus` */
|
|
38
42
|
export type TournamentCurrentStatus = "draft" | "open" | "checkIn" | "seeding" | "live" | "completed" | "cancelled";
|
|
43
|
+
/** `TournamentInvite.status` */
|
|
44
|
+
export type TournamentInviteStatus = "active" | "revoked";
|
|
45
|
+
/** `TournamentJoinRequest.entryType` */
|
|
46
|
+
export type TournamentJoinRequestEntryType = "solo" | "team";
|
|
47
|
+
/** `TournamentJoinRequest.status` */
|
|
48
|
+
export type TournamentJoinRequestStatus = "pending" | "approved" | "declined" | "withdrawn" | "expired";
|
|
39
49
|
/** `TournamentParticipant.entryType` */
|
|
40
50
|
export type TournamentParticipantEntryType = "solo" | "team";
|
|
41
51
|
/** `TournamentParticipant.status` */
|
|
@@ -46,6 +56,8 @@ export type TournamentRoleRole = "moderator" | "admin";
|
|
|
46
56
|
export type TournamentTeamSize = "one" | "two" | "three" | "four" | "five";
|
|
47
57
|
/** `Tournament.type` */
|
|
48
58
|
export type TournamentType = "Single Elimination" | "League" | "Double Elimination";
|
|
59
|
+
/** `Tournament.visibility` */
|
|
60
|
+
export type TournamentVisibility = "public" | "unlisted";
|
|
49
61
|
/** `UserTransaction.stripeStatus` */
|
|
50
62
|
export type UserTransactionStripeStatus = "pending" | "completed" | "failed" | "refunded" | "internal";
|
|
51
63
|
/** `UserTransaction.type` */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BlocksContent, MediaInput, RelationInput } from '../contracts';
|
|
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';
|
|
2
|
+
import type { FaqCategory, FriendshipStatus, GameGenre, GameRequestCustomLobbies, GameRequestGenre, GameRequestTeamPlay, GameVisibility, MatchDisputeReason, MatchLobbyStatus, MatchResultType, MatchRound, MatchStreamPlatform, MatchWinnerSlot, PlatformCode, ReferralStatus, TeamCurrentStatus, TeamInviteStatus, TeamInviteTeamRole, TeamPlayerTeamRole, TournamentAccess, TournamentCurrentStatus, TournamentInviteStatus, TournamentJoinRequestEntryType, TournamentJoinRequestStatus, TournamentParticipantEntryType, TournamentParticipantStatus, TournamentRoleRole, TournamentTeamSize, TournamentType, TournamentVisibility, UserTransactionStripeStatus, UserTransactionType } from './enums';
|
|
3
3
|
/** Write payload for `about`. */
|
|
4
4
|
export interface AboutInput {
|
|
5
5
|
heroTitle?: string;
|
|
@@ -18,6 +18,9 @@ export interface DmMessageInput {
|
|
|
18
18
|
export interface FaqInput {
|
|
19
19
|
question?: string;
|
|
20
20
|
answer?: BlocksContent;
|
|
21
|
+
category?: FaqCategory;
|
|
22
|
+
note?: string;
|
|
23
|
+
order?: number;
|
|
21
24
|
}
|
|
22
25
|
/** Write payload for `featured-game`. */
|
|
23
26
|
export interface FeaturedGameInput {
|
|
@@ -297,7 +300,8 @@ export interface TournamentInput {
|
|
|
297
300
|
rules?: string;
|
|
298
301
|
hasThirdPlace?: boolean;
|
|
299
302
|
hasMatchLobby?: boolean;
|
|
300
|
-
|
|
303
|
+
visibility?: TournamentVisibility;
|
|
304
|
+
access?: TournamentAccess;
|
|
301
305
|
checkInTime?: number;
|
|
302
306
|
region?: RelationInput;
|
|
303
307
|
platform?: RelationInput;
|
|
@@ -309,6 +313,32 @@ export interface TournamentInput {
|
|
|
309
313
|
twitchUrl?: string;
|
|
310
314
|
websiteUrl?: string;
|
|
311
315
|
}
|
|
316
|
+
/** Write payload for `tournament-invite`. */
|
|
317
|
+
export interface TournamentInviteInput {
|
|
318
|
+
tournament?: RelationInput;
|
|
319
|
+
createdByUser?: RelationInput;
|
|
320
|
+
status?: TournamentInviteStatus;
|
|
321
|
+
expiresAt?: string;
|
|
322
|
+
revokedAt?: string;
|
|
323
|
+
useCount?: number;
|
|
324
|
+
lastUsedAt?: string;
|
|
325
|
+
}
|
|
326
|
+
/** Write payload for `tournament-join-request`. */
|
|
327
|
+
export interface TournamentJoinRequestInput {
|
|
328
|
+
tournament?: RelationInput;
|
|
329
|
+
requester?: RelationInput;
|
|
330
|
+
entryType?: TournamentJoinRequestEntryType;
|
|
331
|
+
team?: RelationInput;
|
|
332
|
+
selectedMemberIds?: unknown;
|
|
333
|
+
membersSnapshot?: unknown;
|
|
334
|
+
displayName?: string;
|
|
335
|
+
message?: string;
|
|
336
|
+
feePaid?: number;
|
|
337
|
+
status?: TournamentJoinRequestStatus;
|
|
338
|
+
decidedAt?: string;
|
|
339
|
+
decidedBy?: RelationInput;
|
|
340
|
+
decisionReason?: string;
|
|
341
|
+
}
|
|
312
342
|
/** Write payload for `tournament-participant`. */
|
|
313
343
|
export interface TournamentParticipantInput {
|
|
314
344
|
tournament?: RelationInput;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BlocksContent, StrapiDocument, StrapiMedia, UserRole } from '../contracts';
|
|
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';
|
|
2
|
+
import type { FaqCategory, FriendshipStatus, GameGenre, GameRequestCustomLobbies, GameRequestGenre, GameRequestTeamPlay, GameVisibility, MatchDisputeReason, MatchLobbyStatus, MatchResultType, MatchRound, MatchStreamPlatform, MatchWinnerSlot, PlatformCode, ReferralStatus, TeamCurrentStatus, TeamInviteStatus, TeamInviteTeamRole, TeamPlayerTeamRole, TournamentAccess, TournamentCurrentStatus, TournamentInviteStatus, TournamentJoinRequestEntryType, TournamentJoinRequestStatus, TournamentParticipantEntryType, TournamentParticipantStatus, TournamentRoleRole, TournamentTeamSize, TournamentType, TournamentVisibility, UserTransactionStripeStatus, UserTransactionType } from './enums';
|
|
3
3
|
/** `about` */
|
|
4
4
|
export interface About extends StrapiDocument {
|
|
5
5
|
heroTitle?: string | null;
|
|
@@ -18,6 +18,9 @@ export interface DmMessage extends StrapiDocument {
|
|
|
18
18
|
export interface Faq extends StrapiDocument {
|
|
19
19
|
question?: string | null;
|
|
20
20
|
answer?: BlocksContent | null;
|
|
21
|
+
category?: FaqCategory | null;
|
|
22
|
+
note?: string | null;
|
|
23
|
+
order?: number | null;
|
|
21
24
|
}
|
|
22
25
|
/** `featured-game` */
|
|
23
26
|
export interface FeaturedGame extends StrapiDocument {
|
|
@@ -297,7 +300,8 @@ export interface Tournament extends StrapiDocument {
|
|
|
297
300
|
rules?: string | null;
|
|
298
301
|
hasThirdPlace?: boolean | null;
|
|
299
302
|
hasMatchLobby?: boolean | null;
|
|
300
|
-
|
|
303
|
+
visibility?: TournamentVisibility | null;
|
|
304
|
+
access?: TournamentAccess | null;
|
|
301
305
|
checkInTime?: number | null;
|
|
302
306
|
region?: Region | null;
|
|
303
307
|
platform?: Platform | null;
|
|
@@ -309,6 +313,32 @@ export interface Tournament extends StrapiDocument {
|
|
|
309
313
|
twitchUrl?: string | null;
|
|
310
314
|
websiteUrl?: string | null;
|
|
311
315
|
}
|
|
316
|
+
/** `tournament-invite` */
|
|
317
|
+
export interface TournamentInvite extends StrapiDocument {
|
|
318
|
+
tournament?: Tournament | null;
|
|
319
|
+
createdByUser?: User | null;
|
|
320
|
+
status?: TournamentInviteStatus | null;
|
|
321
|
+
expiresAt?: string | null;
|
|
322
|
+
revokedAt?: string | null;
|
|
323
|
+
useCount?: number | null;
|
|
324
|
+
lastUsedAt?: string | null;
|
|
325
|
+
}
|
|
326
|
+
/** `tournament-join-request` */
|
|
327
|
+
export interface TournamentJoinRequest extends StrapiDocument {
|
|
328
|
+
tournament?: Tournament | null;
|
|
329
|
+
requester?: User | null;
|
|
330
|
+
entryType?: TournamentJoinRequestEntryType | null;
|
|
331
|
+
team?: Team | null;
|
|
332
|
+
selectedMemberIds?: unknown | null;
|
|
333
|
+
membersSnapshot?: unknown | null;
|
|
334
|
+
displayName?: string | null;
|
|
335
|
+
message?: string | null;
|
|
336
|
+
feePaid?: number | null;
|
|
337
|
+
status?: TournamentJoinRequestStatus | null;
|
|
338
|
+
decidedAt?: string | null;
|
|
339
|
+
decidedBy?: User | null;
|
|
340
|
+
decisionReason?: string | null;
|
|
341
|
+
}
|
|
312
342
|
/** `tournament-participant` */
|
|
313
343
|
export interface TournamentParticipant extends StrapiDocument {
|
|
314
344
|
tournament?: Tournament | null;
|
package/dist/manual/index.d.ts
CHANGED
package/dist/manual/index.js
CHANGED
|
@@ -19,3 +19,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
19
19
|
*/
|
|
20
20
|
__exportStar(require("./country"), exports);
|
|
21
21
|
__exportStar(require("./lobby"), exports);
|
|
22
|
+
__exportStar(require("./tournamentEntry"), exports);
|
|
23
|
+
__exportStar(require("./tournamentInvite"), exports);
|
|
24
|
+
__exportStar(require("./tournamentJoinRequest"), exports);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tournament entry after registration — withdrawing it and changing its
|
|
3
|
+
* roster.
|
|
4
|
+
*
|
|
5
|
+
* Neither is a Strapi content type. An entry is a `tournament-participant`
|
|
6
|
+
* row; these name the window in which its registrant may still act on it and
|
|
7
|
+
* the bodies of the endpoints that do so, so the backend guards and the
|
|
8
|
+
* client's buttons agree on both.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Statuses in which an entry can still be withdrawn or have its roster
|
|
12
|
+
* changed. Everything before is unpublished; everything after has a bracket
|
|
13
|
+
* that reads the entry, so the entry is frozen.
|
|
14
|
+
*/
|
|
15
|
+
export declare const ENTRY_EDITABLE_STATUSES: readonly ["open", "checkIn"];
|
|
16
|
+
export type EntryEditableStatus = (typeof ENTRY_EDITABLE_STATUSES)[number];
|
|
17
|
+
/** True when `status` still lets an entry be withdrawn or re-rostered. */
|
|
18
|
+
export declare function isEntryEditable(status: string | null | undefined): status is EntryEditableStatus;
|
|
19
|
+
/**
|
|
20
|
+
* Body of `PUT /tournaments/:id/entry/roster`. Flat, like `/join`, not inside
|
|
21
|
+
* `data`. The full new roster, not a diff — the server works out who was
|
|
22
|
+
* added and who was removed.
|
|
23
|
+
*/
|
|
24
|
+
export interface TournamentRosterUpdateInput {
|
|
25
|
+
selectedMemberIds: number[];
|
|
26
|
+
}
|
|
27
|
+
/** Body of `POST /tournaments/:id/join` for a team entry. Flat, like above. */
|
|
28
|
+
export interface TournamentTeamJoinInput {
|
|
29
|
+
teamId: string;
|
|
30
|
+
selectedMemberIds: number[];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* `error.details.name` values the entry endpoints raise that a client is
|
|
34
|
+
* expected to branch on. Anything else is a plain message.
|
|
35
|
+
*/
|
|
36
|
+
export type TournamentEntryErrorName = 'NotEntryLeader' | 'MembersMissingGameAccount' | 'InsufficientCoins' | 'GameAccountRequired' | 'TeamEntryRequired';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* A tournament entry after registration — withdrawing it and changing its
|
|
4
|
+
* roster.
|
|
5
|
+
*
|
|
6
|
+
* Neither is a Strapi content type. An entry is a `tournament-participant`
|
|
7
|
+
* row; these name the window in which its registrant may still act on it and
|
|
8
|
+
* the bodies of the endpoints that do so, so the backend guards and the
|
|
9
|
+
* client's buttons agree on both.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.ENTRY_EDITABLE_STATUSES = void 0;
|
|
13
|
+
exports.isEntryEditable = isEntryEditable;
|
|
14
|
+
/**
|
|
15
|
+
* Statuses in which an entry can still be withdrawn or have its roster
|
|
16
|
+
* changed. Everything before is unpublished; everything after has a bracket
|
|
17
|
+
* that reads the entry, so the entry is frozen.
|
|
18
|
+
*/
|
|
19
|
+
exports.ENTRY_EDITABLE_STATUSES = ['open', 'checkIn'];
|
|
20
|
+
/** True when `status` still lets an entry be withdrawn or re-rostered. */
|
|
21
|
+
function isEntryEditable(status) {
|
|
22
|
+
return exports.ENTRY_EDITABLE_STATUSES.includes(status !== null && status !== void 0 ? status : '');
|
|
23
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tournament invite links — the contract between the host's invite dialog,
|
|
3
|
+
* the invitee's landing page and the five `/tournaments/…/invite(s)` routes.
|
|
4
|
+
*
|
|
5
|
+
* The `tournament-invite` content type behind these is generated
|
|
6
|
+
* (`TournamentInvite` in `generated/models.ts`), but its `token` is a private
|
|
7
|
+
* attribute the content API never serialises. What the host actually receives
|
|
8
|
+
* is the summary below, which the service builds by hand and which carries
|
|
9
|
+
* the token and the ready-made link; what an invitee receives is a fixed,
|
|
10
|
+
* whitelisted projection of the tournament plus a `state`.
|
|
11
|
+
*/
|
|
12
|
+
import type { TournamentCurrentStatus, TournamentTeamSize, TournamentType } from '../generated/enums';
|
|
13
|
+
import type { StrapiMedia } from '../contracts';
|
|
14
|
+
/**
|
|
15
|
+
* Statuses in which a host may mint an invite and an invitee may use one.
|
|
16
|
+
* The same window as joining: a link nobody can use yet is only confusing.
|
|
17
|
+
*/
|
|
18
|
+
export declare const INVITABLE_STATUSES: readonly ["open", "checkIn"];
|
|
19
|
+
export type InvitableStatus = (typeof INVITABLE_STATUSES)[number];
|
|
20
|
+
/** True when `status` still lets a host mint, and a player use, a link. */
|
|
21
|
+
export declare function isTournamentInvitable(status: string | null | undefined): status is InvitableStatus;
|
|
22
|
+
/** How long a freshly minted link stays valid, in days. */
|
|
23
|
+
export declare const TOURNAMENT_INVITE_TTL_DAYS = 30;
|
|
24
|
+
/**
|
|
25
|
+
* What a link resolves to for one signed-in viewer, from
|
|
26
|
+
* `GET /tournaments/invites/:token`. Reported in a fixed order — row
|
|
27
|
+
* problems first, then the tournament's window, then the viewer's own
|
|
28
|
+
* standing — so a page can branch on it without second-guessing.
|
|
29
|
+
*
|
|
30
|
+
* Only `valid` lets the join button through. The join itself re-checks
|
|
31
|
+
* capacity and duplicates under the tournament row lock, so `full` and
|
|
32
|
+
* `alreadyRegistered` here are a courtesy, not the guard.
|
|
33
|
+
*/
|
|
34
|
+
export type TournamentInviteState = 'valid' | 'revoked' | 'expired' | 'notOpen' | 'closed' | 'full' | 'alreadyRegistered' | 'host';
|
|
35
|
+
/**
|
|
36
|
+
* The host's view, from `GET`/`POST`/`DELETE /tournaments/:id/invite`.
|
|
37
|
+
* `GET` answers `null` when no link has been minted; `DELETE` answers the
|
|
38
|
+
* revoked row, or `null` when there was nothing to revoke.
|
|
39
|
+
*/
|
|
40
|
+
export interface TournamentInviteSummary {
|
|
41
|
+
documentId: string;
|
|
42
|
+
/** The secret. Only ever sent to the host. */
|
|
43
|
+
token: string;
|
|
44
|
+
/** The landing URL, minted server-side from `FRONTEND_URL`: `/invite/<token>`. */
|
|
45
|
+
url: string;
|
|
46
|
+
status: 'active' | 'revoked';
|
|
47
|
+
expiresAt: string | null;
|
|
48
|
+
createdAt: string | null;
|
|
49
|
+
revokedAt: string | null;
|
|
50
|
+
/** Successful joins through this link. */
|
|
51
|
+
useCount: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The slice of a tournament an invitee may see before joining. Nothing here
|
|
55
|
+
* that the public tournament page does not already show: no participant
|
|
56
|
+
* list, no host id.
|
|
57
|
+
*/
|
|
58
|
+
export interface TournamentInviteTournament {
|
|
59
|
+
documentId: string;
|
|
60
|
+
title: string | null;
|
|
61
|
+
slug: string | null;
|
|
62
|
+
currentStatus: TournamentCurrentStatus | null;
|
|
63
|
+
dateTime: string | null;
|
|
64
|
+
entryFee: number | null;
|
|
65
|
+
/** Declared capacity, in entries. */
|
|
66
|
+
participants: number | null;
|
|
67
|
+
/** Entries taken so far. */
|
|
68
|
+
entrantCount: number;
|
|
69
|
+
teamSize: TournamentTeamSize | null;
|
|
70
|
+
type: TournamentType | null;
|
|
71
|
+
imgMain: StrapiMedia | null;
|
|
72
|
+
game: {
|
|
73
|
+
documentId?: string;
|
|
74
|
+
title?: string | null;
|
|
75
|
+
slug?: string | null;
|
|
76
|
+
} | null;
|
|
77
|
+
gameAccount: {
|
|
78
|
+
documentId?: string;
|
|
79
|
+
name?: string | null;
|
|
80
|
+
} | null;
|
|
81
|
+
platform: {
|
|
82
|
+
documentId?: string;
|
|
83
|
+
name?: string | null;
|
|
84
|
+
} | null;
|
|
85
|
+
host: {
|
|
86
|
+
username?: string | null;
|
|
87
|
+
} | null;
|
|
88
|
+
}
|
|
89
|
+
/** Body of `GET /tournaments/invites/:token` for a signed-in viewer. */
|
|
90
|
+
export interface TournamentInvitePreview {
|
|
91
|
+
state: TournamentInviteState;
|
|
92
|
+
expiresAt: string | null;
|
|
93
|
+
tournament: TournamentInviteTournament;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Body of `POST /tournaments/invites/:token/join`. Flat, exactly like
|
|
97
|
+
* `POST /tournaments/:id/join`: omit both for a solo entry, send both for a
|
|
98
|
+
* team entry.
|
|
99
|
+
*/
|
|
100
|
+
export interface TournamentInviteJoinInput {
|
|
101
|
+
teamId?: string;
|
|
102
|
+
selectedMemberIds?: number[];
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* `error.details.name` values the invite routes raise. `NotTournamentHost`
|
|
106
|
+
* arrives as a 403 and `InviteNotFound` as a 404; `InviteUnavailable` is a
|
|
107
|
+
* 400 whose `details.state` is `revoked` or `expired`. A join through a link
|
|
108
|
+
* can also raise any `TournamentEntryErrorName`, since it is the same join.
|
|
109
|
+
*/
|
|
110
|
+
export type TournamentInviteErrorName = 'NotTournamentHost' | 'InviteNotFound' | 'InviteUnavailable'
|
|
111
|
+
/** An invite-only tournament was joined without a link (plain `/join`, 400). */
|
|
112
|
+
| 'InviteRequired'
|
|
113
|
+
/** An approval tournament was joined without a link or the host's yes. */
|
|
114
|
+
| 'ApprovalRequired';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tournament invite links — the contract between the host's invite dialog,
|
|
4
|
+
* the invitee's landing page and the five `/tournaments/…/invite(s)` routes.
|
|
5
|
+
*
|
|
6
|
+
* The `tournament-invite` content type behind these is generated
|
|
7
|
+
* (`TournamentInvite` in `generated/models.ts`), but its `token` is a private
|
|
8
|
+
* attribute the content API never serialises. What the host actually receives
|
|
9
|
+
* is the summary below, which the service builds by hand and which carries
|
|
10
|
+
* the token and the ready-made link; what an invitee receives is a fixed,
|
|
11
|
+
* whitelisted projection of the tournament plus a `state`.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.TOURNAMENT_INVITE_TTL_DAYS = exports.INVITABLE_STATUSES = void 0;
|
|
15
|
+
exports.isTournamentInvitable = isTournamentInvitable;
|
|
16
|
+
/**
|
|
17
|
+
* Statuses in which a host may mint an invite and an invitee may use one.
|
|
18
|
+
* The same window as joining: a link nobody can use yet is only confusing.
|
|
19
|
+
*/
|
|
20
|
+
exports.INVITABLE_STATUSES = ['open', 'checkIn'];
|
|
21
|
+
/** True when `status` still lets a host mint, and a player use, a link. */
|
|
22
|
+
function isTournamentInvitable(status) {
|
|
23
|
+
return exports.INVITABLE_STATUSES.includes(status !== null && status !== void 0 ? status : '');
|
|
24
|
+
}
|
|
25
|
+
/** How long a freshly minted link stays valid, in days. */
|
|
26
|
+
exports.TOURNAMENT_INVITE_TTL_DAYS = 30;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visibility, access and join requests — the contract between the host's
|
|
3
|
+
* Info dialog, the Join CTA, the Participants dialog and the
|
|
4
|
+
* `/tournaments/:id/join-requests` routes.
|
|
5
|
+
*
|
|
6
|
+
* The enums themselves are generated (`TournamentVisibility`,
|
|
7
|
+
* `TournamentAccess`, `TournamentJoinRequestStatus`); this file holds the
|
|
8
|
+
* response shapes the service builds by hand, the inputs, and the small
|
|
9
|
+
* predicates both apps branch on so their buttons and the server agree.
|
|
10
|
+
*/
|
|
11
|
+
import type { TournamentAccess, TournamentJoinRequestStatus, TournamentVisibility } from '../generated/enums';
|
|
12
|
+
import type { StrapiMedia } from '../contracts';
|
|
13
|
+
/** Statuses in which a player may request and a host may approve. */
|
|
14
|
+
export declare const REQUESTABLE_STATUSES: readonly ["open", "checkIn"];
|
|
15
|
+
export declare function isTournamentRequestable(status: string | null | undefined): boolean;
|
|
16
|
+
/** Entry needs the host's link — or, on an approval tournament, their yes. */
|
|
17
|
+
export declare const isInviteOnly: (access: string | null | undefined) => boolean;
|
|
18
|
+
export declare const isApprovalAccess: (access: string | null | undefined) => boolean;
|
|
19
|
+
/**
|
|
20
|
+
* The host's Invite button shows only where the public door is shut: on an
|
|
21
|
+
* invite-only tournament the link is the only way in, on an approval one it
|
|
22
|
+
* is a way past the queue. An open tournament has the public Share button.
|
|
23
|
+
*/
|
|
24
|
+
export declare const showsInviteButton: (access: string | null | undefined) => boolean;
|
|
25
|
+
export declare const isUnlistedVisibility: (visibility: string | null | undefined) => boolean;
|
|
26
|
+
/** Copy for the Info card and dialogs. One place, so every surface agrees. */
|
|
27
|
+
export declare const VISIBILITY_LABELS: Record<TournamentVisibility, string>;
|
|
28
|
+
export declare const ACCESS_LABELS: Record<TournamentAccess, string>;
|
|
29
|
+
export declare const visibilityLabel: (visibility: string | null | undefined) => string;
|
|
30
|
+
export declare const accessLabel: (access: string | null | undefined) => string;
|
|
31
|
+
/** What clients see of a request. Nothing the participants list would not show. */
|
|
32
|
+
export interface TournamentJoinRequestSummary {
|
|
33
|
+
documentId: string;
|
|
34
|
+
status: TournamentJoinRequestStatus;
|
|
35
|
+
entryType: 'solo' | 'team';
|
|
36
|
+
displayName: string | null;
|
|
37
|
+
message: string | null;
|
|
38
|
+
/** Held from the requester's wallet until the host decides. */
|
|
39
|
+
feePaid: number;
|
|
40
|
+
createdAt: string | null;
|
|
41
|
+
decidedAt: string | null;
|
|
42
|
+
requester: {
|
|
43
|
+
id: number;
|
|
44
|
+
username: string | null;
|
|
45
|
+
image: StrapiMedia | null;
|
|
46
|
+
} | null;
|
|
47
|
+
team: {
|
|
48
|
+
documentId: string;
|
|
49
|
+
name: string | null;
|
|
50
|
+
} | null;
|
|
51
|
+
/** The roster as requested; the approval re-reads the live one. */
|
|
52
|
+
membersSnapshot: unknown;
|
|
53
|
+
memberCount: number;
|
|
54
|
+
}
|
|
55
|
+
/** Body of `GET /tournaments/:id/join-requests` for a signed-in viewer. */
|
|
56
|
+
export interface TournamentJoinRequestsResponse {
|
|
57
|
+
/** Pending requests, oldest first. Empty for anyone but the host. */
|
|
58
|
+
requests: TournamentJoinRequestSummary[];
|
|
59
|
+
/** The viewer's own most recent request, whatever its status. */
|
|
60
|
+
mine: TournamentJoinRequestSummary | null;
|
|
61
|
+
pendingCount: number;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Body of `POST /tournaments/:id/join-requests` (the generated `TournamentJoinRequestInput` is the row's write shape). Flat, like `/join`: omit
|
|
65
|
+
* the team fields for a solo entry, send both for a team entry.
|
|
66
|
+
*/
|
|
67
|
+
export interface TournamentJoinRequestBody {
|
|
68
|
+
teamId?: string;
|
|
69
|
+
selectedMemberIds?: number[];
|
|
70
|
+
/** ≤200 characters, shown to the host with the request. */
|
|
71
|
+
message?: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* `error.details.name` values the join-request routes raise. `NotTournamentHost`
|
|
75
|
+
* is a 403 and `JoinRequestNotFound` a 404; the rest are 400s. A plain `/join`
|
|
76
|
+
* on an approval tournament raises `ApprovalRequired`. An approval runs the
|
|
77
|
+
* ordinary join, so it can also raise any `TournamentEntryErrorName`.
|
|
78
|
+
*/
|
|
79
|
+
export type TournamentJoinRequestErrorName = 'ApprovalRequired' | 'JoinRequestNotAllowed' | 'JoinRequestPending' | 'JoinRequestNotFound' | 'JoinRequestNotPending' | 'NotTournamentHost';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Visibility, access and join requests — the contract between the host's
|
|
4
|
+
* Info dialog, the Join CTA, the Participants dialog and the
|
|
5
|
+
* `/tournaments/:id/join-requests` routes.
|
|
6
|
+
*
|
|
7
|
+
* The enums themselves are generated (`TournamentVisibility`,
|
|
8
|
+
* `TournamentAccess`, `TournamentJoinRequestStatus`); this file holds the
|
|
9
|
+
* response shapes the service builds by hand, the inputs, and the small
|
|
10
|
+
* predicates both apps branch on so their buttons and the server agree.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.accessLabel = exports.visibilityLabel = exports.ACCESS_LABELS = exports.VISIBILITY_LABELS = exports.isUnlistedVisibility = exports.showsInviteButton = exports.isApprovalAccess = exports.isInviteOnly = exports.REQUESTABLE_STATUSES = void 0;
|
|
14
|
+
exports.isTournamentRequestable = isTournamentRequestable;
|
|
15
|
+
/** Statuses in which a player may request and a host may approve. */
|
|
16
|
+
exports.REQUESTABLE_STATUSES = ['open', 'checkIn'];
|
|
17
|
+
function isTournamentRequestable(status) {
|
|
18
|
+
return exports.REQUESTABLE_STATUSES.includes(status !== null && status !== void 0 ? status : '');
|
|
19
|
+
}
|
|
20
|
+
/** Entry needs the host's link — or, on an approval tournament, their yes. */
|
|
21
|
+
const isInviteOnly = (access) => access === 'inviteOnly';
|
|
22
|
+
exports.isInviteOnly = isInviteOnly;
|
|
23
|
+
const isApprovalAccess = (access) => access === 'approval';
|
|
24
|
+
exports.isApprovalAccess = isApprovalAccess;
|
|
25
|
+
/**
|
|
26
|
+
* The host's Invite button shows only where the public door is shut: on an
|
|
27
|
+
* invite-only tournament the link is the only way in, on an approval one it
|
|
28
|
+
* is a way past the queue. An open tournament has the public Share button.
|
|
29
|
+
*/
|
|
30
|
+
const showsInviteButton = (access) => access === 'inviteOnly' || access === 'approval';
|
|
31
|
+
exports.showsInviteButton = showsInviteButton;
|
|
32
|
+
const isUnlistedVisibility = (visibility) => visibility === 'unlisted';
|
|
33
|
+
exports.isUnlistedVisibility = isUnlistedVisibility;
|
|
34
|
+
/** Copy for the Info card and dialogs. One place, so every surface agrees. */
|
|
35
|
+
exports.VISIBILITY_LABELS = {
|
|
36
|
+
public: 'Public',
|
|
37
|
+
unlisted: 'Unlisted',
|
|
38
|
+
};
|
|
39
|
+
exports.ACCESS_LABELS = {
|
|
40
|
+
open: 'Open',
|
|
41
|
+
inviteOnly: 'Invite only',
|
|
42
|
+
approval: 'Approval required',
|
|
43
|
+
};
|
|
44
|
+
const visibilityLabel = (visibility) => { var _a, _b; return (_b = exports.VISIBILITY_LABELS[(_a = visibility) !== null && _a !== void 0 ? _a : 'public']) !== null && _b !== void 0 ? _b : 'Public'; };
|
|
45
|
+
exports.visibilityLabel = visibilityLabel;
|
|
46
|
+
const accessLabel = (access) => { var _a, _b; return (_b = exports.ACCESS_LABELS[(_a = access) !== null && _a !== void 0 ? _a : 'open']) !== null && _b !== void 0 ? _b : 'Open'; };
|
|
47
|
+
exports.accessLabel = accessLabel;
|
package/package.json
CHANGED
package/src/generated/enums.ts
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
// Source: xpt-strapi/src/**/content-types/**/schema.json
|
|
3
3
|
// Regenerate with: yarn sync
|
|
4
4
|
|
|
5
|
+
/** `Faq.category` */
|
|
6
|
+
export type FaqCategory =
|
|
7
|
+
| "gettingStarted"
|
|
8
|
+
| "tournamentsMatches"
|
|
9
|
+
| "hosting"
|
|
10
|
+
| "walletCoins"
|
|
11
|
+
| "account";
|
|
12
|
+
|
|
5
13
|
/** `Friendship.status` */
|
|
6
14
|
export type FriendshipStatus =
|
|
7
15
|
| "pending"
|
|
@@ -143,6 +151,12 @@ export type TeamPlayerTeamRole =
|
|
|
143
151
|
| "substitute"
|
|
144
152
|
| "coach";
|
|
145
153
|
|
|
154
|
+
/** `Tournament.access` */
|
|
155
|
+
export type TournamentAccess =
|
|
156
|
+
| "open"
|
|
157
|
+
| "inviteOnly"
|
|
158
|
+
| "approval";
|
|
159
|
+
|
|
146
160
|
/** `Tournament.currentStatus` */
|
|
147
161
|
export type TournamentCurrentStatus =
|
|
148
162
|
| "draft"
|
|
@@ -153,6 +167,24 @@ export type TournamentCurrentStatus =
|
|
|
153
167
|
| "completed"
|
|
154
168
|
| "cancelled";
|
|
155
169
|
|
|
170
|
+
/** `TournamentInvite.status` */
|
|
171
|
+
export type TournamentInviteStatus =
|
|
172
|
+
| "active"
|
|
173
|
+
| "revoked";
|
|
174
|
+
|
|
175
|
+
/** `TournamentJoinRequest.entryType` */
|
|
176
|
+
export type TournamentJoinRequestEntryType =
|
|
177
|
+
| "solo"
|
|
178
|
+
| "team";
|
|
179
|
+
|
|
180
|
+
/** `TournamentJoinRequest.status` */
|
|
181
|
+
export type TournamentJoinRequestStatus =
|
|
182
|
+
| "pending"
|
|
183
|
+
| "approved"
|
|
184
|
+
| "declined"
|
|
185
|
+
| "withdrawn"
|
|
186
|
+
| "expired";
|
|
187
|
+
|
|
156
188
|
/** `TournamentParticipant.entryType` */
|
|
157
189
|
export type TournamentParticipantEntryType =
|
|
158
190
|
| "solo"
|
|
@@ -184,6 +216,11 @@ export type TournamentType =
|
|
|
184
216
|
| "League"
|
|
185
217
|
| "Double Elimination";
|
|
186
218
|
|
|
219
|
+
/** `Tournament.visibility` */
|
|
220
|
+
export type TournamentVisibility =
|
|
221
|
+
| "public"
|
|
222
|
+
| "unlisted";
|
|
223
|
+
|
|
187
224
|
/** `UserTransaction.stripeStatus` */
|
|
188
225
|
export type UserTransactionStripeStatus =
|
|
189
226
|
| "pending"
|
package/src/generated/inputs.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
RelationInput,
|
|
9
9
|
} from '../contracts';
|
|
10
10
|
import type {
|
|
11
|
+
FaqCategory,
|
|
11
12
|
FriendshipStatus,
|
|
12
13
|
GameGenre,
|
|
13
14
|
GameRequestCustomLobbies,
|
|
@@ -26,12 +27,17 @@ import type {
|
|
|
26
27
|
TeamInviteStatus,
|
|
27
28
|
TeamInviteTeamRole,
|
|
28
29
|
TeamPlayerTeamRole,
|
|
30
|
+
TournamentAccess,
|
|
29
31
|
TournamentCurrentStatus,
|
|
32
|
+
TournamentInviteStatus,
|
|
33
|
+
TournamentJoinRequestEntryType,
|
|
34
|
+
TournamentJoinRequestStatus,
|
|
30
35
|
TournamentParticipantEntryType,
|
|
31
36
|
TournamentParticipantStatus,
|
|
32
37
|
TournamentRoleRole,
|
|
33
38
|
TournamentTeamSize,
|
|
34
39
|
TournamentType,
|
|
40
|
+
TournamentVisibility,
|
|
35
41
|
UserTransactionStripeStatus,
|
|
36
42
|
UserTransactionType,
|
|
37
43
|
} from './enums';
|
|
@@ -56,6 +62,9 @@ export interface DmMessageInput {
|
|
|
56
62
|
export interface FaqInput {
|
|
57
63
|
question?: string;
|
|
58
64
|
answer?: BlocksContent;
|
|
65
|
+
category?: FaqCategory;
|
|
66
|
+
note?: string;
|
|
67
|
+
order?: number;
|
|
59
68
|
}
|
|
60
69
|
|
|
61
70
|
/** Write payload for `featured-game`. */
|
|
@@ -363,7 +372,8 @@ export interface TournamentInput {
|
|
|
363
372
|
rules?: string;
|
|
364
373
|
hasThirdPlace?: boolean;
|
|
365
374
|
hasMatchLobby?: boolean;
|
|
366
|
-
|
|
375
|
+
visibility?: TournamentVisibility;
|
|
376
|
+
access?: TournamentAccess;
|
|
367
377
|
checkInTime?: number;
|
|
368
378
|
region?: RelationInput;
|
|
369
379
|
platform?: RelationInput;
|
|
@@ -376,6 +386,34 @@ export interface TournamentInput {
|
|
|
376
386
|
websiteUrl?: string;
|
|
377
387
|
}
|
|
378
388
|
|
|
389
|
+
/** Write payload for `tournament-invite`. */
|
|
390
|
+
export interface TournamentInviteInput {
|
|
391
|
+
tournament?: RelationInput;
|
|
392
|
+
createdByUser?: RelationInput;
|
|
393
|
+
status?: TournamentInviteStatus;
|
|
394
|
+
expiresAt?: string;
|
|
395
|
+
revokedAt?: string;
|
|
396
|
+
useCount?: number;
|
|
397
|
+
lastUsedAt?: string;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Write payload for `tournament-join-request`. */
|
|
401
|
+
export interface TournamentJoinRequestInput {
|
|
402
|
+
tournament?: RelationInput;
|
|
403
|
+
requester?: RelationInput;
|
|
404
|
+
entryType?: TournamentJoinRequestEntryType;
|
|
405
|
+
team?: RelationInput;
|
|
406
|
+
selectedMemberIds?: unknown;
|
|
407
|
+
membersSnapshot?: unknown;
|
|
408
|
+
displayName?: string;
|
|
409
|
+
message?: string;
|
|
410
|
+
feePaid?: number;
|
|
411
|
+
status?: TournamentJoinRequestStatus;
|
|
412
|
+
decidedAt?: string;
|
|
413
|
+
decidedBy?: RelationInput;
|
|
414
|
+
decisionReason?: string;
|
|
415
|
+
}
|
|
416
|
+
|
|
379
417
|
/** Write payload for `tournament-participant`. */
|
|
380
418
|
export interface TournamentParticipantInput {
|
|
381
419
|
tournament?: RelationInput;
|
package/src/generated/models.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
UserRole,
|
|
10
10
|
} from '../contracts';
|
|
11
11
|
import type {
|
|
12
|
+
FaqCategory,
|
|
12
13
|
FriendshipStatus,
|
|
13
14
|
GameGenre,
|
|
14
15
|
GameRequestCustomLobbies,
|
|
@@ -27,12 +28,17 @@ import type {
|
|
|
27
28
|
TeamInviteStatus,
|
|
28
29
|
TeamInviteTeamRole,
|
|
29
30
|
TeamPlayerTeamRole,
|
|
31
|
+
TournamentAccess,
|
|
30
32
|
TournamentCurrentStatus,
|
|
33
|
+
TournamentInviteStatus,
|
|
34
|
+
TournamentJoinRequestEntryType,
|
|
35
|
+
TournamentJoinRequestStatus,
|
|
31
36
|
TournamentParticipantEntryType,
|
|
32
37
|
TournamentParticipantStatus,
|
|
33
38
|
TournamentRoleRole,
|
|
34
39
|
TournamentTeamSize,
|
|
35
40
|
TournamentType,
|
|
41
|
+
TournamentVisibility,
|
|
36
42
|
UserTransactionStripeStatus,
|
|
37
43
|
UserTransactionType,
|
|
38
44
|
} from './enums';
|
|
@@ -57,6 +63,9 @@ export interface DmMessage extends StrapiDocument {
|
|
|
57
63
|
export interface Faq extends StrapiDocument {
|
|
58
64
|
question?: string | null;
|
|
59
65
|
answer?: BlocksContent | null;
|
|
66
|
+
category?: FaqCategory | null;
|
|
67
|
+
note?: string | null;
|
|
68
|
+
order?: number | null;
|
|
60
69
|
}
|
|
61
70
|
|
|
62
71
|
/** `featured-game` */
|
|
@@ -364,7 +373,8 @@ export interface Tournament extends StrapiDocument {
|
|
|
364
373
|
rules?: string | null;
|
|
365
374
|
hasThirdPlace?: boolean | null;
|
|
366
375
|
hasMatchLobby?: boolean | null;
|
|
367
|
-
|
|
376
|
+
visibility?: TournamentVisibility | null;
|
|
377
|
+
access?: TournamentAccess | null;
|
|
368
378
|
checkInTime?: number | null;
|
|
369
379
|
region?: Region | null;
|
|
370
380
|
platform?: Platform | null;
|
|
@@ -377,6 +387,34 @@ export interface Tournament extends StrapiDocument {
|
|
|
377
387
|
websiteUrl?: string | null;
|
|
378
388
|
}
|
|
379
389
|
|
|
390
|
+
/** `tournament-invite` */
|
|
391
|
+
export interface TournamentInvite extends StrapiDocument {
|
|
392
|
+
tournament?: Tournament | null;
|
|
393
|
+
createdByUser?: User | null;
|
|
394
|
+
status?: TournamentInviteStatus | null;
|
|
395
|
+
expiresAt?: string | null;
|
|
396
|
+
revokedAt?: string | null;
|
|
397
|
+
useCount?: number | null;
|
|
398
|
+
lastUsedAt?: string | null;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** `tournament-join-request` */
|
|
402
|
+
export interface TournamentJoinRequest extends StrapiDocument {
|
|
403
|
+
tournament?: Tournament | null;
|
|
404
|
+
requester?: User | null;
|
|
405
|
+
entryType?: TournamentJoinRequestEntryType | null;
|
|
406
|
+
team?: Team | null;
|
|
407
|
+
selectedMemberIds?: unknown | null;
|
|
408
|
+
membersSnapshot?: unknown | null;
|
|
409
|
+
displayName?: string | null;
|
|
410
|
+
message?: string | null;
|
|
411
|
+
feePaid?: number | null;
|
|
412
|
+
status?: TournamentJoinRequestStatus | null;
|
|
413
|
+
decidedAt?: string | null;
|
|
414
|
+
decidedBy?: User | null;
|
|
415
|
+
decisionReason?: string | null;
|
|
416
|
+
}
|
|
417
|
+
|
|
380
418
|
/** `tournament-participant` */
|
|
381
419
|
export interface TournamentParticipant extends StrapiDocument {
|
|
382
420
|
tournament?: Tournament | null;
|
package/src/manual/index.ts
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tournament entry after registration — withdrawing it and changing its
|
|
3
|
+
* roster.
|
|
4
|
+
*
|
|
5
|
+
* Neither is a Strapi content type. An entry is a `tournament-participant`
|
|
6
|
+
* row; these name the window in which its registrant may still act on it and
|
|
7
|
+
* the bodies of the endpoints that do so, so the backend guards and the
|
|
8
|
+
* client's buttons agree on both.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { TournamentCurrentStatus } from '../generated/enums';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Statuses in which an entry can still be withdrawn or have its roster
|
|
15
|
+
* changed. Everything before is unpublished; everything after has a bracket
|
|
16
|
+
* that reads the entry, so the entry is frozen.
|
|
17
|
+
*/
|
|
18
|
+
export const ENTRY_EDITABLE_STATUSES = ['open', 'checkIn'] as const satisfies
|
|
19
|
+
readonly TournamentCurrentStatus[];
|
|
20
|
+
|
|
21
|
+
export type EntryEditableStatus = (typeof ENTRY_EDITABLE_STATUSES)[number];
|
|
22
|
+
|
|
23
|
+
/** True when `status` still lets an entry be withdrawn or re-rostered. */
|
|
24
|
+
export function isEntryEditable(
|
|
25
|
+
status: string | null | undefined
|
|
26
|
+
): status is EntryEditableStatus {
|
|
27
|
+
return (ENTRY_EDITABLE_STATUSES as readonly string[]).includes(status ?? '');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Body of `PUT /tournaments/:id/entry/roster`. Flat, like `/join`, not inside
|
|
32
|
+
* `data`. The full new roster, not a diff — the server works out who was
|
|
33
|
+
* added and who was removed.
|
|
34
|
+
*/
|
|
35
|
+
export interface TournamentRosterUpdateInput {
|
|
36
|
+
selectedMemberIds: number[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Body of `POST /tournaments/:id/join` for a team entry. Flat, like above. */
|
|
40
|
+
export interface TournamentTeamJoinInput {
|
|
41
|
+
teamId: string;
|
|
42
|
+
selectedMemberIds: number[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* `error.details.name` values the entry endpoints raise that a client is
|
|
47
|
+
* expected to branch on. Anything else is a plain message.
|
|
48
|
+
*/
|
|
49
|
+
export type TournamentEntryErrorName =
|
|
50
|
+
| 'NotEntryLeader'
|
|
51
|
+
| 'MembersMissingGameAccount'
|
|
52
|
+
| 'InsufficientCoins'
|
|
53
|
+
| 'GameAccountRequired'
|
|
54
|
+
| 'TeamEntryRequired';
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tournament invite links — the contract between the host's invite dialog,
|
|
3
|
+
* the invitee's landing page and the five `/tournaments/…/invite(s)` routes.
|
|
4
|
+
*
|
|
5
|
+
* The `tournament-invite` content type behind these is generated
|
|
6
|
+
* (`TournamentInvite` in `generated/models.ts`), but its `token` is a private
|
|
7
|
+
* attribute the content API never serialises. What the host actually receives
|
|
8
|
+
* is the summary below, which the service builds by hand and which carries
|
|
9
|
+
* the token and the ready-made link; what an invitee receives is a fixed,
|
|
10
|
+
* whitelisted projection of the tournament plus a `state`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
TournamentCurrentStatus,
|
|
15
|
+
TournamentTeamSize,
|
|
16
|
+
TournamentType,
|
|
17
|
+
} from '../generated/enums';
|
|
18
|
+
import type { StrapiMedia } from '../contracts';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Statuses in which a host may mint an invite and an invitee may use one.
|
|
22
|
+
* The same window as joining: a link nobody can use yet is only confusing.
|
|
23
|
+
*/
|
|
24
|
+
export const INVITABLE_STATUSES = ['open', 'checkIn'] as const satisfies
|
|
25
|
+
readonly TournamentCurrentStatus[];
|
|
26
|
+
|
|
27
|
+
export type InvitableStatus = (typeof INVITABLE_STATUSES)[number];
|
|
28
|
+
|
|
29
|
+
/** True when `status` still lets a host mint, and a player use, a link. */
|
|
30
|
+
export function isTournamentInvitable(
|
|
31
|
+
status: string | null | undefined
|
|
32
|
+
): status is InvitableStatus {
|
|
33
|
+
return (INVITABLE_STATUSES as readonly string[]).includes(status ?? '');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** How long a freshly minted link stays valid, in days. */
|
|
37
|
+
export const TOURNAMENT_INVITE_TTL_DAYS = 30;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* What a link resolves to for one signed-in viewer, from
|
|
41
|
+
* `GET /tournaments/invites/:token`. Reported in a fixed order — row
|
|
42
|
+
* problems first, then the tournament's window, then the viewer's own
|
|
43
|
+
* standing — so a page can branch on it without second-guessing.
|
|
44
|
+
*
|
|
45
|
+
* Only `valid` lets the join button through. The join itself re-checks
|
|
46
|
+
* capacity and duplicates under the tournament row lock, so `full` and
|
|
47
|
+
* `alreadyRegistered` here are a courtesy, not the guard.
|
|
48
|
+
*/
|
|
49
|
+
export type TournamentInviteState =
|
|
50
|
+
| 'valid'
|
|
51
|
+
| 'revoked'
|
|
52
|
+
| 'expired'
|
|
53
|
+
| 'notOpen'
|
|
54
|
+
| 'closed'
|
|
55
|
+
| 'full'
|
|
56
|
+
| 'alreadyRegistered'
|
|
57
|
+
| 'host';
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The host's view, from `GET`/`POST`/`DELETE /tournaments/:id/invite`.
|
|
61
|
+
* `GET` answers `null` when no link has been minted; `DELETE` answers the
|
|
62
|
+
* revoked row, or `null` when there was nothing to revoke.
|
|
63
|
+
*/
|
|
64
|
+
export interface TournamentInviteSummary {
|
|
65
|
+
documentId: string;
|
|
66
|
+
/** The secret. Only ever sent to the host. */
|
|
67
|
+
token: string;
|
|
68
|
+
/** The landing URL, minted server-side from `FRONTEND_URL`: `/invite/<token>`. */
|
|
69
|
+
url: string;
|
|
70
|
+
status: 'active' | 'revoked';
|
|
71
|
+
expiresAt: string | null;
|
|
72
|
+
createdAt: string | null;
|
|
73
|
+
revokedAt: string | null;
|
|
74
|
+
/** Successful joins through this link. */
|
|
75
|
+
useCount: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The slice of a tournament an invitee may see before joining. Nothing here
|
|
80
|
+
* that the public tournament page does not already show: no participant
|
|
81
|
+
* list, no host id.
|
|
82
|
+
*/
|
|
83
|
+
export interface TournamentInviteTournament {
|
|
84
|
+
documentId: string;
|
|
85
|
+
title: string | null;
|
|
86
|
+
slug: string | null;
|
|
87
|
+
currentStatus: TournamentCurrentStatus | null;
|
|
88
|
+
dateTime: string | null;
|
|
89
|
+
entryFee: number | null;
|
|
90
|
+
/** Declared capacity, in entries. */
|
|
91
|
+
participants: number | null;
|
|
92
|
+
/** Entries taken so far. */
|
|
93
|
+
entrantCount: number;
|
|
94
|
+
teamSize: TournamentTeamSize | null;
|
|
95
|
+
type: TournamentType | null;
|
|
96
|
+
imgMain: StrapiMedia | null;
|
|
97
|
+
game: { documentId?: string; title?: string | null; slug?: string | null } | null;
|
|
98
|
+
gameAccount: { documentId?: string; name?: string | null } | null;
|
|
99
|
+
platform: { documentId?: string; name?: string | null } | null;
|
|
100
|
+
host: { username?: string | null } | null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Body of `GET /tournaments/invites/:token` for a signed-in viewer. */
|
|
104
|
+
export interface TournamentInvitePreview {
|
|
105
|
+
state: TournamentInviteState;
|
|
106
|
+
expiresAt: string | null;
|
|
107
|
+
tournament: TournamentInviteTournament;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Body of `POST /tournaments/invites/:token/join`. Flat, exactly like
|
|
112
|
+
* `POST /tournaments/:id/join`: omit both for a solo entry, send both for a
|
|
113
|
+
* team entry.
|
|
114
|
+
*/
|
|
115
|
+
export interface TournamentInviteJoinInput {
|
|
116
|
+
teamId?: string;
|
|
117
|
+
selectedMemberIds?: number[];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* `error.details.name` values the invite routes raise. `NotTournamentHost`
|
|
122
|
+
* arrives as a 403 and `InviteNotFound` as a 404; `InviteUnavailable` is a
|
|
123
|
+
* 400 whose `details.state` is `revoked` or `expired`. A join through a link
|
|
124
|
+
* can also raise any `TournamentEntryErrorName`, since it is the same join.
|
|
125
|
+
*/
|
|
126
|
+
export type TournamentInviteErrorName =
|
|
127
|
+
| 'NotTournamentHost'
|
|
128
|
+
| 'InviteNotFound'
|
|
129
|
+
| 'InviteUnavailable'
|
|
130
|
+
/** An invite-only tournament was joined without a link (plain `/join`, 400). */
|
|
131
|
+
| 'InviteRequired'
|
|
132
|
+
/** An approval tournament was joined without a link or the host's yes. */
|
|
133
|
+
| 'ApprovalRequired';
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visibility, access and join requests — the contract between the host's
|
|
3
|
+
* Info dialog, the Join CTA, the Participants dialog and the
|
|
4
|
+
* `/tournaments/:id/join-requests` routes.
|
|
5
|
+
*
|
|
6
|
+
* The enums themselves are generated (`TournamentVisibility`,
|
|
7
|
+
* `TournamentAccess`, `TournamentJoinRequestStatus`); this file holds the
|
|
8
|
+
* response shapes the service builds by hand, the inputs, and the small
|
|
9
|
+
* predicates both apps branch on so their buttons and the server agree.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
TournamentAccess,
|
|
14
|
+
TournamentCurrentStatus,
|
|
15
|
+
TournamentJoinRequestStatus,
|
|
16
|
+
TournamentVisibility,
|
|
17
|
+
} from '../generated/enums';
|
|
18
|
+
import type { StrapiMedia } from '../contracts';
|
|
19
|
+
|
|
20
|
+
/** Statuses in which a player may request and a host may approve. */
|
|
21
|
+
export const REQUESTABLE_STATUSES = ['open', 'checkIn'] as const satisfies
|
|
22
|
+
readonly TournamentCurrentStatus[];
|
|
23
|
+
|
|
24
|
+
export function isTournamentRequestable(
|
|
25
|
+
status: string | null | undefined
|
|
26
|
+
): boolean {
|
|
27
|
+
return (REQUESTABLE_STATUSES as readonly string[]).includes(status ?? '');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Entry needs the host's link — or, on an approval tournament, their yes. */
|
|
31
|
+
export const isInviteOnly = (access: string | null | undefined): boolean =>
|
|
32
|
+
access === 'inviteOnly';
|
|
33
|
+
|
|
34
|
+
export const isApprovalAccess = (access: string | null | undefined): boolean =>
|
|
35
|
+
access === 'approval';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The host's Invite button shows only where the public door is shut: on an
|
|
39
|
+
* invite-only tournament the link is the only way in, on an approval one it
|
|
40
|
+
* is a way past the queue. An open tournament has the public Share button.
|
|
41
|
+
*/
|
|
42
|
+
export const showsInviteButton = (access: string | null | undefined): boolean =>
|
|
43
|
+
access === 'inviteOnly' || access === 'approval';
|
|
44
|
+
|
|
45
|
+
export const isUnlistedVisibility = (
|
|
46
|
+
visibility: string | null | undefined
|
|
47
|
+
): boolean => visibility === 'unlisted';
|
|
48
|
+
|
|
49
|
+
/** Copy for the Info card and dialogs. One place, so every surface agrees. */
|
|
50
|
+
export const VISIBILITY_LABELS: Record<TournamentVisibility, string> = {
|
|
51
|
+
public: 'Public',
|
|
52
|
+
unlisted: 'Unlisted',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const ACCESS_LABELS: Record<TournamentAccess, string> = {
|
|
56
|
+
open: 'Open',
|
|
57
|
+
inviteOnly: 'Invite only',
|
|
58
|
+
approval: 'Approval required',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const visibilityLabel = (
|
|
62
|
+
visibility: string | null | undefined
|
|
63
|
+
): string => VISIBILITY_LABELS[(visibility as TournamentVisibility) ?? 'public'] ?? 'Public';
|
|
64
|
+
|
|
65
|
+
export const accessLabel = (access: string | null | undefined): string =>
|
|
66
|
+
ACCESS_LABELS[(access as TournamentAccess) ?? 'open'] ?? 'Open';
|
|
67
|
+
|
|
68
|
+
/** What clients see of a request. Nothing the participants list would not show. */
|
|
69
|
+
export interface TournamentJoinRequestSummary {
|
|
70
|
+
documentId: string;
|
|
71
|
+
status: TournamentJoinRequestStatus;
|
|
72
|
+
entryType: 'solo' | 'team';
|
|
73
|
+
displayName: string | null;
|
|
74
|
+
message: string | null;
|
|
75
|
+
/** Held from the requester's wallet until the host decides. */
|
|
76
|
+
feePaid: number;
|
|
77
|
+
createdAt: string | null;
|
|
78
|
+
decidedAt: string | null;
|
|
79
|
+
requester: { id: number; username: string | null; image: StrapiMedia | null } | null;
|
|
80
|
+
team: { documentId: string; name: string | null } | null;
|
|
81
|
+
/** The roster as requested; the approval re-reads the live one. */
|
|
82
|
+
membersSnapshot: unknown;
|
|
83
|
+
memberCount: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Body of `GET /tournaments/:id/join-requests` for a signed-in viewer. */
|
|
87
|
+
export interface TournamentJoinRequestsResponse {
|
|
88
|
+
/** Pending requests, oldest first. Empty for anyone but the host. */
|
|
89
|
+
requests: TournamentJoinRequestSummary[];
|
|
90
|
+
/** The viewer's own most recent request, whatever its status. */
|
|
91
|
+
mine: TournamentJoinRequestSummary | null;
|
|
92
|
+
pendingCount: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Body of `POST /tournaments/:id/join-requests` (the generated `TournamentJoinRequestInput` is the row's write shape). Flat, like `/join`: omit
|
|
97
|
+
* the team fields for a solo entry, send both for a team entry.
|
|
98
|
+
*/
|
|
99
|
+
export interface TournamentJoinRequestBody {
|
|
100
|
+
teamId?: string;
|
|
101
|
+
selectedMemberIds?: number[];
|
|
102
|
+
/** ≤200 characters, shown to the host with the request. */
|
|
103
|
+
message?: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* `error.details.name` values the join-request routes raise. `NotTournamentHost`
|
|
108
|
+
* is a 403 and `JoinRequestNotFound` a 404; the rest are 400s. A plain `/join`
|
|
109
|
+
* on an approval tournament raises `ApprovalRequired`. An approval runs the
|
|
110
|
+
* ordinary join, so it can also raise any `TournamentEntryErrorName`.
|
|
111
|
+
*/
|
|
112
|
+
export type TournamentJoinRequestErrorName =
|
|
113
|
+
| 'ApprovalRequired'
|
|
114
|
+
| 'JoinRequestNotAllowed'
|
|
115
|
+
| 'JoinRequestPending'
|
|
116
|
+
| 'JoinRequestNotFound'
|
|
117
|
+
| 'JoinRequestNotPending'
|
|
118
|
+
| 'NotTournamentHost';
|