xpt-shared-types 1.9.0 → 1.12.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.
@@ -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
- isPrivate?: boolean;
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
- isPrivate?: boolean | null;
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;
@@ -4,3 +4,6 @@
4
4
  export * from './country';
5
5
  export * from './lobby';
6
6
  export * from './tournamentEntry';
7
+ export * from './tournamentInvite';
8
+ export * from './tournamentJoinRequest';
9
+ export * from './tournamentStaff';
@@ -20,3 +20,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
20
20
  __exportStar(require("./country"), exports);
21
21
  __exportStar(require("./lobby"), exports);
22
22
  __exportStar(require("./tournamentEntry"), exports);
23
+ __exportStar(require("./tournamentInvite"), exports);
24
+ __exportStar(require("./tournamentJoinRequest"), exports);
25
+ __exportStar(require("./tournamentStaff"), exports);
@@ -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;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Tournament staff — who runs a tournament and what each of them may do.
3
+ *
4
+ * Three roles. The **owner** is the tournament's `users_permissions_user`
5
+ * and never a row; **admin** and **moderator** are `tournament-role` rows
6
+ * (the `TournamentRoleRole` enum is generated from the schema). A row with
7
+ * no role value is read as a moderator, the least the row can mean.
8
+ *
9
+ * The capability table is the contract between the server's guards and the
10
+ * client's buttons: both resolve the viewer's role with
11
+ * `resolveTournamentStaffRole` and ask `canTournamentStaff`, so a control is
12
+ * never offered for a request the server would refuse. Money and the
13
+ * irreversible lifecycle steps stay with the owner; admins run the event;
14
+ * moderators run matches.
15
+ */
16
+ import type { TournamentRoleRole } from '../generated/enums';
17
+ export type TournamentStaffRole = 'owner' | TournamentRoleRole;
18
+ /** The roles the owner can hand out — everything but their own. */
19
+ export type TournamentAppointedRole = TournamentRoleRole;
20
+ export declare const APPOINTABLE_STAFF_ROLES: readonly ["admin", "moderator"];
21
+ export type TournamentStaffAction =
22
+ /** Appoint, re-role or remove admins and moderators. */
23
+ 'manageStaff'
24
+ /** `PUT /tournaments/:id` — info, rules, stages, links. */
25
+ | 'editTournament'
26
+ /** Draft → open; debits the prize pool. */
27
+ | 'publish'
28
+ /** Drafts only. */
29
+ | 'delete'
30
+ /** The forward transitions: checkIn, seeding, live, completed. */
31
+ | 'changeStatus'
32
+ /** Refunds every entrant. */
33
+ | 'cancel' | 'distributePrizes' | 'manageInvites' | 'reviewJoinRequests' | 'resolveDispute'
34
+ /** Seed, initialise the bracket, place players in slots. */
35
+ | 'seedBracket'
36
+ /** Record scores, start matches, edit match details. */
37
+ | 'manageMatches'
38
+ /** Watch any lobby and post as staff in its chat. */
39
+ | 'enterLobby';
40
+ export declare const TOURNAMENT_STAFF_CAPABILITIES: Record<TournamentStaffAction, readonly TournamentStaffRole[]>;
41
+ export declare const TOURNAMENT_STAFF_ROLE_LABELS: Record<TournamentStaffRole, string>;
42
+ export declare const tournamentStaffRoleLabel: (role: string | null | undefined) => string;
43
+ /** Only what the resolver reads; both apps' tournament shapes satisfy it. */
44
+ export interface TournamentStaffLike {
45
+ users_permissions_user?: {
46
+ id?: number | string | null;
47
+ } | null;
48
+ tournament_roles?: Array<{
49
+ role?: string | null;
50
+ users_permissions_user?: {
51
+ id?: number | string | null;
52
+ } | null;
53
+ }> | null;
54
+ }
55
+ /** An appointed row's role, defaulting an unset value to the least it can mean. */
56
+ export declare const appointedRoleOf: (role: string | null | undefined) => TournamentAppointedRole;
57
+ /**
58
+ * The viewer's staff role on a tournament, or null for everyone else. The
59
+ * owner wins over any row they might also hold.
60
+ */
61
+ export declare function resolveTournamentStaffRole(tournament: TournamentStaffLike | null | undefined, userId: number | string | null | undefined): TournamentStaffRole | null;
62
+ export declare const canTournamentStaff: (role: TournamentStaffRole | null | undefined, action: TournamentStaffAction) => boolean;
63
+ /** `canTournamentStaff` for a viewer, resolving their role first. */
64
+ export declare const tournamentStaffCan: (tournament: TournamentStaffLike | null | undefined, userId: number | string | null | undefined, action: TournamentStaffAction) => boolean;
65
+ /** Staff can be changed until the tournament is settled. */
66
+ export declare const STAFF_EDITABLE_STATUSES: readonly ["draft", "open", "checkIn", "seeding", "live"];
67
+ export declare const isTournamentStaffEditable: (status: string | null | undefined) => boolean;
68
+ /** One appointee as the owner's dialog sends it. */
69
+ export interface TournamentStaffMemberInput {
70
+ /** The user's documentId — what the search results carry. */
71
+ userDocumentId: string;
72
+ role: TournamentAppointedRole;
73
+ }
74
+ /** The whole appointed list; the server diffs it against the rows. */
75
+ export interface UpdateTournamentStaffBody {
76
+ data: {
77
+ staff: TournamentStaffMemberInput[];
78
+ };
79
+ }
80
+ /** `error.details.name` values the staff routes raise. */
81
+ export type TournamentStaffErrorName = 'NotTournamentStaff';
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ /**
3
+ * Tournament staff — who runs a tournament and what each of them may do.
4
+ *
5
+ * Three roles. The **owner** is the tournament's `users_permissions_user`
6
+ * and never a row; **admin** and **moderator** are `tournament-role` rows
7
+ * (the `TournamentRoleRole` enum is generated from the schema). A row with
8
+ * no role value is read as a moderator, the least the row can mean.
9
+ *
10
+ * The capability table is the contract between the server's guards and the
11
+ * client's buttons: both resolve the viewer's role with
12
+ * `resolveTournamentStaffRole` and ask `canTournamentStaff`, so a control is
13
+ * never offered for a request the server would refuse. Money and the
14
+ * irreversible lifecycle steps stay with the owner; admins run the event;
15
+ * moderators run matches.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.isTournamentStaffEditable = exports.STAFF_EDITABLE_STATUSES = exports.tournamentStaffCan = exports.canTournamentStaff = exports.appointedRoleOf = exports.tournamentStaffRoleLabel = exports.TOURNAMENT_STAFF_ROLE_LABELS = exports.TOURNAMENT_STAFF_CAPABILITIES = exports.APPOINTABLE_STAFF_ROLES = void 0;
19
+ exports.resolveTournamentStaffRole = resolveTournamentStaffRole;
20
+ exports.APPOINTABLE_STAFF_ROLES = [
21
+ 'admin',
22
+ 'moderator',
23
+ ];
24
+ const OWNER_ONLY = ['owner'];
25
+ const ADMIN_UP = ['owner', 'admin'];
26
+ const ALL_STAFF = ['owner', 'admin', 'moderator'];
27
+ exports.TOURNAMENT_STAFF_CAPABILITIES = {
28
+ manageStaff: OWNER_ONLY,
29
+ publish: OWNER_ONLY,
30
+ delete: OWNER_ONLY,
31
+ cancel: OWNER_ONLY,
32
+ distributePrizes: OWNER_ONLY,
33
+ editTournament: ADMIN_UP,
34
+ changeStatus: ADMIN_UP,
35
+ manageInvites: ADMIN_UP,
36
+ reviewJoinRequests: ADMIN_UP,
37
+ resolveDispute: ADMIN_UP,
38
+ seedBracket: ADMIN_UP,
39
+ manageMatches: ALL_STAFF,
40
+ enterLobby: ALL_STAFF,
41
+ };
42
+ exports.TOURNAMENT_STAFF_ROLE_LABELS = {
43
+ owner: 'Owner',
44
+ admin: 'Admin',
45
+ moderator: 'Moderator',
46
+ };
47
+ const tournamentStaffRoleLabel = (role) => {
48
+ var _a, _b;
49
+ return (_b = exports.TOURNAMENT_STAFF_ROLE_LABELS[(_a = role) !== null && _a !== void 0 ? _a : 'moderator']) !== null && _b !== void 0 ? _b : 'Moderator';
50
+ };
51
+ exports.tournamentStaffRoleLabel = tournamentStaffRoleLabel;
52
+ const sameId = (a, b) => a != null && b != null && String(a) === String(b);
53
+ /** An appointed row's role, defaulting an unset value to the least it can mean. */
54
+ const appointedRoleOf = (role) => (role === 'admin' ? 'admin' : 'moderator');
55
+ exports.appointedRoleOf = appointedRoleOf;
56
+ /**
57
+ * The viewer's staff role on a tournament, or null for everyone else. The
58
+ * owner wins over any row they might also hold.
59
+ */
60
+ function resolveTournamentStaffRole(tournament, userId) {
61
+ var _a, _b;
62
+ if (!tournament || userId == null)
63
+ return null;
64
+ if (sameId((_a = tournament.users_permissions_user) === null || _a === void 0 ? void 0 : _a.id, userId))
65
+ return 'owner';
66
+ const row = ((_b = tournament.tournament_roles) !== null && _b !== void 0 ? _b : []).find((r) => { var _a; return sameId((_a = r === null || r === void 0 ? void 0 : r.users_permissions_user) === null || _a === void 0 ? void 0 : _a.id, userId); });
67
+ return row ? (0, exports.appointedRoleOf)(row.role) : null;
68
+ }
69
+ const canTournamentStaff = (role, action) => role != null && exports.TOURNAMENT_STAFF_CAPABILITIES[action].includes(role);
70
+ exports.canTournamentStaff = canTournamentStaff;
71
+ /** `canTournamentStaff` for a viewer, resolving their role first. */
72
+ const tournamentStaffCan = (tournament, userId, action) => (0, exports.canTournamentStaff)(resolveTournamentStaffRole(tournament, userId), action);
73
+ exports.tournamentStaffCan = tournamentStaffCan;
74
+ /** Staff can be changed until the tournament is settled. */
75
+ exports.STAFF_EDITABLE_STATUSES = [
76
+ 'draft',
77
+ 'open',
78
+ 'checkIn',
79
+ 'seeding',
80
+ 'live',
81
+ ];
82
+ const isTournamentStaffEditable = (status) => exports.STAFF_EDITABLE_STATUSES.includes(status !== null && status !== void 0 ? status : '');
83
+ exports.isTournamentStaffEditable = isTournamentStaffEditable;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xpt-shared-types",
3
- "version": "1.9.0",
3
+ "version": "1.12.0",
4
4
  "description": "Shared types and data for XPT projects",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -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"
@@ -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
- isPrivate?: boolean;
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;
@@ -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
- isPrivate?: boolean | null;
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;
@@ -4,3 +4,6 @@
4
4
  export * from './country';
5
5
  export * from './lobby';
6
6
  export * from './tournamentEntry';
7
+ export * from './tournamentInvite';
8
+ export * from './tournamentJoinRequest';
9
+ export * from './tournamentStaff';
@@ -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';
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Tournament staff — who runs a tournament and what each of them may do.
3
+ *
4
+ * Three roles. The **owner** is the tournament's `users_permissions_user`
5
+ * and never a row; **admin** and **moderator** are `tournament-role` rows
6
+ * (the `TournamentRoleRole` enum is generated from the schema). A row with
7
+ * no role value is read as a moderator, the least the row can mean.
8
+ *
9
+ * The capability table is the contract between the server's guards and the
10
+ * client's buttons: both resolve the viewer's role with
11
+ * `resolveTournamentStaffRole` and ask `canTournamentStaff`, so a control is
12
+ * never offered for a request the server would refuse. Money and the
13
+ * irreversible lifecycle steps stay with the owner; admins run the event;
14
+ * moderators run matches.
15
+ */
16
+
17
+ import type {
18
+ TournamentCurrentStatus,
19
+ TournamentRoleRole,
20
+ } from '../generated/enums';
21
+
22
+ export type TournamentStaffRole = 'owner' | TournamentRoleRole;
23
+
24
+ /** The roles the owner can hand out — everything but their own. */
25
+ export type TournamentAppointedRole = TournamentRoleRole;
26
+
27
+ export const APPOINTABLE_STAFF_ROLES = [
28
+ 'admin',
29
+ 'moderator',
30
+ ] as const satisfies readonly TournamentAppointedRole[];
31
+
32
+ export type TournamentStaffAction =
33
+ /** Appoint, re-role or remove admins and moderators. */
34
+ | 'manageStaff'
35
+ /** `PUT /tournaments/:id` — info, rules, stages, links. */
36
+ | 'editTournament'
37
+ /** Draft → open; debits the prize pool. */
38
+ | 'publish'
39
+ /** Drafts only. */
40
+ | 'delete'
41
+ /** The forward transitions: checkIn, seeding, live, completed. */
42
+ | 'changeStatus'
43
+ /** Refunds every entrant. */
44
+ | 'cancel'
45
+ | 'distributePrizes'
46
+ | 'manageInvites'
47
+ | 'reviewJoinRequests'
48
+ | 'resolveDispute'
49
+ /** Seed, initialise the bracket, place players in slots. */
50
+ | 'seedBracket'
51
+ /** Record scores, start matches, edit match details. */
52
+ | 'manageMatches'
53
+ /** Watch any lobby and post as staff in its chat. */
54
+ | 'enterLobby';
55
+
56
+ const OWNER_ONLY = ['owner'] as const;
57
+ const ADMIN_UP = ['owner', 'admin'] as const;
58
+ const ALL_STAFF = ['owner', 'admin', 'moderator'] as const;
59
+
60
+ export const TOURNAMENT_STAFF_CAPABILITIES: Record<
61
+ TournamentStaffAction,
62
+ readonly TournamentStaffRole[]
63
+ > = {
64
+ manageStaff: OWNER_ONLY,
65
+ publish: OWNER_ONLY,
66
+ delete: OWNER_ONLY,
67
+ cancel: OWNER_ONLY,
68
+ distributePrizes: OWNER_ONLY,
69
+ editTournament: ADMIN_UP,
70
+ changeStatus: ADMIN_UP,
71
+ manageInvites: ADMIN_UP,
72
+ reviewJoinRequests: ADMIN_UP,
73
+ resolveDispute: ADMIN_UP,
74
+ seedBracket: ADMIN_UP,
75
+ manageMatches: ALL_STAFF,
76
+ enterLobby: ALL_STAFF,
77
+ };
78
+
79
+ export const TOURNAMENT_STAFF_ROLE_LABELS: Record<TournamentStaffRole, string> =
80
+ {
81
+ owner: 'Owner',
82
+ admin: 'Admin',
83
+ moderator: 'Moderator',
84
+ };
85
+
86
+ export const tournamentStaffRoleLabel = (
87
+ role: string | null | undefined
88
+ ): string =>
89
+ TOURNAMENT_STAFF_ROLE_LABELS[(role as TournamentStaffRole) ?? 'moderator'] ??
90
+ 'Moderator';
91
+
92
+ /** Only what the resolver reads; both apps' tournament shapes satisfy it. */
93
+ export interface TournamentStaffLike {
94
+ users_permissions_user?: { id?: number | string | null } | null;
95
+ tournament_roles?: Array<{
96
+ role?: string | null;
97
+ users_permissions_user?: { id?: number | string | null } | null;
98
+ }> | null;
99
+ }
100
+
101
+ const sameId = (
102
+ a: number | string | null | undefined,
103
+ b: number | string | null | undefined
104
+ ): boolean => a != null && b != null && String(a) === String(b);
105
+
106
+ /** An appointed row's role, defaulting an unset value to the least it can mean. */
107
+ export const appointedRoleOf = (
108
+ role: string | null | undefined
109
+ ): TournamentAppointedRole => (role === 'admin' ? 'admin' : 'moderator');
110
+
111
+ /**
112
+ * The viewer's staff role on a tournament, or null for everyone else. The
113
+ * owner wins over any row they might also hold.
114
+ */
115
+ export function resolveTournamentStaffRole(
116
+ tournament: TournamentStaffLike | null | undefined,
117
+ userId: number | string | null | undefined
118
+ ): TournamentStaffRole | null {
119
+ if (!tournament || userId == null) return null;
120
+ if (sameId(tournament.users_permissions_user?.id, userId)) return 'owner';
121
+ const row = (tournament.tournament_roles ?? []).find((r) =>
122
+ sameId(r?.users_permissions_user?.id, userId)
123
+ );
124
+ return row ? appointedRoleOf(row.role) : null;
125
+ }
126
+
127
+ export const canTournamentStaff = (
128
+ role: TournamentStaffRole | null | undefined,
129
+ action: TournamentStaffAction
130
+ ): boolean =>
131
+ role != null && TOURNAMENT_STAFF_CAPABILITIES[action].includes(role);
132
+
133
+ /** `canTournamentStaff` for a viewer, resolving their role first. */
134
+ export const tournamentStaffCan = (
135
+ tournament: TournamentStaffLike | null | undefined,
136
+ userId: number | string | null | undefined,
137
+ action: TournamentStaffAction
138
+ ): boolean =>
139
+ canTournamentStaff(resolveTournamentStaffRole(tournament, userId), action);
140
+
141
+ /** Staff can be changed until the tournament is settled. */
142
+ export const STAFF_EDITABLE_STATUSES = [
143
+ 'draft',
144
+ 'open',
145
+ 'checkIn',
146
+ 'seeding',
147
+ 'live',
148
+ ] as const satisfies readonly TournamentCurrentStatus[];
149
+
150
+ export const isTournamentStaffEditable = (
151
+ status: string | null | undefined
152
+ ): boolean => (STAFF_EDITABLE_STATUSES as readonly string[]).includes(status ?? '');
153
+
154
+ // ─── `PUT /tournaments/:id/staff` ────────────────────────────────────────────
155
+
156
+ /** One appointee as the owner's dialog sends it. */
157
+ export interface TournamentStaffMemberInput {
158
+ /** The user's documentId — what the search results carry. */
159
+ userDocumentId: string;
160
+ role: TournamentAppointedRole;
161
+ }
162
+
163
+ /** The whole appointed list; the server diffs it against the rows. */
164
+ export interface UpdateTournamentStaffBody {
165
+ data: { staff: TournamentStaffMemberInput[] };
166
+ }
167
+
168
+ /** `error.details.name` values the staff routes raise. */
169
+ export type TournamentStaffErrorName = 'NotTournamentStaff';