@sensiblestats/widget-react 0.56.0 → 0.58.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/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # @sensiblestats/widget-react
2
2
 
3
+ ## 0.58.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Fix the Daily Quiz game screen not scrolling: its root `.ss-w-dq` was omitted from the games
8
+ scroll selector, so inside the `overflow:hidden` frame the lower questions and the Submit button
9
+ were clipped and unreachable. It now fills and scrolls like the other game screens.
10
+
11
+ ## 0.57.0
12
+
13
+ ### Minor Changes
14
+
15
+ - Guess the XI now reads as a real formation: the shape is named in text (e.g. "4-3-3") and a
16
+ position label (GK, CB, CM, ST…) sits under every flag, so the goalkeeper's end is unmistakable.
17
+ Wrong-guess reveals show the player's surname on the pitch instead of a raw slot code.
18
+
19
+ Games/Leaderboard tabs are now gated on the operator actually having games for the day. The games
20
+ list endpoint returns 200 with an empty list for non-games operators (only a hard 404 means the
21
+ feature is off), so the hook now enables the tabs from a non-empty list rather than defaulting them
22
+ on — which had shown the tabs on every operator. Also fixes the tabs never appearing when the
23
+ conversation initialises after first render (the one-shot fetch was dropped under StrictMode).
24
+
3
25
  ## 0.56.0
4
26
 
5
27
  ### Minor Changes
package/dist/index.cjs CHANGED
@@ -1724,18 +1724,19 @@ function computeGameDay(timezone) {
1724
1724
  return new Intl.DateTimeFormat("en-CA", { timeZone: timezone }).format(/* @__PURE__ */ new Date());
1725
1725
  }
1726
1726
  function useGames(conversation, { activeTab, operatorTimezone }) {
1727
- const [enabled, setEnabled] = (0, import_react6.useState)(true);
1727
+ const [enabled, setEnabled] = (0, import_react6.useState)(false);
1728
1728
  const [games, setGames] = (0, import_react6.useState)([]);
1729
1729
  const [loading, setLoading] = (0, import_react6.useState)(false);
1730
1730
  const [refreshCount, setRefreshCount] = (0, import_react6.useState)(0);
1731
1731
  const inFlightRef = (0, import_react6.useRef)(false);
1732
- const enabledRef = (0, import_react6.useRef)(true);
1732
+ const hardOffRef = (0, import_react6.useRef)(false);
1733
1733
  const conversationRef = (0, import_react6.useRef)(conversation);
1734
1734
  conversationRef.current = conversation;
1735
1735
  const tzRef = (0, import_react6.useRef)(operatorTimezone ?? DEFAULT_TZ);
1736
1736
  tzRef.current = operatorTimezone ?? DEFAULT_TZ;
1737
+ const ready = conversation != null;
1737
1738
  (0, import_react6.useEffect)(() => {
1738
- if (!enabledRef.current) return;
1739
+ if (hardOffRef.current) return;
1739
1740
  let active = true;
1740
1741
  const doFetch = async () => {
1741
1742
  const conv = conversationRef.current;
@@ -1747,10 +1748,12 @@ function useGames(conversation, { activeTab, operatorTimezone }) {
1747
1748
  const result = await conv.getGames(gameDay);
1748
1749
  if (!active) return;
1749
1750
  if (result === null) {
1750
- enabledRef.current = false;
1751
+ hardOffRef.current = true;
1751
1752
  setEnabled(false);
1753
+ setGames([]);
1752
1754
  } else {
1753
1755
  setGames(result.games);
1756
+ setEnabled(result.games.length > 0);
1754
1757
  }
1755
1758
  } catch {
1756
1759
  } finally {
@@ -1762,7 +1765,7 @@ function useGames(conversation, { activeTab, operatorTimezone }) {
1762
1765
  let intervalId;
1763
1766
  if (activeTab === "games") {
1764
1767
  intervalId = window.setInterval(() => {
1765
- if (!enabledRef.current) {
1768
+ if (hardOffRef.current) {
1766
1769
  window.clearInterval(intervalId);
1767
1770
  return;
1768
1771
  }
@@ -1771,9 +1774,10 @@ function useGames(conversation, { activeTab, operatorTimezone }) {
1771
1774
  }
1772
1775
  return () => {
1773
1776
  active = false;
1777
+ inFlightRef.current = false;
1774
1778
  if (intervalId !== void 0) window.clearInterval(intervalId);
1775
1779
  };
1776
- }, [activeTab, refreshCount]);
1780
+ }, [activeTab, refreshCount, ready]);
1777
1781
  const refresh = (0, import_react6.useCallback)(() => {
1778
1782
  setRefreshCount((c) => c + 1);
1779
1783
  }, []);
@@ -4970,6 +4974,47 @@ function toRows(positions) {
4970
4974
  });
4971
4975
  return [...byRow.keys()].sort((a, b) => b - a).map((k) => byRow.get(k));
4972
4976
  }
4977
+ function formationLabel(rows) {
4978
+ if (rows.length < 2) return "";
4979
+ const gkFirst = [...rows].reverse();
4980
+ return gkFirst.slice(1).map((r) => r.length).join("-");
4981
+ }
4982
+ var FORMATION_POSITIONS = {
4983
+ "4-4-2": [["GK"], ["LB", "CB", "CB", "RB"], ["LM", "CM", "CM", "RM"], ["ST", "ST"]],
4984
+ "4-3-3": [["GK"], ["LB", "CB", "CB", "RB"], ["CM", "CM", "CM"], ["LW", "ST", "RW"]],
4985
+ "4-2-3-1": [["GK"], ["LB", "CB", "CB", "RB"], ["CDM", "CDM"], ["LW", "CAM", "RW"], ["ST"]],
4986
+ "4-1-4-1": [["GK"], ["LB", "CB", "CB", "RB"], ["CDM"], ["LM", "CM", "CM", "RM"], ["ST"]],
4987
+ "4-5-1": [["GK"], ["LB", "CB", "CB", "RB"], ["LM", "CM", "CM", "CM", "RM"], ["ST"]],
4988
+ "4-4-1-1": [["GK"], ["LB", "CB", "CB", "RB"], ["LM", "CM", "CM", "RM"], ["CAM"], ["ST"]],
4989
+ "4-1-2-1-2": [["GK"], ["LB", "CB", "CB", "RB"], ["CDM"], ["LM", "RM"], ["CAM"], ["ST", "ST"]],
4990
+ "3-5-2": [["GK"], ["CB", "CB", "CB"], ["LWB", "CM", "CM", "CM", "RWB"], ["ST", "ST"]],
4991
+ "3-4-3": [["GK"], ["CB", "CB", "CB"], ["LM", "CM", "CM", "RM"], ["LW", "ST", "RW"]],
4992
+ "5-3-2": [["GK"], ["LWB", "CB", "CB", "CB", "RWB"], ["CM", "CM", "CM"], ["ST", "ST"]],
4993
+ "5-4-1": [["GK"], ["LWB", "CB", "CB", "CB", "RWB"], ["LM", "CM", "CM", "RM"], ["ST"]]
4994
+ };
4995
+ function lineFallback(lineIdx, totalLines) {
4996
+ if (lineIdx === 0) return "GK";
4997
+ if (lineIdx === totalLines - 1) return "FWD";
4998
+ if (lineIdx === 1) return "DEF";
4999
+ return "MID";
5000
+ }
5001
+ function positionsBySlot(formation, rowsAttackFirst) {
5002
+ const gkFirst = [...rowsAttackFirst].reverse();
5003
+ const template = FORMATION_POSITIONS[formation];
5004
+ const map = /* @__PURE__ */ new Map();
5005
+ gkFirst.forEach((line, li) => {
5006
+ line.forEach((entry, ci) => {
5007
+ if (!entry.slot) return;
5008
+ const label = template?.[li]?.[ci] ?? lineFallback(li, gkFirst.length);
5009
+ map.set(entry.slot, label);
5010
+ });
5011
+ });
5012
+ return map;
5013
+ }
5014
+ function surname(name) {
5015
+ const parts = name.trim().split(/\s+/).filter(Boolean);
5016
+ return parts.length ? parts[parts.length - 1] : name;
5017
+ }
4973
5018
  function GuessTheXiGame({
4974
5019
  config,
4975
5020
  result,
@@ -4983,9 +5028,11 @@ function GuessTheXiGame({
4983
5028
  const solved = clientState?.status === "solved";
4984
5029
  const guessedTeamIds = new Set(clientState?.guessHistory.map((g) => g.teamId) ?? []);
4985
5030
  const rows = config ? toRows(config.positions) : [];
4986
- const revealBySlot = /* @__PURE__ */ new Map();
5031
+ const formation = config?.hasRealFormation ? formationLabel(rows) : "";
5032
+ const posBySlot = config?.hasRealFormation ? positionsBySlot(formation, rows) : /* @__PURE__ */ new Map();
5033
+ const nameBySlot = /* @__PURE__ */ new Map();
4987
5034
  (clientState?.earnedReveals ?? []).forEach((r) => {
4988
- if (r.slot) revealBySlot.set(r.slot, r);
5035
+ if (r.slot && r.type !== "position") nameBySlot.set(r.slot, r.value);
4989
5036
  });
4990
5037
  const filtered = config && !isOver ? config.teamPool.filter(
4991
5038
  (t) => !guessedTeamIds.has(t.teamId) && t.teamName.toLowerCase().includes(query.toLowerCase())
@@ -4998,13 +5045,16 @@ function GuessTheXiGame({
4998
5045
  onSubmit(JSON.stringify({ guesses: newGuesses }));
4999
5046
  }
5000
5047
  const wrongGuesses = (clientState?.guessHistory ?? []).filter((g) => !g.wasCorrect);
5001
- const revealCount = clientState?.earnedReveals.length ?? 0;
5048
+ const revealCount = nameBySlot.size;
5002
5049
  return /* @__PURE__ */ (0, import_jsx_runtime59.jsxs)("div", { className: "ss-w-gxi", "data-testid": "game-screen-guess-the-xi", children: [
5003
5050
  !config && /* @__PURE__ */ (0, import_jsx_runtime59.jsx)("div", { className: "ss-w-gscreen-body", children: "Loading\u2026" }),
5004
5051
  config && /* @__PURE__ */ (0, import_jsx_runtime59.jsxs)(import_jsx_runtime59.Fragment, { children: [
5005
5052
  /* @__PURE__ */ (0, import_jsx_runtime59.jsxs)("div", { className: "ss-w-gxi-head", children: [
5006
5053
  /* @__PURE__ */ (0, import_jsx_runtime59.jsxs)("div", { children: [
5007
- /* @__PURE__ */ (0, import_jsx_runtime59.jsx)("div", { className: "ss-w-gxi-title", children: "Guess the XI" }),
5054
+ /* @__PURE__ */ (0, import_jsx_runtime59.jsxs)("div", { className: "ss-w-gxi-title", children: [
5055
+ "Guess the XI",
5056
+ formation && /* @__PURE__ */ (0, import_jsx_runtime59.jsx)("span", { className: "ss-w-gxi-form", "aria-label": `Formation ${formation}`, children: formation })
5057
+ ] }),
5008
5058
  /* @__PURE__ */ (0, import_jsx_runtime59.jsxs)("div", { className: "ss-w-gxi-sub", children: [
5009
5059
  "Name the team \xB7 flags are each player's nationality \xB7 ",
5010
5060
  config.poolLabel
@@ -5017,18 +5067,20 @@ function GuessTheXiGame({
5017
5067
  ] }),
5018
5068
  /* @__PURE__ */ (0, import_jsx_runtime59.jsxs)("div", { className: "ss-w-gscroll", children: [
5019
5069
  rows.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime59.jsx)("div", { className: "ss-w-gxi-pitch", children: rows.map((row, r) => /* @__PURE__ */ (0, import_jsx_runtime59.jsx)("div", { className: "ss-w-gxi-prow", children: row.map((pos, c) => {
5020
- const rev = pos.slot ? revealBySlot.get(pos.slot) : void 0;
5070
+ const posLabel = pos.slot ? posBySlot.get(pos.slot) ?? "" : "";
5071
+ const name = pos.slot ? nameBySlot.get(pos.slot) : void 0;
5021
5072
  return /* @__PURE__ */ (0, import_jsx_runtime59.jsxs)("div", { className: "ss-w-gxi-slot", children: [
5022
5073
  /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(
5023
5074
  "div",
5024
5075
  {
5025
- className: `ss-w-gxi-flag${rev ? " ss-w-gxi-flag--revealed" : ""}`,
5076
+ className: `ss-w-gxi-flag${name ? " ss-w-gxi-flag--revealed" : ""}`,
5026
5077
  title: pos.nationality,
5027
- "aria-label": rev ? `${pos.nationality} \u2014 revealed: ${rev.value}` : pos.nationality,
5078
+ "aria-label": `${posLabel ? posLabel + " \u2014 " : ""}${pos.nationality}${name ? " \u2014 " + name : ""}`,
5028
5079
  children: nationalityFlag(pos.nationality)
5029
5080
  }
5030
5081
  ),
5031
- rev && /* @__PURE__ */ (0, import_jsx_runtime59.jsx)("span", { className: "ss-w-gxi-flag-lab", children: rev.value })
5082
+ posLabel && /* @__PURE__ */ (0, import_jsx_runtime59.jsx)("span", { className: "ss-w-gxi-pos", children: posLabel }),
5083
+ name && /* @__PURE__ */ (0, import_jsx_runtime59.jsx)("span", { className: "ss-w-gxi-flag-lab", children: surname(name) })
5032
5084
  ] }, `${pos.slot}-${c}`);
5033
5085
  }) }, r)) }),
5034
5086
  revealCount > 0 && !isOver && /* @__PURE__ */ (0, import_jsx_runtime59.jsxs)("div", { className: "ss-w-gxi-revnote", children: [
@@ -6972,6 +7024,10 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
6972
7024
  .ss-w-gscreen { flex: 1; min-height: 0; display: flex; flex-direction: column; gap: 0; }
6973
7025
  .ss-w-gscreen > .ss-w-gxi, .ss-w-gscreen > .ss-w-gt, .ss-w-gscreen > .ss-w-cpath,
6974
7026
  .ss-w-gscreen > .ss-w-btg, .ss-w-gscreen > .ss-w-hl { flex: 1; min-height: 0; }
7027
+ /* Daily Quiz has no inner .ss-w-gscroll region \u2014 its whole body (score, questions, submit) is the
7028
+ scroller, so it takes the fill AND the overflow here. Without this it was clipped by the
7029
+ overflow:hidden frame and the lower questions + Submit could not be reached. */
7030
+ .ss-w-gscreen > .ss-w-dq { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
6975
7031
  /* Scrollable board + guess-history region. */
6976
7032
  .ss-w-gscroll { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; display: flex; flex-direction: column; gap: 10px; padding-bottom: 2px; }
6977
7033
  /* Bottom-pinned bar (search input / action). */
@@ -7189,7 +7245,9 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
7189
7245
  .ss-w-gxi { display: flex; flex-direction: column; gap: 12px; }
7190
7246
  /* Header */
7191
7247
  .ss-w-gxi-head { display: flex; align-items: flex-start; gap: 8px; }
7192
- .ss-w-gxi-title { font-size: 18px; font-weight: 800; text-transform: uppercase; letter-spacing: .02em; color: var(--ss-w-ink); line-height: 1; }
7248
+ .ss-w-gxi-title { font-size: 18px; font-weight: 800; text-transform: uppercase; letter-spacing: .02em; color: var(--ss-w-ink); line-height: 1; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
7249
+ /* Formation pill next to the title, e.g. "4-2-3-1". */
7250
+ .ss-w-gxi-form { font-size: 12px; font-weight: 800; letter-spacing: .06em; color: var(--ss-w-accent); background: color-mix(in srgb, var(--ss-w-accent) 12%, var(--ss-w-panel)); border: 1px solid color-mix(in srgb, var(--ss-w-accent) 26%, var(--ss-w-line)); border-radius: 999px; padding: 2px 9px; font-variant-numeric: tabular-nums; }
7193
7251
  .ss-w-gxi-sub { font-size: 11px; color: var(--ss-w-faint); margin-top: 4px; }
7194
7252
  .ss-w-gxi-left { margin-left: auto; flex: none; font-size: 11px; font-weight: 700; color: var(--ss-w-accent); background: color-mix(in srgb, var(--ss-w-accent) 11%, var(--ss-w-panel)); border: 1px solid color-mix(in srgb, var(--ss-w-accent) 22%, var(--ss-w-line)); border-radius: 999px; padding: 4px 10px; white-space: nowrap; }
7195
7253
  /* Pitch: a flag per slot laid out in formation rows on a green field. A real min-height plus
@@ -7208,6 +7266,10 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
7208
7266
  .ss-w-gxi-flag { width: 32px; height: 32px; flex: none; border-radius: 50%; display: grid; place-items: center; font-size: 17px; line-height: 1; background: rgba(9, 12, 10, .55); border: 1px solid rgba(255,255,255,.16); box-shadow: 0 1px 3px rgba(0,0,0,.3); overflow: hidden; }
7209
7267
  /* A slot whose player has been revealed by a wrong guess. */
7210
7268
  .ss-w-gxi-flag--revealed { border-color: var(--ss-w-accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--ss-w-accent) 45%, transparent); }
7269
+ /* Position label shown under every flag (GK, CB, CM, ST\u2026) \u2014 reads the formation and marks the
7270
+ keeper's end. Muted so it sits behind the flag, not competing with it. */
7271
+ .ss-w-gxi-pos { font-size: 8.5px; line-height: 1.05; font-weight: 800; letter-spacing: .04em; text-align: center; color: rgba(255,255,255,.82); text-shadow: 0 1px 2px rgba(0,0,0,.5); }
7272
+ /* Player surname, shown under the position once a wrong guess reveals it \u2014 in the accent colour. */
7211
7273
  .ss-w-gxi-flag-lab { font-size: 8px; line-height: 1.05; font-weight: 700; text-align: center; color: var(--ss-w-accent); max-width: 46px; word-break: break-word; }
7212
7274
  .ss-w-gxi-revnote { font-size: 11px; color: var(--ss-w-faint); }
7213
7275
  /* Wrong-guess trail */