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