@sensiblestats/widget-react 0.43.0 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -279,6 +279,18 @@ function isTerminal(result) {
279
279
  if (result.status === "price_changed") return result.accepted;
280
280
  return result.status === "added" || result.status === "duplicate";
281
281
  }
282
+ function slipOutcome(result) {
283
+ if (!result) return "added";
284
+ switch (result.status) {
285
+ case "added":
286
+ case "duplicate":
287
+ return result.status;
288
+ case "price_changed":
289
+ return result.accepted ? "added" : "price_changed";
290
+ case "rejected":
291
+ return result.reason;
292
+ }
293
+ }
282
294
  function resultToToast(result, shownOdds, ui) {
283
295
  if (!result) return { kind: "success", text: ui.slipAdded };
284
296
  switch (result.status) {
@@ -300,8 +312,28 @@ import { ENTITY_TYPE_CODES } from "@sensiblestats/widget-sdk";
300
312
  function isSignalEntityType(value) {
301
313
  return Object.prototype.hasOwnProperty.call(ENTITY_TYPE_CODES, value);
302
314
  }
303
- function createDashboardTapSignal(sendSignal) {
315
+ function toWidgetEntityType(value) {
316
+ switch (value.toLowerCase()) {
317
+ case "match":
318
+ return "match";
319
+ case "team":
320
+ return "team";
321
+ case "player":
322
+ return "player";
323
+ case "competition":
324
+ return "competition";
325
+ case "market":
326
+ return "market";
327
+ default:
328
+ return null;
329
+ }
330
+ }
331
+ function createDashboardTapSignal(sendSignal, log) {
304
332
  return (tile, index, layout) => {
333
+ const widgetEntityType = toWidgetEntityType(tile.signal.entityType);
334
+ if (widgetEntityType) {
335
+ log?.log({ name: "tile_tap", entityType: widgetEntityType, entityId: tile.signal.entityId });
336
+ }
305
337
  if (!isSignalEntityType(tile.signal.entityType)) return;
306
338
  try {
307
339
  void sendSignal({
@@ -1373,6 +1405,13 @@ function useLiveMonitor(opts) {
1373
1405
  };
1374
1406
  }
1375
1407
 
1408
+ // src/eventLogContext.ts
1409
+ import { createContext, useContext } from "react";
1410
+ var EventLogContext = createContext(null);
1411
+ function useEventLog() {
1412
+ return useContext(EventLogContext);
1413
+ }
1414
+
1376
1415
  // src/components/ChatFab.tsx
1377
1416
  import { useEffect as useEffect4, useRef as useRef4, useState as useState3 } from "react";
1378
1417
 
@@ -1701,19 +1740,32 @@ function withTimeout(value) {
1701
1740
  new Promise((_, reject) => setTimeout(() => reject(new Error("add-to-slip timed out")), ADD_TO_SLIP_TIMEOUT_MS))
1702
1741
  ]);
1703
1742
  }
1704
- function SlipButton({ selection, shownOdds, marketName, slip, ui }) {
1743
+ function SlipButton({ selection, shownOdds, marketName, marketDeveloperName, slip, ui }) {
1705
1744
  const [pending, setPending] = useState6(false);
1706
1745
  const [settled, setSettled] = useState6(false);
1707
1746
  const [toast, setToast] = useState6(null);
1708
1747
  const dismiss = useCallback3(() => setToast(null), []);
1748
+ const log = useEventLog();
1709
1749
  const handler = slip.onAddToSlip;
1710
1750
  if (!ui || !handler || !selection) return null;
1711
1751
  const onClick = async () => {
1712
1752
  setPending(true);
1753
+ if (marketDeveloperName !== void 0) {
1754
+ log?.log({ name: "slip_click", entityType: "market", marketDeveloperName, entityId: marketDeveloperName });
1755
+ }
1713
1756
  try {
1714
1757
  const raw = await withTimeout(Promise.resolve(handler(selection)));
1715
1758
  if (raw != null && !isAddToSlipResult(raw)) throw new Error("malformed add-to-slip result");
1716
1759
  const result = raw ?? void 0;
1760
+ if (marketDeveloperName !== void 0) {
1761
+ log?.log({
1762
+ name: "slip_result",
1763
+ entityType: "market",
1764
+ marketDeveloperName,
1765
+ entityId: marketDeveloperName,
1766
+ outcome: slipOutcome(result)
1767
+ });
1768
+ }
1717
1769
  setToast(resultToToast(result, shownOdds || selection.oddsDecimal.toFixed(2), ui));
1718
1770
  setSettled(isTerminal(result));
1719
1771
  if (wasAdded(result) && slip.onMarketClicked) {
@@ -1723,6 +1775,9 @@ function SlipButton({ selection, shownOdds, marketName, slip, ui }) {
1723
1775
  }
1724
1776
  }
1725
1777
  } catch {
1778
+ if (marketDeveloperName !== void 0) {
1779
+ log?.log({ name: "slip_result", entityType: "market", marketDeveloperName, entityId: marketDeveloperName, outcome: "timeout" });
1780
+ }
1726
1781
  setToast({ kind: "error", text: ui.slipFailed });
1727
1782
  } finally {
1728
1783
  setPending(false);
@@ -1744,7 +1799,17 @@ function SlipButton({ selection, shownOdds, marketName, slip, ui }) {
1744
1799
  }
1745
1800
  function AddToSlipButton({ it, slip, ui }) {
1746
1801
  const selection = it.addToSlipEnabled ? toSlipSelection(it, slip.conversationId) : null;
1747
- return /* @__PURE__ */ jsx9(SlipButton, { selection, shownOdds: it.odds ?? "", marketName: it.market, slip, ui });
1802
+ return /* @__PURE__ */ jsx9(
1803
+ SlipButton,
1804
+ {
1805
+ selection,
1806
+ shownOdds: it.odds ?? "",
1807
+ marketName: it.market,
1808
+ marketDeveloperName: it.marketDeveloperName,
1809
+ slip,
1810
+ ui
1811
+ }
1812
+ );
1748
1813
  }
1749
1814
 
1750
1815
  // src/components/OddsMovementBadge.tsx
@@ -1794,13 +1859,21 @@ var PIN_GLYPHS = {
1794
1859
  function PinToggle({ pin, monitor, ui }) {
1795
1860
  const [toast, setToast] = useState8(null);
1796
1861
  const dismiss = useCallback4(() => setToast(null), []);
1862
+ const log = useEventLog();
1797
1863
  if (!monitor || !monitor.enabled) return null;
1798
1864
  const key = pinKey(pin);
1799
1865
  const pinned = monitor.isPinned(key);
1800
1866
  const labels = pin.kind === "match" ? { pin: ui.matchPinLabel, unpin: ui.matchUnpinLabel } : { pin: ui.pinLabel, unpin: ui.unpinLabel };
1801
1867
  const onClick = () => {
1802
1868
  const result = monitor.togglePin({ ...pin, key });
1803
- if (result === "match_limit") {
1869
+ if ((result === "added" || result === "removed") && (pin.kind === "match" || pin.marketDeveloperName !== void 0)) {
1870
+ log?.log({
1871
+ name: result === "added" ? "pin_add" : "pin_remove",
1872
+ entityType: pin.kind === "match" ? "match" : "market",
1873
+ entityId: pin.kind === "match" ? String(pin.matchId) : pin.marketDeveloperName,
1874
+ marketDeveloperName: pin.marketDeveloperName
1875
+ });
1876
+ } else if (result === "match_limit") {
1804
1877
  setToast({ kind: "error", text: ui.matchLimitToast });
1805
1878
  } else if (result === "market_limit") {
1806
1879
  setToast({ kind: "error", text: ui.marketLimitToast });
@@ -1857,11 +1930,19 @@ function hasStatsContent(it) {
1857
1930
  }
1858
1931
  function ReasonStrip({ it, ui }) {
1859
1932
  const [open, setOpen] = useState9(false);
1933
+ const log = useEventLog();
1860
1934
  const hasScopes = !!it.scopes && it.scopes.length >= 2;
1861
1935
  const hasStats = hasStatsContent(it);
1862
1936
  const hasText = !!it.text;
1863
1937
  if (!hasText && !hasStats) return null;
1864
1938
  const expandable = hasStats || (it.text?.length ?? 0) > 90;
1939
+ const onToggle = () => {
1940
+ setOpen((v) => {
1941
+ const next = !v;
1942
+ if (next) log?.log({ name: "insight_expand", entityType: "match", entityId: String(it.matchId) });
1943
+ return next;
1944
+ });
1945
+ };
1865
1946
  return /* @__PURE__ */ jsxs9("div", { className: "ss-w-bi-reason", children: [
1866
1947
  /* @__PURE__ */ jsx13("span", { className: "ss-w-bi-chart", "aria-hidden": "true" }),
1867
1948
  /* @__PURE__ */ jsxs9("div", { className: "ss-w-bi-reason-body", children: [
@@ -1875,7 +1956,7 @@ function ReasonStrip({ it, ui }) {
1875
1956
  className: "ss-w-bi-expand",
1876
1957
  "aria-expanded": open,
1877
1958
  "aria-label": open ? ui?.hideStats ?? "Hide stats" : ui?.showStats ?? "Show stats",
1878
- onClick: () => setOpen((v) => !v),
1959
+ onClick: onToggle,
1879
1960
  children: /* @__PURE__ */ jsx13("span", { className: `ss-w-bi-arrow${open ? " ss-w-bi-arrow-open" : ""}`, "aria-hidden": "true" })
1880
1961
  }
1881
1962
  ) : null
@@ -1981,7 +2062,7 @@ function OddsRow({ vm, row, ui, slip, inSlipIds }) {
1981
2062
  "\u2713 ",
1982
2063
  ui.inSlipLabel
1983
2064
  ] }) : null,
1984
- /* @__PURE__ */ jsx14(SlipButton, { selection, shownOdds: row.odds, marketName: vm.market, slip, ui })
2065
+ /* @__PURE__ */ jsx14(SlipButton, { selection, shownOdds: row.odds, marketName: vm.market, marketDeveloperName: vm.marketDeveloperName, slip, ui })
1985
2066
  ] });
1986
2067
  }
1987
2068
  function MarketOddsCard({ vm, ui, slip, monitor, inSlipIds, hideHeader, pinRef }) {
@@ -3167,7 +3248,7 @@ function MarketAccordion({ match, monitor, ui, slip }) {
3167
3248
  if (markets.length === 0) return null;
3168
3249
  const open = monitor.openMarket[match.matchId] ?? null;
3169
3250
  return /* @__PURE__ */ jsx46("div", { className: "ss-w-acc", children: markets.map((m) => {
3170
- const isOpen = open === m.marketDeveloperName;
3251
+ const isOpen = open === m.marketDeveloperName && !m.suspended;
3171
3252
  const key = `${match.matchId}:${m.marketDeveloperName}`;
3172
3253
  const entry = monitor.marketOdds[key];
3173
3254
  return /* @__PURE__ */ jsxs41("div", { className: isOpen ? "ss-w-acc-row ss-w-acc-row-open" : "ss-w-acc-row", children: [
@@ -3176,11 +3257,12 @@ function MarketAccordion({ match, monitor, ui, slip }) {
3176
3257
  "button",
3177
3258
  {
3178
3259
  type: "button",
3179
- className: "ss-w-acc-toggle",
3260
+ className: m.suspended ? "ss-w-acc-toggle ss-w-acc-toggle--susp" : "ss-w-acc-toggle",
3180
3261
  "aria-expanded": isOpen,
3262
+ disabled: m.suspended,
3181
3263
  onClick: () => monitor.toggleMarket(match.matchId, m.marketDeveloperName),
3182
3264
  children: [
3183
- /* @__PURE__ */ jsx46("span", { className: "ss-w-acc-chevron", children: isOpen ? "\u25BE" : "\u25B8" }),
3265
+ /* @__PURE__ */ jsx46("span", { className: "ss-w-acc-chevron", children: m.suspended ? "" : isOpen ? "\u25BE" : "\u25B8" }),
3184
3266
  /* @__PURE__ */ jsx46("span", { className: "ss-w-acc-label", children: m.marketLabel }),
3185
3267
  m.hasInsight && /* @__PURE__ */ jsx46("span", { className: "ss-w-acc-badge", children: "\u2605" }),
3186
3268
  m.suspended && /* @__PURE__ */ jsx46("span", { className: "ss-w-acc-susp", children: ui.accordionSuspended })
@@ -3312,6 +3394,7 @@ function MonitorMatchCard({ match, promotedPins, monitor, ui, slip }) {
3312
3394
  selection,
3313
3395
  shownOdds: typeof row.market.price === "number" ? row.market.price.toFixed(2) : "",
3314
3396
  marketName: row.market.marketLabel,
3397
+ marketDeveloperName: row.market.marketDeveloperName,
3315
3398
  slip,
3316
3399
  ui
3317
3400
  }
@@ -4287,6 +4370,9 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
4287
4370
  /* Row header: expand toggle (flex:1) + the market PinToggle as a sibling (no nested buttons). */
4288
4371
  .ss-w-acc-head { display: flex; align-items: center; gap: 8px; padding: 6px 9px 6px 11px; }
4289
4372
  .ss-w-acc-toggle { flex: 1; min-width: 0; display: flex; align-items: center; gap: 8px; background: none; border: none; cursor: pointer; text-align: left; font: inherit; color: var(--ss-w-ink); padding: 5px 0; }
4373
+ /* Suspended: the row stays listed (knowing a market is off is useful) but is not tappable --
4374
+ its card is unservable, so a pointer cursor would promise something that cannot load. */
4375
+ .ss-w-acc-toggle--susp { cursor: default; color: var(--ss-w-faint); }
4290
4376
  .ss-w-acc-chevron { color: var(--ss-w-faint); font-size: 11px; flex: none; }
4291
4377
  .ss-w-acc-label { font-size: 12.5px; font-weight: 600; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
4292
4378
  .ss-w-acc-badge { font-size: 10.5px; font-weight: 700; color: var(--ss-w-accent); flex: none; }
@@ -4323,6 +4409,13 @@ function StatsWidget(props) {
4323
4409
  const injectedClient = "client" in props ? props.client : void 0;
4324
4410
  const cfg = props.config;
4325
4411
  const built = useMemo2(() => buildClient(props), [injectedClient, cfg?.operatorId, cfg?.publicKey, cfg?.baseUrl]);
4412
+ const logger = useMemo2(
4413
+ () => built ? built.client.createEventLogger({
4414
+ operatorUserId: cfg?.userContext?.operatorUserId,
4415
+ anonymousUserId: cfg?.userContext?.anonymousUserId
4416
+ }) : null,
4417
+ [built?.client]
4418
+ );
4326
4419
  const ui = useMemo2(() => normalizeUi(props.config ?? { operatorId: "", publicKey: "" }), [props.config]);
4327
4420
  const rootRef = useRef8(null);
4328
4421
  const introDoneRef = useRef8(false);
@@ -4361,9 +4454,33 @@ function StatsWidget(props) {
4361
4454
  [props.config?.onAddToSlip, controller.state.conversationId, built, monitor.markInSlip]
4362
4455
  );
4363
4456
  const onDashboardTap = useMemo2(
4364
- () => built ? createDashboardTapSignal((input) => built.conversation.sendSignal(input)) : void 0,
4365
- [built]
4457
+ () => built ? createDashboardTapSignal((input) => built.conversation.sendSignal(input), logger) : void 0,
4458
+ [built, logger]
4366
4459
  );
4460
+ const operatorUserId = cfg?.userContext?.operatorUserId;
4461
+ const anonymousUserId = cfg?.userContext?.anonymousUserId;
4462
+ const conversationId = controller.state.conversationId ?? void 0;
4463
+ useEffect11(() => {
4464
+ if (!logger) return;
4465
+ logger.setIdentity({ operatorUserId, anonymousUserId, conversationId });
4466
+ }, [logger, operatorUserId, anonymousUserId, conversationId]);
4467
+ useEffect11(() => {
4468
+ if (!logger) return;
4469
+ const onHide = () => {
4470
+ logger.flushOnHide();
4471
+ };
4472
+ const onVisibilityHide = () => {
4473
+ if (document.visibilityState === "hidden") logger.flushOnHide();
4474
+ };
4475
+ window.addEventListener("pagehide", onHide);
4476
+ document.addEventListener("visibilitychange", onVisibilityHide);
4477
+ return () => {
4478
+ window.removeEventListener("pagehide", onHide);
4479
+ document.removeEventListener("visibilitychange", onVisibilityHide);
4480
+ logger.flushOnHide();
4481
+ logger.dispose();
4482
+ };
4483
+ }, [logger]);
4367
4484
  useEffect11(() => {
4368
4485
  if (!built || !ui.injectStyles) return;
4369
4486
  const node = rootRef.current?.getRootNode();
@@ -4371,7 +4488,7 @@ function StatsWidget(props) {
4371
4488
  }, [built, ui.injectStyles]);
4372
4489
  if (!built) return null;
4373
4490
  const attrs = themeAttrs(ui);
4374
- return /* @__PURE__ */ jsx50("div", { ref: rootRef, className: "ss-w-root", "data-ss-w-theme": attrs["data-ss-w-theme"], "data-ss-w-pos": attrs["data-ss-w-pos"], style: attrs.style, children: controller.state.isOpen ? /* @__PURE__ */ jsx50(ChatPanel, { controller, ui, slip, onDashboardTap, monitor: monitor.enabled ? monitor : void 0 }) : /* @__PURE__ */ jsx50(
4491
+ return /* @__PURE__ */ jsx50(EventLogContext.Provider, { value: logger, children: /* @__PURE__ */ jsx50("div", { ref: rootRef, className: "ss-w-root", "data-ss-w-theme": attrs["data-ss-w-theme"], "data-ss-w-pos": attrs["data-ss-w-pos"], style: attrs.style, children: controller.state.isOpen ? /* @__PURE__ */ jsx50(ChatPanel, { controller, ui, slip, onDashboardTap, monitor: monitor.enabled ? monitor : void 0 }) : /* @__PURE__ */ jsx50(
4375
4492
  ChatFab,
4376
4493
  {
4377
4494
  label: ui.launcherLabel,
@@ -4384,7 +4501,7 @@ function StatsWidget(props) {
4384
4501
  introAllowed: !introDoneRef.current,
4385
4502
  onIntroStart: markIntroDone
4386
4503
  }
4387
- ) });
4504
+ ) }) });
4388
4505
  }
4389
4506
  function emptyConversation() {
4390
4507
  return { send: () => {