@sensiblestats/widget-react 0.46.0 → 0.47.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # @sensiblestats/widget-react
2
2
 
3
+ ## 0.47.1
4
+
5
+ ### Patch Changes
6
+
7
+ - A match whose live reading has gone stale now leaves the live band as well as losing its clock,
8
+ with the band and the competition group deciding from one shared predicate so it can never fall
9
+ between them.
10
+
11
+ ## 0.47.0
12
+
13
+ ### Minor Changes
14
+
15
+ - Shows a half-time label instead of a frozen 45', ages the live minute against when the reading was
16
+ actually taken so it no longer sits up to two minutes behind, and stops asserting a clock for a
17
+ single match whose sync window has stalled.
18
+
19
+ ### Patch Changes
20
+
21
+ - Updated dependencies
22
+ - @sensiblestats/widget-sdk@0.35.0
23
+
3
24
  ## 0.46.0
4
25
 
5
26
  ### Minor Changes
package/dist/index.cjs CHANGED
@@ -128,6 +128,7 @@ var STRINGS = {
128
128
  accordionSuspended: "Suspended",
129
129
  tabToday: "Today",
130
130
  sectionLive: "Live",
131
+ halfTime: "HT",
131
132
  staleStates: "Live scores can't be updated \u2014 showing the last known state."
132
133
  },
133
134
  tr: {
@@ -205,6 +206,7 @@ var STRINGS = {
205
206
  accordionSuspended: "Ask\u0131da",
206
207
  tabToday: "Bug\xFCn",
207
208
  sectionLive: "Canl\u0131",
209
+ halfTime: "\u0130Y",
208
210
  staleStates: "Canl\u0131 skorlar g\xFCncellenemiyor \u2014 son bilinen durum g\xF6steriliyor."
209
211
  }
210
212
  };
@@ -283,6 +285,7 @@ function normalizeUi(cfg) {
283
285
  tabOdds: strings.tabOdds,
284
286
  tabToday: strings.tabToday,
285
287
  sectionLive: strings.sectionLive,
288
+ halfTime: strings.halfTime,
286
289
  staleStates: strings.staleStates,
287
290
  pinLabel: strings.pinLabel,
288
291
  unpinLabel: strings.unpinLabel,
@@ -1542,8 +1545,43 @@ function dueMatchIds(groups, states, nowMs) {
1542
1545
  return [...due];
1543
1546
  }
1544
1547
 
1548
+ // src/liveClock.ts
1549
+ var STALE_READING_MS = 5 * 6e4;
1550
+ function parseMinute(label) {
1551
+ if (!label) return null;
1552
+ const plus = /^(\d+)\+(\d+)'$/.exec(label);
1553
+ if (plus) return Number(plus[1]) + Number(plus[2]);
1554
+ const plain = /^(\d+)'$/.exec(label);
1555
+ return plain ? Number(plain[1]) : null;
1556
+ }
1557
+ function readingAgeMs(state, nowMs) {
1558
+ if (!state?.stateAsOfUtc) return null;
1559
+ const t = Date.parse(state.stateAsOfUtc);
1560
+ return Number.isNaN(t) ? null : Math.max(0, nowMs - t);
1561
+ }
1562
+ function isReadingStale(state, nowMs) {
1563
+ if (state?.isHalfTime) return false;
1564
+ const age = readingAgeMs(state, nowMs);
1565
+ return age !== null && age > STALE_READING_MS;
1566
+ }
1567
+ function displayMinute(state, nowMs) {
1568
+ if (!state || state.phase !== "live") return null;
1569
+ if (state.isHalfTime) return null;
1570
+ if (isReadingStale(state, nowMs)) return null;
1571
+ const base = parseMinute(state.minuteLabel);
1572
+ if (base === null) return null;
1573
+ const age = readingAgeMs(state, nowMs);
1574
+ if (age === null) return state.minuteLabel ?? null;
1575
+ const advanced = base + Math.floor(age / 6e4);
1576
+ return `${advanced}'`;
1577
+ }
1578
+ function isTreatedLive(state, pollStale, nowMs) {
1579
+ return state?.phase === "live" && !pollStale && !isReadingStale(state, nowMs);
1580
+ }
1581
+
1545
1582
  // src/useMatchDiscovery.ts
1546
1583
  var POLL_MS = 6e4;
1584
+ var CLOCK_MS = 1e4;
1547
1585
  var ROLLOVER_CHECK_MS = 6e4;
1548
1586
  var STALE_AFTER_FAILURES = 2;
1549
1587
  function localDateOf(now) {
@@ -1556,6 +1594,7 @@ function useMatchDiscovery(opts) {
1556
1594
  const [groups, setGroups] = (0, import_react4.useState)([]);
1557
1595
  const [states, setStates] = (0, import_react4.useState)({});
1558
1596
  const [failures, setFailures] = (0, import_react4.useState)(0);
1597
+ const [nowMs, setNowMs] = (0, import_react4.useState)(() => Date.now());
1559
1598
  const [expanded, setExpanded] = (0, import_react4.useState)({});
1560
1599
  const [touched, setTouched] = (0, import_react4.useState)({});
1561
1600
  const groupsRef = (0, import_react4.useRef)(groups);
@@ -1633,16 +1672,21 @@ function useMatchDiscovery(opts) {
1633
1672
  }, POLL_MS);
1634
1673
  return () => clearInterval(id);
1635
1674
  }, [opts.isOpen, tick, groups]);
1675
+ (0, import_react4.useEffect)(() => {
1676
+ if (!opts.isOpen) return;
1677
+ const id = setInterval(() => setNowMs(Date.now()), CLOCK_MS);
1678
+ return () => clearInterval(id);
1679
+ }, [opts.isOpen]);
1636
1680
  const stale = failures >= STALE_AFTER_FAILURES;
1637
1681
  const liveCount = (0, import_react4.useMemo)(
1638
- () => stale ? 0 : Object.values(states).filter((s) => s.phase === "live").length,
1639
- [states, stale]
1682
+ () => stale ? 0 : Object.values(states).filter((s) => s.phase === "live" && !isReadingStale(s, nowMs)).length,
1683
+ [states, stale, nowMs]
1640
1684
  );
1641
1685
  const toggleGroup = (0, import_react4.useCallback)((competitionId) => {
1642
1686
  setTouched((prev) => ({ ...prev, [competitionId]: true }));
1643
1687
  setExpanded((prev) => ({ ...prev, [competitionId]: !prev[competitionId] }));
1644
1688
  }, []);
1645
- return { groups, states, liveCount, stale, expanded, toggleGroup, _tick: tick };
1689
+ return { groups, states, nowMs, liveCount, stale, expanded, toggleGroup, _tick: tick };
1646
1690
  }
1647
1691
 
1648
1692
  // src/eventLogContext.ts
@@ -3000,7 +3044,7 @@ function computeDisplayMinute(minute, clockRunning, receivedAtMs) {
3000
3044
  return minute + Math.min(Math.max(0, elapsedMinutes), MAX_LIVE_TICK_MINUTES);
3001
3045
  }
3002
3046
  function useLiveClock(minute, clockRunning, receivedAtMs) {
3003
- const [displayMinute, setDisplayMinute] = (0, import_react19.useState)(
3047
+ const [displayMinute2, setDisplayMinute] = (0, import_react19.useState)(
3004
3048
  () => computeDisplayMinute(minute, clockRunning, receivedAtMs)
3005
3049
  );
3006
3050
  (0, import_react19.useEffect)(() => {
@@ -3011,7 +3055,7 @@ function useLiveClock(minute, clockRunning, receivedAtMs) {
3011
3055
  }, TICK_MS);
3012
3056
  return () => window.clearInterval(id);
3013
3057
  }, [minute, clockRunning, receivedAtMs]);
3014
- return displayMinute;
3058
+ return displayMinute2;
3015
3059
  }
3016
3060
 
3017
3061
  // src/components/dashboard/LiveScoreCard.tsx
@@ -3043,7 +3087,7 @@ function LiveScoreCard({
3043
3087
  if (!seedRef.current || seedRef.current.key !== seedKey) {
3044
3088
  seedRef.current = { key: seedKey, receivedAtMs: Date.now() };
3045
3089
  }
3046
- const displayMinute = useLiveClock(data.minute, data.clockRunning, seedRef.current.receivedAtMs);
3090
+ const displayMinute2 = useLiveClock(data.minute, data.clockRunning, seedRef.current.receivedAtMs);
3047
3091
  const minuteSuffix = data.minuteLabel.replace(/^\d+/, "");
3048
3092
  const scorers = scorersLine(goalEvents);
3049
3093
  return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "ss-w-fandash-card ss-w-fandash-live-score", children: [
@@ -3053,7 +3097,7 @@ function LiveScoreCard({
3053
3097
  /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "ss-w-fandash-live-dot", "aria-hidden": "true" }),
3054
3098
  data.statusLabel
3055
3099
  ] }),
3056
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "ss-w-fandash-live-minute", children: `${displayMinute}${minuteSuffix}` }),
3100
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "ss-w-fandash-live-minute", children: `${displayMinute2}${minuteSuffix}` }),
3057
3101
  data.competition ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "ss-w-fandash-live-competition", children: data.competition }) : null
3058
3102
  ] }),
3059
3103
  /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "ss-w-fandash-live-main", children: [
@@ -3735,12 +3779,14 @@ function Side({ name, crest }) {
3735
3779
  /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("span", { className: "ss-w-drow-nm", children: name })
3736
3780
  ] });
3737
3781
  }
3738
- function FixtureRow({ match, state, stale, ui, monitor }) {
3739
- const isLive = state?.phase === "live" && !stale;
3782
+ function FixtureRow({ match, state, stale, nowMs, ui, monitor }) {
3783
+ const readingStale = isReadingStale(state, nowMs);
3784
+ const isLive = state?.phase === "live" && !stale && !readingStale;
3740
3785
  const hasScore = (state?.phase === "live" || state?.phase === "fullTime") && typeof state.homeScore === "number" && typeof state.awayScore === "number";
3741
- const when = stale && state?.phase === "live" ? "\u2014" : isLive ? state?.minuteLabel ?? "\u2014" : kickoffLabel(match.kickoffUnix);
3742
- return /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("div", { className: isLive ? "ss-w-drow ss-w-drow--live" : "ss-w-drow", children: [
3743
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("span", { className: isLive ? "ss-w-drow-when ss-w-drow-when--live" : "ss-w-drow-when", children: when }),
3786
+ const ticked = displayMinute(state, nowMs);
3787
+ const when = state?.isHalfTime && !stale ? ui.halfTime : isLive ? ticked ?? "\u2014" : state?.phase === "live" ? "\u2014" : kickoffLabel(match.kickoffUnix);
3788
+ return /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("div", { className: isLive || state?.isHalfTime ? "ss-w-drow ss-w-drow--live" : "ss-w-drow", children: [
3789
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("span", { className: isLive || state?.isHalfTime ? "ss-w-drow-when ss-w-drow-when--live" : "ss-w-drow-when", children: when }),
3744
3790
  /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("span", { className: "ss-w-drow-fx", children: [
3745
3791
  /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Side, { name: match.homeTeamName, crest: match.homeTeamImagePath }),
3746
3792
  /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("span", { className: "ss-w-drow-dash", children: "\u2013" }),
@@ -3753,8 +3799,8 @@ function FixtureRow({ match, state, stale, ui, monitor }) {
3753
3799
 
3754
3800
  // src/components/CompetitionGroup.tsx
3755
3801
  var import_jsx_runtime50 = require("react/jsx-runtime");
3756
- function CompetitionGroup({ group, open, onToggle, states, stale, ui, monitor, disambiguate = false }) {
3757
- const own = group.matches.filter((m) => states[m.matchId]?.phase !== "live" || stale);
3802
+ function CompetitionGroup({ group, open, onToggle, states, stale, nowMs, ui, monitor, disambiguate = false }) {
3803
+ const own = group.matches.filter((m) => !isTreatedLive(states[m.matchId], stale, nowMs));
3758
3804
  const hasLive = own.length !== group.matches.length;
3759
3805
  return /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("div", { className: "ss-w-grp", children: [
3760
3806
  /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("button", { type: "button", className: "ss-w-grp-head", "aria-expanded": open, onClick: onToggle, children: [
@@ -3770,15 +3816,15 @@ function CompetitionGroup({ group, open, onToggle, states, stale, ui, monitor, d
3770
3816
  ] }),
3771
3817
  /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("span", { className: "ss-w-grp-count", children: own.length })
3772
3818
  ] }),
3773
- open && /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("div", { className: "ss-w-grp-body", children: own.map((m) => /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(FixtureRow, { match: m, state: states[m.matchId], stale, ui, monitor }, m.matchId)) })
3819
+ open && /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("div", { className: "ss-w-grp-body", children: own.map((m) => /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(FixtureRow, { match: m, state: states[m.matchId], stale, nowMs, ui, monitor }, m.matchId)) })
3774
3820
  ] });
3775
3821
  }
3776
3822
 
3777
3823
  // src/components/TodayPanel.tsx
3778
3824
  var import_jsx_runtime51 = require("react/jsx-runtime");
3779
3825
  function TodayPanel({ discovery, ui, monitor }) {
3780
- const { groups, states, stale, expanded, toggleGroup } = discovery;
3781
- const liveRows = stale ? [] : groups.flatMap((group) => group.matches.filter((m) => states[m.matchId]?.phase === "live").map((match) => ({ match, rank: group.rank }))).sort((a, b) => a.rank - b.rank);
3826
+ const { groups, states, nowMs, stale, expanded, toggleGroup } = discovery;
3827
+ const liveRows = stale ? [] : groups.flatMap((group) => group.matches.filter((m) => isTreatedLive(states[m.matchId], stale, nowMs)).map((match) => ({ match, rank: group.rank }))).sort((a, b) => a.rank - b.rank);
3782
3828
  const nameCounts = groups.reduce((acc, g) => {
3783
3829
  acc[g.competitionName] = (acc[g.competitionName] ?? 0) + 1;
3784
3830
  return acc;
@@ -3796,6 +3842,7 @@ function TodayPanel({ discovery, ui, monitor }) {
3796
3842
  match,
3797
3843
  state: states[match.matchId],
3798
3844
  stale,
3845
+ nowMs,
3799
3846
  ui,
3800
3847
  monitor
3801
3848
  },
@@ -3811,6 +3858,7 @@ function TodayPanel({ discovery, ui, monitor }) {
3811
3858
  disambiguate: (nameCounts[group.competitionName] ?? 0) > 1,
3812
3859
  states,
3813
3860
  stale,
3861
+ nowMs,
3814
3862
  ui,
3815
3863
  monitor
3816
3864
  },