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