@stamprally/ui 0.8.0 → 0.10.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,298 @@ var react = require('react');
5
5
  var jsxRuntime = require('react/jsx-runtime');
6
6
 
7
7
  // src/index.tsx
8
- function RallyViewer({
9
- config,
10
- adapter,
11
- client,
8
+ var label = (dictionary, locale, key, fallback) => dictionary?.[locale]?.[key] ?? fallback;
9
+ function DefaultCondition({
10
+ condition,
11
+ dictionary,
12
12
  locale,
13
- onSpotSelect
13
+ disabled,
14
+ onSubmit
14
15
  }) {
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: []
16
+ const [value, setValue] = react.useState("");
17
+ const [status, setStatus] = react.useState("idle");
18
+ const [error, setError] = react.useState(null);
19
+ const videoRef = react.useRef(null);
20
+ const text = (key, fallback) => label(dictionary, locale, key, fallback);
21
+ const verify = async (proof) => {
22
+ setStatus("loading");
23
+ setError(null);
24
+ try {
25
+ const result = await onSubmit(proof);
26
+ if (result !== void 0 && typeof result === "object" && result !== null && "ok" in result && result.ok === false) {
27
+ setStatus("error");
28
+ setError(text("verificationFailed", "Verification failed."));
29
+ } else setStatus("success");
30
+ } catch {
31
+ setStatus("error");
32
+ setError(text("verificationFailed", "Verification failed."));
33
+ }
22
34
  };
23
- const emptyState = {
24
- rallyId: renderConfig.id,
25
- records: [],
26
- updatedAt: (/* @__PURE__ */ new Date(0)).toISOString()
35
+ const submit = (event) => {
36
+ event.preventDefault();
37
+ if (condition.type === "gps") return;
38
+ if (condition.type === "nfc") return;
39
+ void verify(condition.type === "passcode" ? value : value);
27
40
  };
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);
33
- react.useEffect(() => {
34
- if (client === void 0) return;
35
- return client.subscribe(() => setClientRevision((revision) => revision + 1));
36
- }, [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
- }
41
+ const scanQr = () => {
42
+ if (videoRef.current === null) return;
43
+ setStatus("loading");
44
+ setError(null);
45
+ void core.readQrContext(videoRef.current).then((result) => {
46
+ if (result.ok) void verify(result.value);
47
+ else {
48
+ setStatus("error");
49
+ setError(result.error.message);
50
+ }
51
+ });
49
52
  };
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" })
56
- ] }),
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",
53
+ const readLocation = () => {
54
+ setStatus("loading");
55
+ setError(null);
56
+ void core.getCurrentGeoContext().then((result) => {
57
+ if (result.ok) void verify(result.value);
58
+ else {
59
+ setStatus("error");
60
+ setError(result.error.message);
61
+ }
62
+ });
63
+ };
64
+ const readNfc = () => {
65
+ setStatus("loading");
66
+ setError(null);
67
+ void core.readNfcContext().then((result) => {
68
+ if (result.ok) void verify(result.value);
69
+ else {
70
+ setStatus("error");
71
+ setError(result.error.message);
72
+ }
73
+ });
74
+ };
75
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
76
+ condition.type === "qr" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
77
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
78
+ text("qrValue", "QR value"),
79
+ /* @__PURE__ */ jsxRuntime.jsx(
80
+ "input",
81
+ {
82
+ "aria-label": text("qrValue", "QR value"),
83
+ placeholder: condition.qrEntryUrl ?? text("qrPlaceholder", "Paste a QR value"),
84
+ value,
85
+ onChange: (event) => setValue(event.target.value)
86
+ }
87
+ )
88
+ ] }),
89
+ /* @__PURE__ */ jsxRuntime.jsx(
90
+ "video",
60
91
  {
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
- ]
92
+ ref: videoRef,
93
+ "aria-label": text("qrCamera", "QR camera"),
94
+ hidden: !core.isQrSupported(),
95
+ children: /* @__PURE__ */ jsxRuntime.jsx("track", { kind: "captions" })
76
96
  }
77
97
  ),
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";
86
- 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",
91
- {
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"
98
- }
99
- )
100
- ] }, reward.id);
101
- })
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
98
  /* @__PURE__ */ jsxRuntime.jsx(
111
99
  "button",
112
100
  {
113
101
  type: "button",
114
- onClick: () => void submit(),
115
- disabled: claimed.has(selectedSpot.id),
116
- children: "Check in"
102
+ className: "sry-action",
103
+ disabled: disabled || !core.isQrSupported(),
104
+ onClick: scanQr,
105
+ children: text("scanQr", "Scan QR")
117
106
  }
118
- ),
119
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => setSelectedSpotId(null), children: "Close" })
120
- ] })
121
- ] });
122
- }
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();
107
+ )
108
+ ] }),
109
+ condition.type === "gps" && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "sry-action", disabled, onClick: readLocation, children: text("checkLocation", "Check location") }),
110
+ condition.type === "nfc" && /* @__PURE__ */ jsxRuntime.jsx(
111
+ "button",
112
+ {
113
+ type: "button",
114
+ className: "sry-action",
115
+ disabled: disabled || !core.isNfcSupported(),
116
+ onClick: readNfc,
117
+ children: text("readNfc", "Read NFC")
156
118
  }
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
188
- }) {
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
- return /* @__PURE__ */ jsxRuntime.jsxs(
195
- "button",
196
- {
197
- 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)" },
206
- 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",
119
+ ),
120
+ (condition.type === "passcode" || condition.type === "custom" || condition.type === "qr") && /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: submit, children: [
121
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
122
+ text(
123
+ condition.type === "passcode" ? "passcode" : "proof",
124
+ condition.type === "passcode" ? "Passcode" : "Proof"
125
+ ),
126
+ /* @__PURE__ */ jsxRuntime.jsx(
127
+ "input",
222
128
  {
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
- ] })
129
+ "aria-label": text("proof", "Proof"),
130
+ value,
131
+ onChange: (event) => setValue(event.target.value)
230
132
  }
231
133
  )
232
- ]
233
- }
234
- );
134
+ ] }),
135
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", className: "sry-action", disabled: disabled || value.trim() === "", children: text("checkIn", "Check in") })
136
+ ] }),
137
+ /* @__PURE__ */ jsxRuntime.jsxs(
138
+ "div",
139
+ {
140
+ className: "sry-feedback",
141
+ "aria-live": "polite",
142
+ role: status === "error" ? "alert" : void 0,
143
+ children: [
144
+ status === "loading" && text("verifying", "Verifying\u2026"),
145
+ status === "success" && text("verified", "Verified"),
146
+ status === "error" && (error ?? text("verificationFailed", "Verification failed."))
147
+ ]
148
+ }
149
+ )
150
+ ] });
235
151
  }
236
- function StampSheet({
237
- title,
238
- config,
239
- state = null,
240
- progress,
241
- presentations = {},
242
- animatedStampId = null,
243
- disabled = false,
244
- locale = "ja",
152
+ function RallyViewer({
153
+ config: providedConfig,
154
+ client,
155
+ adapter,
156
+ locale,
245
157
  dictionary,
246
- theme = config.theme ?? core.DEFAULT_SHEET_THEME,
247
- onStampSelect,
248
- adapter
158
+ classNames = {},
159
+ style,
160
+ customConditionRenderers = {}
249
161
  }) {
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
- };
162
+ const config = providedConfig ?? client?.getConfig() ?? adapter?.config;
163
+ const [state, setState] = react.useState(() => client?.getState() ?? null);
164
+ const [busy, setBusy] = react.useState(null);
165
+ react.useEffect(() => {
166
+ if (client === void 0) return;
167
+ const unsubscribe = client.subscribe(setState);
168
+ void client.init().then(setState);
169
+ return unsubscribe;
170
+ }, [client]);
171
+ if (config === void 0) throw new Error("RallyViewer requires config, client, or adapter.");
172
+ const progress = react.useMemo(
173
+ () => core.calculateProgress(
174
+ state ?? { rallyId: config.id, userId: null, records: [], rewards: [], updatedAt: "" },
175
+ config
176
+ ),
177
+ [config, state]
178
+ );
179
+ const checkIn = adapter?.onCheckIn ?? (client === void 0 ? void 0 : (spotId, proof, options) => client.checkIn(spotId, proof, options));
180
+ const claim = adapter?.onClaimReward ?? (client === void 0 ? void 0 : (rewardId, options) => client.claimReward(rewardId, options));
181
+ const submit = react.useCallback(
182
+ (spotId, proof) => {
183
+ if (checkIn === void 0) return Promise.resolve(void 0);
184
+ setBusy(spotId);
185
+ return checkIn(spotId, proof).finally(() => setBusy(null));
186
+ },
187
+ [checkIn]
188
+ );
298
189
  return /* @__PURE__ */ jsxRuntime.jsxs(
299
190
  "section",
300
191
  {
301
- className: `stamp-sheet ${percentage >= 100 ? "stamp-sheet--completed" : ""}`,
192
+ className: classNames.root,
302
193
  style,
303
- "aria-label": core.resolveLocalizedText(title, locale) || config.id,
194
+ "aria-label": label(dictionary, locale, "viewer", "Stamp rally"),
304
195
  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
377
- }) {
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: [
196
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { children: core.resolveLocalizedText(config.title, locale) || config.id }),
402
197
  /* @__PURE__ */ jsxRuntime.jsx(
403
- "button",
198
+ "progress",
404
199
  {
405
- type: "button",
406
- "aria-label": "Close stamp details",
407
- "aria-disabled": isPending,
408
- onClick: onClose,
409
- disabled: isPending,
410
- children: "\xD7"
200
+ "aria-label": label(dictionary, locale, "progress", "Progress"),
201
+ max: 100,
202
+ value: progress.percentage
411
203
  }
412
204
  ),
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",
205
+ " ",
206
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
207
+ progress.acquired,
208
+ "/",
209
+ progress.total
210
+ ] }),
211
+ /* @__PURE__ */ jsxRuntime.jsx("div", { children: config.spots.map(
212
+ (spot) => spot.conditions.map((condition) => {
213
+ const Renderer = customConditionRenderers[condition.type] ?? DefaultCondition;
214
+ return /* @__PURE__ */ jsxRuntime.jsxs(
215
+ "article",
216
+ {
217
+ className: classNames.condition,
218
+ children: [
219
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { children: core.resolveLocalizedText(spot.name, locale) }),
220
+ /* @__PURE__ */ jsxRuntime.jsx(
221
+ Renderer,
222
+ {
223
+ spot,
224
+ condition,
225
+ locale,
226
+ ...dictionary === void 0 ? {} : { dictionary },
227
+ disabled: busy !== null,
228
+ onSubmit: (proof) => submit(spot.id, proof)
229
+ }
230
+ )
231
+ ]
232
+ },
233
+ `${spot.id}-${condition.type}-${JSON.stringify(condition)}`
234
+ );
235
+ })
236
+ ) }),
237
+ /* @__PURE__ */ jsxRuntime.jsx("section", { "aria-label": label(dictionary, locale, "rewards", "Rewards"), children: config.rewards.map((reward) => /* @__PURE__ */ jsxRuntime.jsx(
238
+ RewardButton,
442
239
  {
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
- ] })
240
+ reward,
241
+ state: state?.rewards.find((item) => item.rewardId === reward.id),
242
+ locale,
243
+ ...dictionary === void 0 ? {} : { dictionary },
244
+ ...classNames.reward === void 0 ? {} : { className: classNames.reward },
245
+ onClaim: claim
246
+ },
247
+ reward.id
248
+ )) })
249
+ ]
452
250
  }
453
251
  );
454
252
  }
455
- function RewardPanel({
456
- rewards,
457
- states,
458
- locale = "ja",
253
+ function RewardButton({
254
+ reward,
255
+ state,
256
+ locale,
459
257
  dictionary,
460
- isPending = false,
461
- open = true,
462
- onClose,
463
- onRedeem,
464
- onNotify
258
+ className,
259
+ onClaim
465
260
  }) {
466
- const [passcodes, setPasscodes] = react.useState({});
467
- const panelRef = useFocusTrap(open && onClose !== void 0, onClose, isPending);
468
- if (!open) return null;
261
+ const status = state?.status ?? "LOCKED";
469
262
  return /* @__PURE__ */ jsxRuntime.jsxs(
470
- "section",
263
+ "button",
471
264
  {
472
- ref: panelRef,
473
- role: "dialog",
474
- "aria-labelledby": "stamprally-rewards-title",
475
- "aria-modal": "true",
476
- className: "stamprally-rewards",
477
- tabIndex: -1,
265
+ type: "button",
266
+ className,
267
+ disabled: status !== "AVAILABLE" || onClaim === void 0,
268
+ onClick: () => {
269
+ if (onClaim !== void 0) void onClaim(reward.id);
270
+ },
478
271
  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
- })
272
+ core.resolveLocalizedText(reward.title, locale),
273
+ " (",
274
+ label(dictionary, locale, `status.${status.toLowerCase()}`, status),
275
+ ")"
560
276
  ]
561
277
  }
562
278
  );
563
279
  }
280
+ function StampSheet({
281
+ config,
282
+ state,
283
+ title,
284
+ progress,
285
+ locale = "en",
286
+ dictionary
287
+ }) {
288
+ const current = progress ?? core.calculateProgress(
289
+ state ?? { rallyId: config.id, userId: null, records: [], rewards: [], updatedAt: "" },
290
+ config
291
+ );
292
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { "aria-label": title ?? label(dictionary, locale, "stampSheet", "Stamp sheet"), children: [
293
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { children: title ?? core.resolveLocalizedText(config.title, locale) }),
294
+ /* @__PURE__ */ jsxRuntime.jsx("progress", { max: 100, value: current.percentage }),
295
+ /* @__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)) })
296
+ ] });
297
+ }
564
298
 
565
299
  exports.RallyViewer = RallyViewer;
566
- exports.RewardPanel = RewardPanel;
567
- exports.StampModal = StampModal;
568
300
  exports.StampSheet = StampSheet;
569
- exports.StampSlot = StampSlot;
570
301
  //# sourceMappingURL=index.cjs.map
571
302
  //# sourceMappingURL=index.cjs.map