@stamprally/ui 0.7.0 → 0.9.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.cjs CHANGED
@@ -5,448 +5,157 @@ var react = require('react');
5
5
  var jsxRuntime = require('react/jsx-runtime');
6
6
 
7
7
  // src/index.tsx
8
- function RallyViewer({
9
- config,
8
+ var label = (dictionary, locale, key, fallback) => dictionary?.[locale]?.[key] ?? fallback;
9
+ function DefaultCondition({
10
+ condition,
11
+ dictionary,
10
12
  locale,
11
- onSpotSelect
13
+ disabled,
14
+ onSubmit
12
15
  }) {
13
- const activeLocale = locale ?? "en";
14
- return /* @__PURE__ */ jsxRuntime.jsxs("section", { "aria-label": "Stamp rally viewer", className: "stamprally-viewer", children: [
15
- /* @__PURE__ */ jsxRuntime.jsxs("header", { children: [
16
- /* @__PURE__ */ jsxRuntime.jsx("h1", { children: core.resolveLocalizedText(config.title, activeLocale) }),
17
- config.description !== void 0 && /* @__PURE__ */ jsxRuntime.jsx("p", { children: core.resolveLocalizedText(config.description, activeLocale) })
16
+ const [value, setValue] = react.useState("");
17
+ const submit = (event) => {
18
+ event.preventDefault();
19
+ onSubmit(
20
+ condition.type === "gps" ? { latitude: Number(value.split(",")[0]), longitude: Number(value.split(",")[1]) } : condition.type === "nfc" ? { tagId: value } : value
21
+ );
22
+ };
23
+ return /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: submit, children: [
24
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
25
+ label(dictionary, locale, "proof", "Proof"),
26
+ /* @__PURE__ */ jsxRuntime.jsx(
27
+ "input",
28
+ {
29
+ "aria-label": label(dictionary, locale, "proof", "Proof"),
30
+ value,
31
+ onChange: (event) => setValue(event.target.value)
32
+ }
33
+ )
18
34
  ] }),
19
- /* @__PURE__ */ jsxRuntime.jsx("ol", { children: config.spots.map((spot) => /* @__PURE__ */ jsxRuntime.jsxs("li", { children: [
20
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => onSpotSelect?.(spot.id), children: core.resolveLocalizedText(spot.name, activeLocale) }),
21
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: spot.conditions[0]?.type ?? "custom" })
22
- ] }, spot.id)) })
35
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", disabled, children: condition.type === "gps" ? label(dictionary, locale, "checkLocation", "Check location") : label(dictionary, locale, "checkIn", "Check in") })
23
36
  ] });
24
37
  }
25
- function useFocusTrap(active, onClose, closeDisabled = false) {
26
- const containerRef = react.useRef(null);
38
+ function RallyViewer({
39
+ config: providedConfig,
40
+ client,
41
+ adapter,
42
+ locale,
43
+ dictionary,
44
+ customConditionRenderers = {}
45
+ }) {
46
+ const config = providedConfig ?? client?.getConfig() ?? adapter?.config;
47
+ const [state, setState] = react.useState(() => client?.getState() ?? null);
48
+ const [busy, setBusy] = react.useState(null);
27
49
  react.useEffect(() => {
28
- if (!active) return;
29
- const container = containerRef.current;
30
- if (container === null) return;
31
- const focusableSelector = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
32
- const focusable = () => Array.from(container.querySelectorAll(focusableSelector));
33
- const first = focusable()[0];
34
- first?.focus();
35
- const handleKeyDown = (event) => {
36
- if (event.key === "Escape") {
37
- if (!closeDisabled) {
38
- event.preventDefault();
39
- onClose?.();
40
- }
41
- return;
42
- }
43
- if (event.key !== "Tab") return;
44
- const elements = focusable();
45
- if (elements.length === 0) {
46
- event.preventDefault();
47
- container.focus();
48
- return;
49
- }
50
- const firstElement = elements[0];
51
- const lastElement = elements[elements.length - 1];
52
- if (event.shiftKey && document.activeElement === firstElement) {
53
- event.preventDefault();
54
- lastElement?.focus();
55
- } else if (!event.shiftKey && document.activeElement === lastElement) {
56
- event.preventDefault();
57
- firstElement?.focus();
50
+ if (client === void 0) return;
51
+ const unsubscribe = client.subscribe(setState);
52
+ void client.init().then(setState);
53
+ return unsubscribe;
54
+ }, [client]);
55
+ if (config === void 0) throw new Error("RallyViewer requires config, client, or adapter.");
56
+ const progress = react.useMemo(
57
+ () => core.calculateProgress(
58
+ state ?? { rallyId: config.id, userId: null, records: [], rewards: [], updatedAt: "" },
59
+ config
60
+ ),
61
+ [config, state]
62
+ );
63
+ const checkIn = adapter?.onCheckIn ?? (client === void 0 ? void 0 : (spotId, proof, options) => client.checkIn(spotId, proof, options));
64
+ const claim = adapter?.onClaimReward ?? (client === void 0 ? void 0 : (rewardId, options) => client.claimReward(rewardId, options));
65
+ const submit = react.useCallback(
66
+ (spotId, proof) => {
67
+ if (checkIn === void 0) return;
68
+ setBusy(spotId);
69
+ void checkIn(spotId, proof).finally(() => setBusy(null));
70
+ },
71
+ [checkIn]
72
+ );
73
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { "aria-label": label(dictionary, locale, "viewer", "Stamp rally"), children: [
74
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { children: core.resolveLocalizedText(config.title, locale) || config.id }),
75
+ /* @__PURE__ */ jsxRuntime.jsx(
76
+ "progress",
77
+ {
78
+ "aria-label": label(dictionary, locale, "progress", "Progress"),
79
+ max: 100,
80
+ value: progress.percentage
58
81
  }
59
- };
60
- document.addEventListener("keydown", handleKeyDown);
61
- return () => document.removeEventListener("keydown", handleKeyDown);
62
- }, [active, closeDisabled, onClose]);
63
- return containerRef;
64
- }
65
- var FONT_STACKS = {
66
- "system-ui": 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
67
- serif: 'Georgia, "Times New Roman", "Yu Mincho", serif',
68
- "rounded-sans": '"Arial Rounded MT Bold", "Hiragino Maru Gothic ProN", "Yu Gothic", sans-serif',
69
- monospace: 'ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace',
70
- handwritten: '"Segoe Print", "Bradley Hand", "Comic Sans MS", cursive'
71
- };
72
- function formatStampDate(value) {
73
- const date = new Date(value);
74
- if (Number.isNaN(date.getTime())) return value;
75
- const pad = (part) => String(part).padStart(2, "0");
76
- return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
82
+ ),
83
+ " ",
84
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
85
+ progress.acquired,
86
+ "/",
87
+ progress.total
88
+ ] }),
89
+ /* @__PURE__ */ jsxRuntime.jsx("div", { children: config.spots.map(
90
+ (spot) => spot.conditions.map((condition) => {
91
+ const Renderer = customConditionRenderers[condition.type] ?? DefaultCondition;
92
+ return /* @__PURE__ */ jsxRuntime.jsxs("article", { children: [
93
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { children: core.resolveLocalizedText(spot.name, locale) }),
94
+ /* @__PURE__ */ jsxRuntime.jsx(
95
+ Renderer,
96
+ {
97
+ spot,
98
+ condition,
99
+ locale,
100
+ ...dictionary === void 0 ? {} : { dictionary },
101
+ disabled: busy !== null,
102
+ onSubmit: (proof) => submit(spot.id, proof)
103
+ }
104
+ )
105
+ ] }, `${spot.id}-${condition.type}-${JSON.stringify(condition)}`);
106
+ })
107
+ ) }),
108
+ /* @__PURE__ */ jsxRuntime.jsx("section", { "aria-label": label(dictionary, locale, "rewards", "Rewards"), children: config.rewards.map((reward) => /* @__PURE__ */ jsxRuntime.jsx(
109
+ RewardButton,
110
+ {
111
+ reward,
112
+ state: state?.rewards.find((item) => item.rewardId === reward.id),
113
+ onClaim: claim
114
+ },
115
+ reward.id
116
+ )) })
117
+ ] });
77
118
  }
78
- function StampSlot({
79
- stamp,
80
- record,
81
- isNext = false,
82
- slotNumber = 1,
83
- presentation = {},
84
- isAnimating = false,
85
- disabled = false,
86
- slotShape = "rounded",
87
- locale = "ja",
88
- statusText,
89
- onSelect
119
+ function RewardButton({
120
+ reward,
121
+ state,
122
+ onClaim
90
123
  }) {
91
- const status = record === void 0 ? isNext ? "available" : "locked" : "stamped";
92
- const resolvedStatus = statusText ?? status;
93
- const statusSeparator = statusText !== void 0 && locale === "ja" ? "\u3001" : ", ";
94
- const name = core.resolveLocalizedText(stamp.name, locale);
95
- const label = presentation.label ?? stamp.condition.type;
96
124
  return /* @__PURE__ */ jsxRuntime.jsxs(
97
125
  "button",
98
126
  {
99
127
  type: "button",
100
- disabled,
101
- onClick: onSelect,
102
- "aria-label": `#${String(slotNumber).padStart(2, "0")} ${name}${statusSeparator}${resolvedStatus}`,
103
- "aria-disabled": disabled,
104
- "data-status": status,
105
- "data-shape": slotShape,
106
- className: `stamp-slot stamp-slot--${status} stamp-slot--${presentation.ink ?? "vermilion"} stamp-slot--shape-${slotShape}${isAnimating ? " stamp-slot--animating" : ""}`,
107
- style: { "--stamp-primary": "var(--stamprally-primary, #9e551e)" },
128
+ disabled: state?.status !== "AVAILABLE" || onClaim === void 0,
129
+ onClick: () => {
130
+ if (onClaim !== void 0) void onClaim(reward.id);
131
+ },
108
132
  children: [
109
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "stamp-slot__topline", children: [
110
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "stamp-slot__number", children: [
111
- "#",
112
- String(slotNumber).padStart(2, "0")
113
- ] }),
114
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: `stamp-slot__state stamp-slot__state--${status}`, children: resolvedStatus })
115
- ] }),
116
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "stamp-slot__watermark", "aria-hidden": "true", children: presentation.icon ?? "\u2726" }),
117
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "stamp-slot__name", children: name }),
118
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "stamp-slot__condition", children: [
119
- /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: presentation.icon ?? "\u2726" }),
120
- label
121
- ] }),
122
- record !== void 0 && /* @__PURE__ */ jsxRuntime.jsx(
123
- "span",
124
- {
125
- className: `stamp-imprint ${isAnimating ? "stamp-press" : ""}`,
126
- role: "img",
127
- "aria-label": `Acquired at ${formatStampDate(record.acquiredAt)}`,
128
- children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "stamp-imprint__inner", children: [
129
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "stamp-imprint__word", children: "STAMP" }),
130
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "stamp-imprint__date", children: formatStampDate(record.acquiredAt) })
131
- ] })
132
- }
133
- )
133
+ core.resolveLocalizedText(reward.title, "en"),
134
+ " (",
135
+ state?.status ?? "LOCKED",
136
+ ")"
134
137
  ]
135
138
  }
136
139
  );
137
140
  }
138
141
  function StampSheet({
139
- title,
140
142
  config,
141
- state = null,
142
- progress,
143
- presentations = {},
144
- animatedStampId = null,
145
- disabled = false,
146
- locale = "ja",
147
- dictionary,
148
- theme = config.theme ?? core.DEFAULT_SHEET_THEME,
149
- onStampSelect
150
- }) {
151
- const acquired = new Set(state?.records.map((record) => record.stampId) ?? []);
152
- const next = new Set(progress?.nextAvailableStamps.map((stamp) => stamp.id) ?? []);
153
- const count = progress?.acquired ?? acquired.size;
154
- const total = progress?.total ?? config.stamps.length;
155
- const percentage = progress?.percentage ?? (total === 0 ? 0 : count / total * 100);
156
- const style = {
157
- "--stamp-primary": `var(--stamprally-primary-override, ${theme.primaryColor})`,
158
- "--stamp-bg": `var(--stamprally-background-override, ${theme.backgroundColor ?? core.DEFAULT_SHEET_THEME.backgroundColor ?? "#ffffff"})`,
159
- "--stamp-card-bg": `var(--stamprally-card-override, ${theme.cardBackgroundColor})`,
160
- "--stamp-text": `var(--stamprally-text-override, ${theme.textColor})`,
161
- "--stamp-grid-cols": String(theme.gridColumns),
162
- "--stamp-grid-cols-mobile": String(Math.min(theme.gridColumns, 2)),
163
- "--stamp-unclaimed-opacity": String(theme.unclaimedOpacity ?? 1),
164
- "--stamp-font-family": FONT_STACKS[theme.fontFamily ?? "serif"],
165
- ...theme.completedStampColor === void 0 ? {} : { "--stamp-completed-color": theme.completedStampColor },
166
- "--stamprally-primary": `var(--stamprally-primary-override, ${theme.primaryColor})`,
167
- "--stamprally-background": `var(--stamprally-background-override, ${theme.backgroundColor ?? "transparent"})`,
168
- "--stamprally-card": `var(--stamprally-card-override, ${theme.cardBackgroundColor})`,
169
- "--stamprally-text": `var(--stamprally-text-override, ${theme.textColor})`,
170
- "--stamprally-columns": String(theme.gridColumns)
171
- };
172
- const messages = {
173
- available: dictionary?.[locale]?.available ?? (locale === "en" ? "available" : "\u53D6\u5F97\u53EF\u80FD"),
174
- locked: dictionary?.[locale]?.locked ?? (locale === "en" ? "locked" : "\u9806\u5E8F\u5F85\u3061"),
175
- stamped: dictionary?.[locale]?.stamped ?? (locale === "en" ? "stamped" : "\u53D6\u5F97\u6E08\u307F"),
176
- remaining: (value) => dictionary?.[locale]?.remaining?.replace("{count}", String(value)) ?? (locale === "en" ? `${value} spots remaining` : `\u3042\u3068${value}\u7B87\u6240`),
177
- completed: dictionary?.[locale]?.completed ?? (locale === "en" ? "RALLY COMPLETED" : "\u30E9\u30EA\u30FC\u9054\u6210")
178
- };
179
- return /* @__PURE__ */ jsxRuntime.jsxs(
180
- "section",
181
- {
182
- className: `stamp-sheet ${percentage >= 100 ? "stamp-sheet--completed" : ""}`,
183
- style,
184
- "aria-label": core.resolveLocalizedText(title, locale) || config.id,
185
- children: [
186
- /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "stamp-sheet__header", children: [
187
- /* @__PURE__ */ jsxRuntime.jsx("h2", { children: core.resolveLocalizedText(title, locale) || config.id }),
188
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "stamp-sheet__score", role: "status", "aria-live": "polite", children: [
189
- count,
190
- " / ",
191
- total
192
- ] })
193
- ] }),
194
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "stamp-sheet__progress-label", children: [
195
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: messages.remaining(Math.max(0, total - count)) }),
196
- /* @__PURE__ */ jsxRuntime.jsxs("strong", { children: [
197
- Math.round(percentage),
198
- "%"
199
- ] })
200
- ] }),
201
- /* @__PURE__ */ jsxRuntime.jsx(
202
- "div",
203
- {
204
- className: "stamp-sheet__progress-track",
205
- role: "progressbar",
206
- "aria-valuemin": 0,
207
- "aria-valuemax": 100,
208
- "aria-valuenow": Math.round(percentage),
209
- "aria-label": locale === "en" ? "Rally progress" : "\u30B9\u30BF\u30F3\u30D7\u53D6\u5F97\u9032\u6357",
210
- children: /* @__PURE__ */ jsxRuntime.jsx(
211
- "span",
212
- {
213
- className: "stamp-sheet__progress-fill",
214
- style: { display: "block", width: `${percentage}%` }
215
- }
216
- )
217
- }
218
- ),
219
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "stamp-sheet__grid", children: config.stamps.map((stamp, index) => {
220
- const recordForStamp = state?.records.find((record) => record.stampId === stamp.id);
221
- return /* @__PURE__ */ jsxRuntime.jsx(
222
- StampSlot,
223
- {
224
- stamp,
225
- ...recordForStamp === void 0 ? {} : { record: recordForStamp },
226
- isNext: next.has(stamp.id),
227
- slotNumber: index + 1,
228
- ...presentations[stamp.id] === void 0 ? {} : { presentation: presentations[stamp.id] },
229
- isAnimating: animatedStampId === stamp.id,
230
- disabled,
231
- slotShape: theme.slotShape,
232
- locale,
233
- ...dictionary === void 0 ? {} : { dictionary },
234
- statusText: recordForStamp === void 0 ? next.has(stamp.id) ? messages.available : messages.locked : messages.stamped,
235
- onSelect: () => onStampSelect?.(stamp.id)
236
- },
237
- stamp.id
238
- );
239
- }) }),
240
- percentage >= 100 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "stamp-sheet__complete-mark", role: "status", "aria-live": "polite", children: "COMPLETE!!" }),
241
- percentage >= 100 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "stamp-sheet__complete-message", children: messages.completed })
242
- ]
243
- }
244
- );
245
- }
246
- function StampModal({
247
- open,
248
- stamp,
249
- record,
250
- isAvailable = true,
251
- isPending = false,
252
- locale = "ja",
253
- dictionary,
254
- onClose,
255
- onAcquire,
256
- onCheckIn,
257
- onNotify
258
- }) {
259
- const [token, setToken] = react.useState("");
260
- const modalRef = useFocusTrap(open && stamp !== null, onClose, isPending);
261
- react.useEffect(() => {
262
- if (!open) setToken("");
263
- }, [open]);
264
- if (!open || stamp === null) return null;
265
- const submit = async (context) => {
266
- if (!isAvailable || record !== void 0) return;
267
- const handler = onAcquire ?? onCheckIn;
268
- if (handler === void 0) return;
269
- const feedback = await handler(stamp.id, context);
270
- onNotify?.(feedback);
271
- if (feedback.ok) onClose();
272
- };
273
- const name = core.resolveLocalizedText(stamp.name, locale);
274
- const messages = dictionary?.[locale] ?? {};
275
- return /* @__PURE__ */ jsxRuntime.jsx(
276
- "div",
277
- {
278
- role: "dialog",
279
- "aria-modal": "true",
280
- "aria-labelledby": "stamprally-modal-title",
281
- className: "stamprally-modal",
282
- children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "stamprally-modal__panel", ref: modalRef, tabIndex: -1, children: [
283
- /* @__PURE__ */ jsxRuntime.jsx(
284
- "button",
285
- {
286
- type: "button",
287
- "aria-label": "Close stamp details",
288
- "aria-disabled": isPending,
289
- onClick: onClose,
290
- disabled: isPending,
291
- children: "\xD7"
292
- }
293
- ),
294
- /* @__PURE__ */ jsxRuntime.jsx("h2", { id: "stamprally-modal-title", children: name }),
295
- record !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { children: messages.alreadyClaimed ?? "Already claimed." }) : /* @__PURE__ */ jsxRuntime.jsx("p", { children: core.resolveLocalizedText(stamp.description, locale) }),
296
- stamp.condition.type === "token" ? /* @__PURE__ */ jsxRuntime.jsxs(
297
- "form",
298
- {
299
- onSubmit: (event) => {
300
- event.preventDefault();
301
- void submit({ type: "token", token });
302
- },
303
- children: [
304
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
305
- messages.passcode ?? "Passcode",
306
- " ",
307
- /* @__PURE__ */ jsxRuntime.jsx("input", { value: token, onChange: (event) => setToken(event.target.value) })
308
- ] }),
309
- /* @__PURE__ */ jsxRuntime.jsx(
310
- "button",
311
- {
312
- type: "submit",
313
- "aria-label": "Claim stamp",
314
- "aria-disabled": !isAvailable || isPending,
315
- disabled: !isAvailable || isPending,
316
- children: messages.claimStamp ?? "Claim stamp"
317
- }
318
- )
319
- ]
320
- }
321
- ) : /* @__PURE__ */ jsxRuntime.jsx(
322
- "button",
323
- {
324
- type: "button",
325
- "aria-label": "Claim stamp",
326
- "aria-disabled": !isAvailable || isPending,
327
- onClick: () => void submit({ type: "instant" }),
328
- disabled: !isAvailable || isPending,
329
- children: messages.claimStamp ?? "Claim stamp"
330
- }
331
- )
332
- ] })
333
- }
334
- );
335
- }
336
- function RewardPanel({
337
- rewards,
338
- states,
339
- locale = "ja",
340
- dictionary,
341
- isPending = false,
342
- open = true,
343
- onClose,
344
- onRedeem,
345
- onNotify
143
+ state,
144
+ title,
145
+ progress
346
146
  }) {
347
- const [passcodes, setPasscodes] = react.useState({});
348
- const panelRef = useFocusTrap(open && onClose !== void 0, onClose, isPending);
349
- if (!open) return null;
350
- return /* @__PURE__ */ jsxRuntime.jsxs(
351
- "section",
352
- {
353
- ref: panelRef,
354
- role: "dialog",
355
- "aria-labelledby": "stamprally-rewards-title",
356
- "aria-modal": "true",
357
- className: "stamprally-rewards",
358
- tabIndex: -1,
359
- children: [
360
- /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "stamprally-rewards__header", children: [
361
- /* @__PURE__ */ jsxRuntime.jsx("h2", { id: "stamprally-rewards-title", children: dictionary?.[locale]?.rewards ?? "Rewards" }),
362
- onClose !== void 0 && /* @__PURE__ */ jsxRuntime.jsx(
363
- "button",
364
- {
365
- type: "button",
366
- "aria-label": "Close rewards",
367
- "aria-disabled": isPending,
368
- disabled: isPending,
369
- onClick: onClose,
370
- children: "\xD7"
371
- }
372
- )
373
- ] }),
374
- rewards.map((reward) => {
375
- const state = states.find((item) => item.rewardId === reward.id);
376
- const available = state?.status === "AVAILABLE";
377
- const redeem = async (options) => {
378
- const result = await onRedeem(reward.id, options);
379
- onNotify?.(
380
- result.ok,
381
- result.ok ? dictionary?.[locale]?.rewardClaimed ?? "Reward claimed." : result.error.code
382
- );
383
- };
384
- return /* @__PURE__ */ jsxRuntime.jsxs(
385
- "article",
386
- {
387
- className: `stamprally-reward stamprally-reward--${state?.status ?? "LOCKED"}`,
388
- children: [
389
- /* @__PURE__ */ jsxRuntime.jsx("h3", { children: core.resolveLocalizedText(reward.title, locale) }),
390
- /* @__PURE__ */ jsxRuntime.jsx("p", { children: core.resolveLocalizedText(reward.description, locale) }),
391
- /* @__PURE__ */ jsxRuntime.jsx("span", { role: "status", "aria-live": "polite", children: state?.status ?? "LOCKED" }),
392
- reward.redemptionMethod === "staff_passcode" && /* @__PURE__ */ jsxRuntime.jsxs(
393
- "form",
394
- {
395
- onSubmit: (event) => {
396
- event.preventDefault();
397
- void redeem({ passcode: passcodes[reward.id] ?? "" });
398
- },
399
- children: [
400
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
401
- dictionary?.[locale]?.staffPasscode ?? "Staff passcode",
402
- " ",
403
- /* @__PURE__ */ jsxRuntime.jsx(
404
- "input",
405
- {
406
- type: "password",
407
- value: passcodes[reward.id] ?? "",
408
- onChange: (event) => setPasscodes((current) => ({ ...current, [reward.id]: event.target.value }))
409
- }
410
- )
411
- ] }),
412
- /* @__PURE__ */ jsxRuntime.jsx(
413
- "button",
414
- {
415
- type: "submit",
416
- "aria-label": `Redeem ${core.resolveLocalizedText(reward.title, locale)}`,
417
- "aria-disabled": !available || isPending,
418
- disabled: !available || isPending,
419
- children: dictionary?.[locale]?.redeem ?? "Redeem"
420
- }
421
- )
422
- ]
423
- }
424
- ),
425
- reward.redemptionMethod !== "staff_passcode" && /* @__PURE__ */ jsxRuntime.jsx(
426
- "button",
427
- {
428
- type: "button",
429
- "aria-label": `Redeem ${core.resolveLocalizedText(reward.title, locale)}`,
430
- "aria-disabled": !available || isPending,
431
- disabled: !available || isPending,
432
- onClick: () => void redeem(),
433
- children: dictionary?.[locale]?.redeem ?? "Redeem"
434
- }
435
- )
436
- ]
437
- },
438
- reward.id
439
- );
440
- })
441
- ]
442
- }
147
+ const current = progress ?? core.calculateProgress(
148
+ state ?? { rallyId: config.id, userId: null, records: [], rewards: [], updatedAt: "" },
149
+ config
443
150
  );
151
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { "aria-label": title ?? "Stamp sheet", children: [
152
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { children: title ?? core.resolveLocalizedText(config.title, "en") }),
153
+ /* @__PURE__ */ jsxRuntime.jsx("progress", { max: 100, value: current.percentage }),
154
+ /* @__PURE__ */ jsxRuntime.jsx("div", { children: config.spots.map((spot) => /* @__PURE__ */ jsxRuntime.jsx("span", { children: state?.records.some((record) => record.stampId === spot.id) ? "\u2713" : "\u25CB" }, spot.id)) })
155
+ ] });
444
156
  }
445
157
 
446
158
  exports.RallyViewer = RallyViewer;
447
- exports.RewardPanel = RewardPanel;
448
- exports.StampModal = StampModal;
449
159
  exports.StampSheet = StampSheet;
450
- exports.StampSlot = StampSlot;
451
160
  //# sourceMappingURL=index.cjs.map
452
161
  //# sourceMappingURL=index.cjs.map