@sonatel-os/strapi-plugin-tcg 0.2.0 → 0.3.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/README.md CHANGED
@@ -16,7 +16,7 @@ It was built to move **Jambaar** (the Dakar 2026 Youth Olympics card game) off i
16
16
  | **Live** | **Boosts**: multiply the odds of any rarity for a time window, season-wide or on one pack; presets like "Golden hour". Open/sold-out drops, rewards running low. | `GET /live` |
17
17
  | **Pulls** | Dashboard rarity split vs target, audit log with seed, roll, **odds snapshot**, **pity** flag and **mint number**. | `POST /pulls/scan`, `GET /pulls/history`, `GET /pulls/cooldowns` |
18
18
  | **Collection** | Per-player view with completion, rank, sets, badges, mint numbers. | `GET /collection`, `GET /collection/stats` |
19
- | **Trades** | Counts on the dashboard. | `POST /trades/offer`, `POST /trades/scan`, `…/accept`, `…/reject`, `…/cancel`, `GET /trades/active`, `GET /trades/history` |
19
+ | **Trades** | A handshake both players confirm: the initiator's offer (QR) → a receiver joins → proposes cards → the initiator confirms (atomic swap) or declines. Both screens poll `GET /trades/offer/:id` (`status`, `role`, both sides' cards). Counts on the dashboard. | `POST /trades/offer`, `POST /trades/scan`, `GET …/offer/:id`, `…/join`, `…/propose`, `…/confirm`, `…/decline`, `…/reject`, `…/cancel`, `GET /trades/active`, `GET /trades/history` |
20
20
  | **Rewards** | **Catalog** with tiers, stock, per-player limit, booth, availability window and a **cost builder**: N cards of a rarity (or at least a rarity), a specific card, a complete set (owned or handed in). | `GET /rewards`, `GET /rewards/eligibility` |
21
21
  | **Booth** | Type or scan a redemption code, see the cards and the reward, hand it over or cancel and give the cards back (stock released). | `POST /redemptions`, `GET /redemptions/:id`, `GET /redemptions/history` |
22
22
  | **Sets & badges** | **Sets** are collection goals and reward costs. **Badges** are achievements with rules (pulls, rarity pulls, packs visited, streak, completion, mint ≤ N, scanned a drop, set complete…), secret or visible; nine starters seeded per season. | `GET /sets`, `GET /badges` |
@@ -104,7 +104,7 @@ Clients branch on `code` (see `server/src/constants.js`), never on the French me
104
104
 
105
105
  Pulls are deterministic and auditable: `sha256(player:pack:pullNumber:timestamp)` seeds a Mulberry32 PRNG; the first roll picks the rarity from the season's cumulative table, the second picks a card by weight inside that rarity (falling back to the whole pool). Seed, roll, rarity and a verification hash are stored on every pull, and the Audit screen shows them. The Simulate tab runs the same engine 2 000 times against a pack without writing anything.
106
106
 
107
- QR tokens are HMAC-signed (`p.<pack>.<nonce>.<exp>.<sig>` / `t.<offer>.<player>.<nonce>.<exp>.<sig>`), expire, and are single-use: consumed nonces are written to `tcg_nonces` so replay protection works across restarts and pods. Expired nonces and trade offers are swept every minute.
107
+ QR tokens are HMAC-signed. A **static** drop code (`s.<drop>.<sig>`, the printed poster) never expires and is bounded per player by the drop's cooldown and caps. A **rotating** drop code (`d.<drop>.<nonce>.<exp>.<sig>`, the booth screen) is valid until it rotates and every player may scan it **once** — the ledger key is nonce + player, so a crowd around one screen all get their card; the cooldown is the player's, not the code's. Trade tokens burn their nonce globally. Consumed nonces live in `tcg_nonces` so replay protection works across restarts and pods; expired nonces and trade offers are swept every minute. A drop can hand out several cards per scan (`cardsPerPull`, 1–10): one scan counts once against the cooldown and the daily limits, every card is minted and recorded (`isExtra` on the extra pulls), and the response carries `cards[]` next to the first `card`.
108
108
 
109
109
  ## Permissions
110
110
 
@@ -7,9 +7,9 @@ const admin = require("@strapi/strapi/admin");
7
7
  const reactRouterDom = require("react-router-dom");
8
8
  const reactIntl = require("react-intl");
9
9
  const styledComponents = require("styled-components");
10
- const api = require("./api-Aney2S9e.js");
10
+ const api = require("./api-hTExOv_J.js");
11
11
  const react = require("react");
12
- const index = require("./index-DXuaf32J.js");
12
+ const index = require("./index-CRbzJppw.js");
13
13
  const QR = require("qrcode");
14
14
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
15
15
  const QR__default = /* @__PURE__ */ _interopDefault(QR);
@@ -3071,6 +3071,7 @@ const DropEditPage = () => {
3071
3071
  maxScansTotal: drop.maxScansTotal ?? "",
3072
3072
  maxScansPerPlayer: drop.maxScansPerPlayer ?? "",
3073
3073
  cooldownMs: drop.cooldownMs ?? "",
3074
+ cardsPerPull: drop.cardsPerPull ?? 1,
3074
3075
  sortOrder: drop.sortOrder ?? 0
3075
3076
  });
3076
3077
  if (drop && !pool)
@@ -3118,6 +3119,7 @@ const DropEditPage = () => {
3118
3119
  maxScansTotal: num(form.maxScansTotal),
3119
3120
  maxScansPerPlayer: num(form.maxScansPerPlayer),
3120
3121
  cooldownMs: num(form.cooldownMs),
3122
+ cardsPerPull: Math.min(10, Math.max(1, Number(form.cardsPerPull) || 1)),
3121
3123
  sortOrder: Number(form.sortOrder) || 0
3122
3124
  }
3123
3125
  }).unwrap();
@@ -3170,7 +3172,7 @@ const DropEditPage = () => {
3170
3172
  drop.pack?.name,
3171
3173
  drop.mode === "static" ? "printed QR" : "rotating QR",
3172
3174
  `${drop.scanCount ?? 0} scans`
3173
- ].filter(Boolean).join(" · ") + `${drop.maxScansTotal != null ? ` / ${drop.maxScansTotal}` : ""}`,
3175
+ ].filter(Boolean).join(" · ") + `${drop.maxScansTotal != null ? ` / ${drop.maxScansTotal}` : ""}${Number(drop.cardsPerPull) > 1 ? ` · ${drop.cardsPerPull} cards per scan` : ""}`,
3174
3176
  intro: {
3175
3177
  id: "drop-edit",
3176
3178
  title: "Setting up this drop",
@@ -3407,6 +3409,18 @@ const DropEditPage = () => {
3407
3409
  }
3408
3410
  )
3409
3411
  ] }) }),
3412
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 4, s: 12, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { name: "cardsPerPull", style: { width: "100%" }, children: [
3413
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Cards per scan" }),
3414
+ /* @__PURE__ */ jsxRuntime.jsx(
3415
+ designSystem.NumberInput,
3416
+ {
3417
+ value: Number(form.cardsPerPull) || 1,
3418
+ onValueChange: (v) => set("cardsPerPull", v ?? 1),
3419
+ disabled: ro
3420
+ }
3421
+ ),
3422
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, { children: "1 to 10. One scan counts once against the cooldown and the daily limits." })
3423
+ ] }) }),
3410
3424
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 12, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { name: "description", style: { width: "100%" }, children: [
3411
3425
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Description (shown to players)" }),
3412
3426
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -5,9 +5,9 @@ import { Page, useRBAC, useNotification } from "@strapi/strapi/admin";
5
5
  import { useNavigate, useParams, Routes, Route, NavLink } from "react-router-dom";
6
6
  import { useIntl } from "react-intl";
7
7
  import { styled } from "styled-components";
8
- import { u as useGetSeasonsQuery, E as Eyebrow, I as Intro, S as SPRING$1, a as useGetDashboardQuery, b as useGetLeaderboardQuery, c as useGetRewardsQuery, d as useGetSetsQuery, e as useGetBadgesQuery, f as Empty, g as Section, h as SetupSteps, i as StatTile, j as Sparkline, k as fmtNumber, R as RarityBadge, B as Bar, t as timeAgo, C as Card, P as Pill, l as StatusBadge, m as fmtDate, n as useGetSeasonDefaultsQuery, o as useGetSeasonQuery, p as useCreateSeasonMutation, q as useUpdateSeasonMutation, r as useDeleteSeasonMutation, s as errorMessage, v as useGetCardsQuery, w as useCreateCardMutation, x as useUpdateCardMutation, y as useDeleteCardMutation, z as rarityOf, A as useGetPacksQuery, D as useCreatePackMutation, F as useGetPackQuery, G as useUpdatePackMutation, H as useSetPackCardsMutation, J as useDeletePackMutation, K as useCreateDropMutation, L as useLazySimulatePackQuery, M as useGetPlayersQuery, N as fmtDateTime, O as useGetPlayerQuery, Q as useSetPlayerActiveMutation, T as useGrantCardMutation, U as useLazyGetRedemptionQuery, V as useFulfillRedemptionMutation, W as useCancelRedemptionMutation, X as useGetRedemptionsQuery, Y as Mono, Z as useGetPullsQuery, _ as useGetDropQuery, $ as useUpdateDropMutation, a0 as useSetDropCardsMutation, a1 as useDeleteDropMutation, a2 as useRotateDropSecretMutation, a3 as useDropQRMutation, a4 as useGetBoostsQuery, a5 as useCreateBoostMutation, a6 as useUpdateBoostMutation, a7 as useDeleteBoostMutation, a8 as useCreateRewardMutation, a9 as useUpdateRewardMutation, aa as useDeleteRewardMutation, ab as useCreateSetMutation, ac as useUpdateSetMutation, ad as useDeleteSetMutation, ae as useCreateBadgeMutation, af as useUpdateBadgeMutation, ag as useDeleteBadgeMutation, ah as useSeedBadgesMutation } from "./api-DACN35Y5.mjs";
8
+ import { u as useGetSeasonsQuery, E as Eyebrow, I as Intro, S as SPRING$1, a as useGetDashboardQuery, b as useGetLeaderboardQuery, c as useGetRewardsQuery, d as useGetSetsQuery, e as useGetBadgesQuery, f as Empty, g as Section, h as SetupSteps, i as StatTile, j as Sparkline, k as fmtNumber, R as RarityBadge, B as Bar, t as timeAgo, C as Card, P as Pill, l as StatusBadge, m as fmtDate, n as useGetSeasonDefaultsQuery, o as useGetSeasonQuery, p as useCreateSeasonMutation, q as useUpdateSeasonMutation, r as useDeleteSeasonMutation, s as errorMessage, v as useGetCardsQuery, w as useCreateCardMutation, x as useUpdateCardMutation, y as useDeleteCardMutation, z as rarityOf, A as useGetPacksQuery, D as useCreatePackMutation, F as useGetPackQuery, G as useUpdatePackMutation, H as useSetPackCardsMutation, J as useDeletePackMutation, K as useCreateDropMutation, L as useLazySimulatePackQuery, M as useGetPlayersQuery, N as fmtDateTime, O as useGetPlayerQuery, Q as useSetPlayerActiveMutation, T as useGrantCardMutation, U as useLazyGetRedemptionQuery, V as useFulfillRedemptionMutation, W as useCancelRedemptionMutation, X as useGetRedemptionsQuery, Y as Mono, Z as useGetPullsQuery, _ as useGetDropQuery, $ as useUpdateDropMutation, a0 as useSetDropCardsMutation, a1 as useDeleteDropMutation, a2 as useRotateDropSecretMutation, a3 as useDropQRMutation, a4 as useGetBoostsQuery, a5 as useCreateBoostMutation, a6 as useUpdateBoostMutation, a7 as useDeleteBoostMutation, a8 as useCreateRewardMutation, a9 as useUpdateRewardMutation, aa as useDeleteRewardMutation, ab as useCreateSetMutation, ac as useUpdateSetMutation, ad as useDeleteSetMutation, ae as useCreateBadgeMutation, af as useUpdateBadgeMutation, ag as useDeleteBadgeMutation, ah as useSeedBadgesMutation } from "./api-BgR-fpDp.mjs";
9
9
  import { useState, useEffect, useMemo, createContext, useContext, useRef } from "react";
10
- import { P as PLUGIN_ID, a as PERMISSIONS } from "./index-DSm3eEtn.mjs";
10
+ import { P as PLUGIN_ID, a as PERMISSIONS } from "./index-ClDgxnnH.mjs";
11
11
  import QR from "qrcode";
12
12
  const KEY = "tcg.season";
13
13
  const SeasonContext = createContext({
@@ -3067,6 +3067,7 @@ const DropEditPage = () => {
3067
3067
  maxScansTotal: drop.maxScansTotal ?? "",
3068
3068
  maxScansPerPlayer: drop.maxScansPerPlayer ?? "",
3069
3069
  cooldownMs: drop.cooldownMs ?? "",
3070
+ cardsPerPull: drop.cardsPerPull ?? 1,
3070
3071
  sortOrder: drop.sortOrder ?? 0
3071
3072
  });
3072
3073
  if (drop && !pool)
@@ -3114,6 +3115,7 @@ const DropEditPage = () => {
3114
3115
  maxScansTotal: num(form.maxScansTotal),
3115
3116
  maxScansPerPlayer: num(form.maxScansPerPlayer),
3116
3117
  cooldownMs: num(form.cooldownMs),
3118
+ cardsPerPull: Math.min(10, Math.max(1, Number(form.cardsPerPull) || 1)),
3117
3119
  sortOrder: Number(form.sortOrder) || 0
3118
3120
  }
3119
3121
  }).unwrap();
@@ -3166,7 +3168,7 @@ const DropEditPage = () => {
3166
3168
  drop.pack?.name,
3167
3169
  drop.mode === "static" ? "printed QR" : "rotating QR",
3168
3170
  `${drop.scanCount ?? 0} scans`
3169
- ].filter(Boolean).join(" · ") + `${drop.maxScansTotal != null ? ` / ${drop.maxScansTotal}` : ""}`,
3171
+ ].filter(Boolean).join(" · ") + `${drop.maxScansTotal != null ? ` / ${drop.maxScansTotal}` : ""}${Number(drop.cardsPerPull) > 1 ? ` · ${drop.cardsPerPull} cards per scan` : ""}`,
3170
3172
  intro: {
3171
3173
  id: "drop-edit",
3172
3174
  title: "Setting up this drop",
@@ -3403,6 +3405,18 @@ const DropEditPage = () => {
3403
3405
  }
3404
3406
  )
3405
3407
  ] }) }),
3408
+ /* @__PURE__ */ jsx(Grid.Item, { col: 4, s: 12, children: /* @__PURE__ */ jsxs(Field.Root, { name: "cardsPerPull", style: { width: "100%" }, children: [
3409
+ /* @__PURE__ */ jsx(Field.Label, { children: "Cards per scan" }),
3410
+ /* @__PURE__ */ jsx(
3411
+ NumberInput,
3412
+ {
3413
+ value: Number(form.cardsPerPull) || 1,
3414
+ onValueChange: (v) => set("cardsPerPull", v ?? 1),
3415
+ disabled: ro
3416
+ }
3417
+ ),
3418
+ /* @__PURE__ */ jsx(Field.Hint, { children: "1 to 10. One scan counts once against the cooldown and the daily limits." })
3419
+ ] }) }),
3406
3420
  /* @__PURE__ */ jsx(Grid.Item, { col: 12, children: /* @__PURE__ */ jsxs(Field.Root, { name: "description", style: { width: "100%" }, children: [
3407
3421
  /* @__PURE__ */ jsx(Field.Label, { children: "Description (shown to players)" }),
3408
3422
  /* @__PURE__ */ jsx(
@@ -3,8 +3,8 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const jsxRuntime = require("react/jsx-runtime");
4
4
  const designSystem = require("@strapi/design-system");
5
5
  const admin = require("@strapi/strapi/admin");
6
- const api = require("./api-Aney2S9e.js");
7
- const index = require("./index-DXuaf32J.js");
6
+ const api = require("./api-hTExOv_J.js");
7
+ const index = require("./index-CRbzJppw.js");
8
8
  const Row = ({ k, v, hint, tone }) => /* @__PURE__ */ jsxRuntime.jsxs(
9
9
  designSystem.Flex,
10
10
  {
@@ -1,8 +1,8 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { Grid, Box, Flex, Typography, Badge } from "@strapi/design-system";
3
3
  import { Page, Layouts } from "@strapi/strapi/admin";
4
- import { ai as useGetSettingsQuery, I as Intro, g as Section, Y as Mono } from "./api-DACN35Y5.mjs";
5
- import { a as PERMISSIONS } from "./index-DSm3eEtn.mjs";
4
+ import { ai as useGetSettingsQuery, I as Intro, g as Section, Y as Mono } from "./api-BgR-fpDp.mjs";
5
+ import { a as PERMISSIONS } from "./index-ClDgxnnH.mjs";
6
6
  const Row = ({ k, v, hint, tone }) => /* @__PURE__ */ jsxs(
7
7
  Flex,
8
8
  {
@@ -6,7 +6,7 @@ import { EmptyDocuments } from "@strapi/icons/symbols";
6
6
  import { useNavigate } from "react-router-dom";
7
7
  import { styled, css, keyframes } from "styled-components";
8
8
  import { adminApi } from "@strapi/strapi/admin";
9
- import { P as PLUGIN_ID } from "./index-DSm3eEtn.mjs";
9
+ import { P as PLUGIN_ID } from "./index-ClDgxnnH.mjs";
10
10
  const fmtDateTime = (value, locale) => {
11
11
  if (!value) return "—";
12
12
  try {
@@ -7,7 +7,7 @@ const symbols = require("@strapi/icons/symbols");
7
7
  const reactRouterDom = require("react-router-dom");
8
8
  const styledComponents = require("styled-components");
9
9
  const admin = require("@strapi/strapi/admin");
10
- const index = require("./index-DXuaf32J.js");
10
+ const index = require("./index-CRbzJppw.js");
11
11
  const fmtDateTime = (value, locale) => {
12
12
  if (!value) return "—";
13
13
  try {
@@ -63,7 +63,7 @@ const index = {
63
63
  intlLabel: { id: `${PLUGIN_ID}.menu.label`, defaultMessage: "TCG" },
64
64
  permissions: PERMISSIONS.access,
65
65
  Component: async () => {
66
- const { App } = await Promise.resolve().then(() => require("./App-DvvgeCGs.js"));
66
+ const { App } = await Promise.resolve().then(() => require("./App-B-WZGNkc.js"));
67
67
  return App;
68
68
  }
69
69
  });
@@ -76,7 +76,7 @@ const index = {
76
76
  intlLabel: { id: `${PLUGIN_ID}.settings.link`, defaultMessage: "Configuration" },
77
77
  permissions: PERMISSIONS.settingsRead,
78
78
  Component: async () => {
79
- const { SettingsPage } = await Promise.resolve().then(() => require("./SettingsPage-Bxa_71iT.js"));
79
+ const { SettingsPage } = await Promise.resolve().then(() => require("./SettingsPage-DNWXuY8j.js"));
80
80
  return SettingsPage;
81
81
  }
82
82
  }
@@ -62,7 +62,7 @@ const index = {
62
62
  intlLabel: { id: `${PLUGIN_ID}.menu.label`, defaultMessage: "TCG" },
63
63
  permissions: PERMISSIONS.access,
64
64
  Component: async () => {
65
- const { App } = await import("./App-j9qu2-To.mjs");
65
+ const { App } = await import("./App-CS_cVYEI.mjs");
66
66
  return App;
67
67
  }
68
68
  });
@@ -75,7 +75,7 @@ const index = {
75
75
  intlLabel: { id: `${PLUGIN_ID}.settings.link`, defaultMessage: "Configuration" },
76
76
  permissions: PERMISSIONS.settingsRead,
77
77
  Component: async () => {
78
- const { SettingsPage } = await import("./SettingsPage-BcJXkWnW.mjs");
78
+ const { SettingsPage } = await import("./SettingsPage-fGiEcX6l.mjs");
79
79
  return SettingsPage;
80
80
  }
81
81
  }
@@ -1,3 +1,3 @@
1
1
  "use strict";
2
- const index = require("../_chunks/index-DXuaf32J.js");
2
+ const index = require("../_chunks/index-CRbzJppw.js");
3
3
  module.exports = index.index;
@@ -1,4 +1,4 @@
1
- import { i } from "../_chunks/index-DSm3eEtn.mjs";
1
+ import { i } from "../_chunks/index-ClDgxnnH.mjs";
2
2
  export {
3
3
  i as default
4
4
  };