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