@stamprally/ui 0.10.0 → 0.12.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/README.md +36 -0
- package/dist/index.cjs +229 -124
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +51 -12
- package/dist/index.d.ts +51 -12
- package/dist/index.js +229 -124
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# @stamprally/ui
|
|
2
|
+
|
|
3
|
+
Participant-facing React components for `@stamprally/core`.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { RallyViewer } from "@stamprally/ui";
|
|
9
|
+
import "@stamprally/ui/styles.css";
|
|
10
|
+
|
|
11
|
+
<RallyViewer config={publicConfig} locale="en" />;
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
For a stateful client, pass `client={new StampRallyClient(publicConfig, options)}`.
|
|
15
|
+
The viewer subscribes to client state, renders all configured verification conditions,
|
|
16
|
+
and exposes check-in and reward actions through the same client.
|
|
17
|
+
|
|
18
|
+
## Render Props customization
|
|
19
|
+
|
|
20
|
+
`renderSpotCard`, `renderRewardCard`, `renderStatusBadge`, and the header/footer slots
|
|
21
|
+
allow a host application to replace presentation while retaining the domain engine.
|
|
22
|
+
`customConditionRenderers` can provide an application-specific QR, NFC, or custom
|
|
23
|
+
verification control. A custom renderer calls `onSubmit(proof)` when it has a proof.
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
<RallyViewer
|
|
27
|
+
config={publicConfig}
|
|
28
|
+
locale="ja"
|
|
29
|
+
renderStatusBadge={({ status }) => <span data-status={status}>{status}</span>}
|
|
30
|
+
renderSpotCard={({ spot, children }) => <article><h2>{spot.name}</h2>{children}</article>}
|
|
31
|
+
/>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Keep private condition secrets, staff passcodes, digital content URLs, and
|
|
35
|
+
`serverMetadata` in the server-side `AdminRallyConfig`; publish only the result of
|
|
36
|
+
`toPublicConfig`.
|
package/dist/index.cjs
CHANGED
|
@@ -6,11 +6,17 @@ var jsxRuntime = require('react/jsx-runtime');
|
|
|
6
6
|
|
|
7
7
|
// src/index.tsx
|
|
8
8
|
var label = (dictionary, locale, key, fallback) => dictionary?.[locale]?.[key] ?? fallback;
|
|
9
|
+
var join = (...names) => {
|
|
10
|
+
const value = names.filter((name) => name !== void 0 && name !== "").join(" ");
|
|
11
|
+
return value === "" ? void 0 : value;
|
|
12
|
+
};
|
|
9
13
|
function DefaultCondition({
|
|
10
14
|
condition,
|
|
11
15
|
dictionary,
|
|
12
16
|
locale,
|
|
13
17
|
disabled,
|
|
18
|
+
classNames,
|
|
19
|
+
styles,
|
|
14
20
|
onSubmit
|
|
15
21
|
}) {
|
|
16
22
|
const [value, setValue] = react.useState("");
|
|
@@ -32,17 +38,9 @@ function DefaultCondition({
|
|
|
32
38
|
setError(text("verificationFailed", "Verification failed."));
|
|
33
39
|
}
|
|
34
40
|
};
|
|
35
|
-
const
|
|
36
|
-
event.preventDefault();
|
|
37
|
-
if (condition.type === "gps") return;
|
|
38
|
-
if (condition.type === "nfc") return;
|
|
39
|
-
void verify(condition.type === "passcode" ? value : value);
|
|
40
|
-
};
|
|
41
|
-
const scanQr = () => {
|
|
42
|
-
if (videoRef.current === null) return;
|
|
41
|
+
const readLocation = () => {
|
|
43
42
|
setStatus("loading");
|
|
44
|
-
|
|
45
|
-
void core.readQrContext(videoRef.current).then((result) => {
|
|
43
|
+
void core.getCurrentGeoContext().then((result) => {
|
|
46
44
|
if (result.ok) void verify(result.value);
|
|
47
45
|
else {
|
|
48
46
|
setStatus("error");
|
|
@@ -50,10 +48,9 @@ function DefaultCondition({
|
|
|
50
48
|
}
|
|
51
49
|
});
|
|
52
50
|
};
|
|
53
|
-
const
|
|
51
|
+
const readNfc = () => {
|
|
54
52
|
setStatus("loading");
|
|
55
|
-
|
|
56
|
-
void core.getCurrentGeoContext().then((result) => {
|
|
53
|
+
void core.readNfcContext().then((result) => {
|
|
57
54
|
if (result.ok) void verify(result.value);
|
|
58
55
|
else {
|
|
59
56
|
setStatus("error");
|
|
@@ -61,10 +58,10 @@ function DefaultCondition({
|
|
|
61
58
|
}
|
|
62
59
|
});
|
|
63
60
|
};
|
|
64
|
-
const
|
|
61
|
+
const scanQr = () => {
|
|
62
|
+
if (videoRef.current === null) return;
|
|
65
63
|
setStatus("loading");
|
|
66
|
-
|
|
67
|
-
void core.readNfcContext().then((result) => {
|
|
64
|
+
void core.readQrContext(videoRef.current).then((result) => {
|
|
68
65
|
if (result.ok) void verify(result.value);
|
|
69
66
|
else {
|
|
70
67
|
setStatus("error");
|
|
@@ -99,45 +96,76 @@ function DefaultCondition({
|
|
|
99
96
|
"button",
|
|
100
97
|
{
|
|
101
98
|
type: "button",
|
|
102
|
-
className: "sry-action",
|
|
99
|
+
className: join("sry-action", classNames?.action, classNames?.button),
|
|
100
|
+
style: styles?.button ?? styles?.action,
|
|
103
101
|
disabled: disabled || !core.isQrSupported(),
|
|
104
102
|
onClick: scanQr,
|
|
105
103
|
children: text("scanQr", "Scan QR")
|
|
106
104
|
}
|
|
107
105
|
)
|
|
108
106
|
] }),
|
|
109
|
-
condition.type === "gps" && /* @__PURE__ */ jsxRuntime.jsx(
|
|
107
|
+
condition.type === "gps" && /* @__PURE__ */ jsxRuntime.jsx(
|
|
108
|
+
"button",
|
|
109
|
+
{
|
|
110
|
+
type: "button",
|
|
111
|
+
className: join("sry-action", classNames?.action, classNames?.button),
|
|
112
|
+
style: styles?.button ?? styles?.action,
|
|
113
|
+
disabled,
|
|
114
|
+
onClick: readLocation,
|
|
115
|
+
children: text("checkLocation", "Check location")
|
|
116
|
+
}
|
|
117
|
+
),
|
|
110
118
|
condition.type === "nfc" && /* @__PURE__ */ jsxRuntime.jsx(
|
|
111
119
|
"button",
|
|
112
120
|
{
|
|
113
121
|
type: "button",
|
|
114
|
-
className: "sry-action",
|
|
122
|
+
className: join("sry-action", classNames?.action, classNames?.button),
|
|
123
|
+
style: styles?.button ?? styles?.action,
|
|
115
124
|
disabled: disabled || !core.isNfcSupported(),
|
|
116
125
|
onClick: readNfc,
|
|
117
126
|
children: text("readNfc", "Read NFC")
|
|
118
127
|
}
|
|
119
128
|
),
|
|
120
|
-
(condition.type === "passcode" || condition.type === "custom" || condition.type === "qr") && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
{
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
129
|
+
(condition.type === "passcode" || condition.type === "custom" || condition.type === "qr") && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
130
|
+
"form",
|
|
131
|
+
{
|
|
132
|
+
onSubmit: (event) => {
|
|
133
|
+
event.preventDefault();
|
|
134
|
+
void verify(value);
|
|
135
|
+
},
|
|
136
|
+
children: [
|
|
137
|
+
/* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
|
|
138
|
+
text(
|
|
139
|
+
condition.type === "passcode" ? "passcode" : "proof",
|
|
140
|
+
condition.type === "passcode" ? "Passcode" : "Proof"
|
|
141
|
+
),
|
|
142
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
143
|
+
"input",
|
|
144
|
+
{
|
|
145
|
+
"aria-label": text("proof", "Proof"),
|
|
146
|
+
value,
|
|
147
|
+
onChange: (event) => setValue(event.target.value)
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
] }),
|
|
151
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
152
|
+
"button",
|
|
153
|
+
{
|
|
154
|
+
type: "submit",
|
|
155
|
+
className: join("sry-action", classNames?.action, classNames?.button),
|
|
156
|
+
style: styles?.button ?? styles?.action,
|
|
157
|
+
disabled: disabled || value.trim() === "",
|
|
158
|
+
children: text("checkIn", "Check in")
|
|
159
|
+
}
|
|
160
|
+
)
|
|
161
|
+
]
|
|
162
|
+
}
|
|
163
|
+
),
|
|
137
164
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
138
165
|
"div",
|
|
139
166
|
{
|
|
140
|
-
className: "sry-feedback",
|
|
167
|
+
className: join("sry-feedback", classNames?.feedback),
|
|
168
|
+
style: styles?.feedback,
|
|
141
169
|
"aria-live": "polite",
|
|
142
170
|
role: status === "error" ? "alert" : void 0,
|
|
143
171
|
children: [
|
|
@@ -149,6 +177,35 @@ function DefaultCondition({
|
|
|
149
177
|
)
|
|
150
178
|
] });
|
|
151
179
|
}
|
|
180
|
+
function DefaultRewardCard({
|
|
181
|
+
reward,
|
|
182
|
+
state,
|
|
183
|
+
locale,
|
|
184
|
+
dictionary,
|
|
185
|
+
onClaim,
|
|
186
|
+
classNames,
|
|
187
|
+
styles
|
|
188
|
+
}) {
|
|
189
|
+
const status = state?.status ?? "LOCKED";
|
|
190
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
191
|
+
"button",
|
|
192
|
+
{
|
|
193
|
+
type: "button",
|
|
194
|
+
className: join(classNames?.reward, classNames?.button),
|
|
195
|
+
style: styles?.reward ?? styles?.button,
|
|
196
|
+
disabled: status !== "AVAILABLE" || onClaim === void 0,
|
|
197
|
+
onClick: () => {
|
|
198
|
+
if (onClaim !== void 0) void onClaim(reward.id);
|
|
199
|
+
},
|
|
200
|
+
children: [
|
|
201
|
+
core.resolveLocalizedText(reward.title, locale),
|
|
202
|
+
" (",
|
|
203
|
+
label(dictionary, locale, `status.${status.toLowerCase()}`, status),
|
|
204
|
+
")"
|
|
205
|
+
]
|
|
206
|
+
}
|
|
207
|
+
);
|
|
208
|
+
}
|
|
152
209
|
function RallyViewer({
|
|
153
210
|
config: providedConfig,
|
|
154
211
|
client,
|
|
@@ -156,12 +213,22 @@ function RallyViewer({
|
|
|
156
213
|
locale,
|
|
157
214
|
dictionary,
|
|
158
215
|
classNames = {},
|
|
216
|
+
styles = {},
|
|
159
217
|
style,
|
|
160
|
-
customConditionRenderers = {}
|
|
218
|
+
customConditionRenderers = {},
|
|
219
|
+
headerSlot,
|
|
220
|
+
footerSlot,
|
|
221
|
+
renderSpotCard,
|
|
222
|
+
renderRewardCard,
|
|
223
|
+
renderStatusBadge,
|
|
224
|
+
renderVerifyingState,
|
|
225
|
+
renderSuccessFeedback,
|
|
226
|
+
renderErrorFeedback
|
|
161
227
|
}) {
|
|
162
228
|
const config = providedConfig ?? client?.getConfig() ?? adapter?.config;
|
|
163
229
|
const [state, setState] = react.useState(() => client?.getState() ?? null);
|
|
164
230
|
const [busy, setBusy] = react.useState(null);
|
|
231
|
+
const [feedback, setFeedback] = react.useState(null);
|
|
165
232
|
react.useEffect(() => {
|
|
166
233
|
if (client === void 0) return;
|
|
167
234
|
const unsubscribe = client.subscribe(setState);
|
|
@@ -169,20 +236,26 @@ function RallyViewer({
|
|
|
169
236
|
return unsubscribe;
|
|
170
237
|
}, [client]);
|
|
171
238
|
if (config === void 0) throw new Error("RallyViewer requires config, client, or adapter.");
|
|
239
|
+
const currentState = state ?? {
|
|
240
|
+
rallyId: config.id,
|
|
241
|
+
userId: null,
|
|
242
|
+
records: [],
|
|
243
|
+
rewards: [],
|
|
244
|
+
updatedAt: ""
|
|
245
|
+
};
|
|
172
246
|
const progress = react.useMemo(
|
|
173
|
-
() => core.calculateProgress(
|
|
174
|
-
|
|
175
|
-
config
|
|
176
|
-
),
|
|
177
|
-
[config, state]
|
|
247
|
+
() => core.calculateProgress(currentState, config),
|
|
248
|
+
[config, currentState]
|
|
178
249
|
);
|
|
179
250
|
const checkIn = adapter?.onCheckIn ?? (client === void 0 ? void 0 : (spotId, proof, options) => client.checkIn(spotId, proof, options));
|
|
180
251
|
const claim = adapter?.onClaimReward ?? (client === void 0 ? void 0 : (rewardId, options) => client.claimReward(rewardId, options));
|
|
181
252
|
const submit = react.useCallback(
|
|
182
|
-
(spotId, proof) => {
|
|
183
|
-
if (checkIn === void 0) return
|
|
253
|
+
async (spotId, proof) => {
|
|
254
|
+
if (checkIn === void 0) return void 0;
|
|
184
255
|
setBusy(spotId);
|
|
185
|
-
|
|
256
|
+
const result = await checkIn(spotId, proof).finally(() => setBusy(null));
|
|
257
|
+
setFeedback(result);
|
|
258
|
+
return result;
|
|
186
259
|
},
|
|
187
260
|
[checkIn]
|
|
188
261
|
);
|
|
@@ -190,111 +263,143 @@ function RallyViewer({
|
|
|
190
263
|
"section",
|
|
191
264
|
{
|
|
192
265
|
className: classNames.root,
|
|
193
|
-
style,
|
|
266
|
+
style: styles.root ?? style,
|
|
194
267
|
"aria-label": label(dictionary, locale, "viewer", "Stamp rally"),
|
|
195
268
|
children: [
|
|
196
|
-
/* @__PURE__ */ jsxRuntime.
|
|
197
|
-
|
|
198
|
-
"
|
|
199
|
-
|
|
200
|
-
"
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
269
|
+
/* @__PURE__ */ jsxRuntime.jsxs("header", { className: classNames.header, style: styles.header, children: [
|
|
270
|
+
typeof headerSlot === "function" ? headerSlot({ config, state: currentState }) : headerSlot,
|
|
271
|
+
headerSlot === void 0 && /* @__PURE__ */ jsxRuntime.jsx("h1", { children: core.resolveLocalizedText(config.title, locale) || config.id }),
|
|
272
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
273
|
+
"progress",
|
|
274
|
+
{
|
|
275
|
+
"aria-label": label(dictionary, locale, "progress", "Progress"),
|
|
276
|
+
max: 100,
|
|
277
|
+
value: progress.percentage
|
|
278
|
+
}
|
|
279
|
+
),
|
|
280
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
281
|
+
progress.acquired,
|
|
282
|
+
"/",
|
|
283
|
+
progress.total
|
|
284
|
+
] })
|
|
210
285
|
] }),
|
|
211
|
-
|
|
212
|
-
|
|
286
|
+
busy !== null && renderVerifyingState?.(),
|
|
287
|
+
feedback?.ok === true && renderSuccessFeedback?.(feedback),
|
|
288
|
+
feedback?.ok === false && renderErrorFeedback?.(
|
|
289
|
+
"message" in feedback.error ? feedback.error.message : feedback.error.code
|
|
290
|
+
),
|
|
291
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { children: config.spots.map((spot) => {
|
|
292
|
+
const claimed = currentState.records.some((record) => record.stampId === spot.id);
|
|
293
|
+
const status = busy === spot.id ? "verifying" : claimed ? "claimed" : "available";
|
|
294
|
+
const children = spot.conditions.map((condition) => {
|
|
213
295
|
const Renderer = customConditionRenderers[condition.type] ?? DefaultCondition;
|
|
214
|
-
return /* @__PURE__ */ jsxRuntime.
|
|
215
|
-
"
|
|
296
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
297
|
+
"div",
|
|
216
298
|
{
|
|
217
299
|
className: classNames.condition,
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
300
|
+
style: styles.condition,
|
|
301
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
302
|
+
Renderer,
|
|
303
|
+
{
|
|
304
|
+
spot,
|
|
305
|
+
condition,
|
|
306
|
+
locale,
|
|
307
|
+
...dictionary === void 0 ? {} : { dictionary },
|
|
308
|
+
disabled: busy !== null || claimed,
|
|
309
|
+
...Object.keys(classNames).length === 0 ? {} : { classNames },
|
|
310
|
+
...Object.keys(styles).length === 0 ? {} : { styles },
|
|
311
|
+
onSubmit: (proof) => submit(spot.id, proof)
|
|
312
|
+
}
|
|
313
|
+
)
|
|
232
314
|
},
|
|
233
315
|
`${spot.id}-${condition.type}-${JSON.stringify(condition)}`
|
|
234
316
|
);
|
|
235
|
-
})
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
317
|
+
});
|
|
318
|
+
const props = {
|
|
319
|
+
spot,
|
|
320
|
+
state: currentState,
|
|
321
|
+
status,
|
|
322
|
+
locale,
|
|
323
|
+
...dictionary === void 0 ? {} : { dictionary },
|
|
324
|
+
children
|
|
325
|
+
};
|
|
326
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: classNames.card, style: styles.card, children: renderSpotCard?.(props) ?? /* @__PURE__ */ jsxRuntime.jsxs("article", { children: [
|
|
327
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { children: core.resolveLocalizedText(spot.name, locale) }),
|
|
328
|
+
renderStatusBadge?.({ status }) ?? /* @__PURE__ */ jsxRuntime.jsx("span", { className: classNames.badge, style: styles.badge, children: status }),
|
|
329
|
+
children
|
|
330
|
+
] }) }, spot.id);
|
|
331
|
+
}) }),
|
|
332
|
+
/* @__PURE__ */ jsxRuntime.jsx("section", { "aria-label": label(dictionary, locale, "rewards", "Rewards"), children: config.rewards.map((reward) => {
|
|
333
|
+
const props = {
|
|
240
334
|
reward,
|
|
241
|
-
state:
|
|
335
|
+
state: currentState.rewards.find((item) => item.rewardId === reward.id),
|
|
242
336
|
locale,
|
|
243
337
|
...dictionary === void 0 ? {} : { dictionary },
|
|
244
|
-
...classNames.reward === void 0 ? {} : { className: classNames.reward },
|
|
245
338
|
onClaim: claim
|
|
246
|
-
}
|
|
247
|
-
reward.id
|
|
248
|
-
)
|
|
339
|
+
};
|
|
340
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: classNames.reward, style: styles.reward, children: renderRewardCard?.(props) ?? /* @__PURE__ */ jsxRuntime.jsx(DefaultRewardCard, { ...props, classNames, styles }) }, reward.id);
|
|
341
|
+
}) }),
|
|
342
|
+
/* @__PURE__ */ jsxRuntime.jsx("footer", { className: classNames.footer, style: styles.footer, children: footerSlot })
|
|
249
343
|
]
|
|
250
344
|
}
|
|
251
345
|
);
|
|
252
346
|
}
|
|
253
|
-
function
|
|
254
|
-
|
|
347
|
+
function StampSheet({
|
|
348
|
+
config,
|
|
255
349
|
state,
|
|
256
|
-
|
|
350
|
+
title,
|
|
351
|
+
progress,
|
|
352
|
+
locale = "en",
|
|
257
353
|
dictionary,
|
|
258
|
-
|
|
259
|
-
|
|
354
|
+
classNames = {},
|
|
355
|
+
styles = {},
|
|
356
|
+
style,
|
|
357
|
+
headerSlot,
|
|
358
|
+
footerSlot,
|
|
359
|
+
renderSpotCard,
|
|
360
|
+
renderStatusBadge
|
|
260
361
|
}) {
|
|
261
|
-
const
|
|
362
|
+
const currentState = state ?? {
|
|
363
|
+
rallyId: config.id,
|
|
364
|
+
userId: null,
|
|
365
|
+
records: [],
|
|
366
|
+
rewards: [],
|
|
367
|
+
updatedAt: ""
|
|
368
|
+
};
|
|
369
|
+
const current = progress ?? core.calculateProgress(currentState, config);
|
|
262
370
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
263
|
-
"
|
|
371
|
+
"section",
|
|
264
372
|
{
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
onClick: () => {
|
|
269
|
-
if (onClaim !== void 0) void onClaim(reward.id);
|
|
270
|
-
},
|
|
373
|
+
className: classNames.root,
|
|
374
|
+
style: styles.root ?? style,
|
|
375
|
+
"aria-label": title ?? label(dictionary, locale, "stampSheet", "Stamp sheet"),
|
|
271
376
|
children: [
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
377
|
+
/* @__PURE__ */ jsxRuntime.jsxs("header", { className: classNames.header, style: styles.header, children: [
|
|
378
|
+
typeof headerSlot === "function" ? headerSlot({ config, state: currentState }) : headerSlot ?? /* @__PURE__ */ jsxRuntime.jsx("h2", { children: title ?? core.resolveLocalizedText(config.title, locale) }),
|
|
379
|
+
/* @__PURE__ */ jsxRuntime.jsx("progress", { max: 100, value: current.percentage })
|
|
380
|
+
] }),
|
|
381
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { children: config.spots.map((spot) => {
|
|
382
|
+
const claimed = currentState.records.some((record) => record.stampId === spot.id);
|
|
383
|
+
const status = claimed ? "claimed" : "available";
|
|
384
|
+
const props = {
|
|
385
|
+
spot,
|
|
386
|
+
state: currentState,
|
|
387
|
+
status,
|
|
388
|
+
locale,
|
|
389
|
+
...dictionary === void 0 ? {} : { dictionary },
|
|
390
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: classNames.slot, style: styles.slot, children: claimed ? "\u2713" : "\u25CB" })
|
|
391
|
+
};
|
|
392
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: classNames.card, style: styles.card, children: renderSpotCard?.(props) ?? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: classNames.slot, style: styles.slot, children: [
|
|
393
|
+
core.resolveLocalizedText(spot.name, locale),
|
|
394
|
+
" ",
|
|
395
|
+
renderStatusBadge?.({ status }) ?? (claimed ? "\u2713" : "\u25CB")
|
|
396
|
+
] }) }, spot.id);
|
|
397
|
+
}) }),
|
|
398
|
+
/* @__PURE__ */ jsxRuntime.jsx("footer", { className: classNames.footer, style: styles.footer, children: footerSlot })
|
|
276
399
|
]
|
|
277
400
|
}
|
|
278
401
|
);
|
|
279
402
|
}
|
|
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
|
-
}
|
|
298
403
|
|
|
299
404
|
exports.RallyViewer = RallyViewer;
|
|
300
405
|
exports.StampSheet = StampSheet;
|