@stamprally/ui 0.9.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
@@ -14,25 +14,139 @@ function DefaultCondition({
14
14
  onSubmit
15
15
  }) {
16
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
+ }
34
+ };
17
35
  const submit = (event) => {
18
36
  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
- );
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;
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
+ });
52
+ };
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
+ });
22
74
  };
23
- return /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: submit, children: [
24
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
25
- label(dictionary, locale, "proof", "Proof"),
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
+ ] }),
26
89
  /* @__PURE__ */ jsxRuntime.jsx(
27
- "input",
90
+ "video",
28
91
  {
29
- "aria-label": label(dictionary, locale, "proof", "Proof"),
30
- value,
31
- onChange: (event) => setValue(event.target.value)
92
+ ref: videoRef,
93
+ "aria-label": text("qrCamera", "QR camera"),
94
+ hidden: !core.isQrSupported(),
95
+ children: /* @__PURE__ */ jsxRuntime.jsx("track", { kind: "captions" })
96
+ }
97
+ ),
98
+ /* @__PURE__ */ jsxRuntime.jsx(
99
+ "button",
100
+ {
101
+ type: "button",
102
+ className: "sry-action",
103
+ disabled: disabled || !core.isQrSupported(),
104
+ onClick: scanQr,
105
+ children: text("scanQr", "Scan QR")
32
106
  }
33
107
  )
34
108
  ] }),
35
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", disabled, children: condition.type === "gps" ? label(dictionary, locale, "checkLocation", "Check location") : label(dictionary, locale, "checkIn", "Check in") })
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")
118
+ }
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",
128
+ {
129
+ "aria-label": text("proof", "Proof"),
130
+ value,
131
+ onChange: (event) => setValue(event.target.value)
132
+ }
133
+ )
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
+ )
36
150
  ] });
37
151
  }
38
152
  function RallyViewer({
@@ -41,6 +155,8 @@ function RallyViewer({
41
155
  adapter,
42
156
  locale,
43
157
  dictionary,
158
+ classNames = {},
159
+ style,
44
160
  customConditionRenderers = {}
45
161
  }) {
46
162
  const config = providedConfig ?? client?.getConfig() ?? adapter?.config;
@@ -64,75 +180,98 @@ function RallyViewer({
64
180
  const claim = adapter?.onClaimReward ?? (client === void 0 ? void 0 : (rewardId, options) => client.claimReward(rewardId, options));
65
181
  const submit = react.useCallback(
66
182
  (spotId, proof) => {
67
- if (checkIn === void 0) return;
183
+ if (checkIn === void 0) return Promise.resolve(void 0);
68
184
  setBusy(spotId);
69
- void checkIn(spotId, proof).finally(() => setBusy(null));
185
+ return checkIn(spotId, proof).finally(() => setBusy(null));
70
186
  },
71
187
  [checkIn]
72
188
  );
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
88
- ] }),
89
- /* @__PURE__ */ jsxRuntime.jsx("div", { children: config.spots.map(
90
- (spot) => spot.conditions.map((condition) => {
91
- const Renderer = customConditionRenderers[condition.type] ?? DefaultCondition;
92
- return /* @__PURE__ */ jsxRuntime.jsxs("article", { children: [
93
- /* @__PURE__ */ jsxRuntime.jsx("h2", { children: core.resolveLocalizedText(spot.name, locale) }),
94
- /* @__PURE__ */ jsxRuntime.jsx(
95
- Renderer,
96
- {
97
- spot,
98
- condition,
99
- locale,
100
- ...dictionary === void 0 ? {} : { dictionary },
101
- disabled: busy !== null,
102
- onSubmit: (proof) => submit(spot.id, proof)
103
- }
104
- )
105
- ] }, `${spot.id}-${condition.type}-${JSON.stringify(condition)}`);
106
- })
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
- )) })
117
- ] });
189
+ return /* @__PURE__ */ jsxRuntime.jsxs(
190
+ "section",
191
+ {
192
+ className: classNames.root,
193
+ style,
194
+ "aria-label": label(dictionary, locale, "viewer", "Stamp rally"),
195
+ children: [
196
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { children: core.resolveLocalizedText(config.title, locale) || config.id }),
197
+ /* @__PURE__ */ jsxRuntime.jsx(
198
+ "progress",
199
+ {
200
+ "aria-label": label(dictionary, locale, "progress", "Progress"),
201
+ max: 100,
202
+ value: progress.percentage
203
+ }
204
+ ),
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,
239
+ {
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
+ ]
250
+ }
251
+ );
118
252
  }
119
253
  function RewardButton({
120
254
  reward,
121
255
  state,
256
+ locale,
257
+ dictionary,
258
+ className,
122
259
  onClaim
123
260
  }) {
261
+ const status = state?.status ?? "LOCKED";
124
262
  return /* @__PURE__ */ jsxRuntime.jsxs(
125
263
  "button",
126
264
  {
127
265
  type: "button",
128
- disabled: state?.status !== "AVAILABLE" || onClaim === void 0,
266
+ className,
267
+ disabled: status !== "AVAILABLE" || onClaim === void 0,
129
268
  onClick: () => {
130
269
  if (onClaim !== void 0) void onClaim(reward.id);
131
270
  },
132
271
  children: [
133
- core.resolveLocalizedText(reward.title, "en"),
272
+ core.resolveLocalizedText(reward.title, locale),
134
273
  " (",
135
- state?.status ?? "LOCKED",
274
+ label(dictionary, locale, `status.${status.toLowerCase()}`, status),
136
275
  ")"
137
276
  ]
138
277
  }
@@ -142,14 +281,16 @@ function StampSheet({
142
281
  config,
143
282
  state,
144
283
  title,
145
- progress
284
+ progress,
285
+ locale = "en",
286
+ dictionary
146
287
  }) {
147
288
  const current = progress ?? core.calculateProgress(
148
289
  state ?? { rallyId: config.id, userId: null, records: [], rewards: [], updatedAt: "" },
149
290
  config
150
291
  );
151
- return /* @__PURE__ */ jsxRuntime.jsxs("section", { "aria-label": title ?? "Stamp sheet", children: [
152
- /* @__PURE__ */ jsxRuntime.jsx("h2", { children: title ?? core.resolveLocalizedText(config.title, "en") }),
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) }),
153
294
  /* @__PURE__ */ jsxRuntime.jsx("progress", { max: 100, value: current.percentage }),
154
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)) })
155
296
  ] });
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.tsx"],"names":["useState","jsxs","jsx","useEffect","useMemo","calculateProgress","useCallback","resolveLocalizedText"],"mappings":"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","file":"index.cjs","sourcesContent":["import type {\n CheckInOptions,\n CheckInResult,\n ClaimOptions,\n ClaimResult,\n LocaleDictionary,\n PublicCheckInCondition,\n PublicRallyConfig,\n PublicSpotItem,\n RallyConfig,\n Reward,\n RewardState,\n StampRallyClient,\n StampRallyProgress,\n StampRallyState,\n UserRallyState,\n} from \"@stamprally/core\";\nimport { calculateProgress, resolveLocalizedText } from \"@stamprally/core\";\nimport {\n type ComponentType,\n type FormEvent,\n type ReactElement,\n useCallback,\n useEffect,\n useMemo,\n useState,\n} from \"react\";\n\nexport interface ConditionRendererProps<TLocale extends string = string> {\n readonly spot: PublicSpotItem<TLocale>;\n readonly condition: PublicCheckInCondition;\n readonly locale: TLocale;\n readonly dictionary?: LocaleDictionary<TLocale>;\n readonly disabled: boolean;\n readonly onSubmit: (proof: unknown) => void;\n}\nexport type ConditionRenderer<TLocale extends string = string> = ComponentType<\n ConditionRendererProps<TLocale>\n>;\nexport interface RallyViewerAdapter<TLocale extends string = string> {\n readonly config: PublicRallyConfig<TLocale>;\n readonly onCheckIn: (\n spotId: string,\n proof: unknown,\n options?: CheckInOptions,\n ) => Promise<CheckInResult>;\n readonly onClaimReward?: (rewardId: string, options?: ClaimOptions) => Promise<ClaimResult>;\n}\nexport interface RallyViewerProps<TLocale extends string = string> {\n readonly config?: PublicRallyConfig<TLocale>;\n readonly client?: StampRallyClient;\n readonly adapter?: RallyViewerAdapter<TLocale>;\n readonly locale: TLocale;\n readonly dictionary?: LocaleDictionary<TLocale>;\n readonly customConditionRenderers?: Partial<\n Record<PublicCheckInCondition[\"type\"], ConditionRenderer<TLocale>>\n >;\n}\nconst label = (\n dictionary: LocaleDictionary<string> | undefined,\n locale: string,\n key: string,\n fallback: string,\n): string => dictionary?.[locale]?.[key] ?? fallback;\nfunction DefaultCondition<TLocale extends string>({\n condition,\n dictionary,\n locale,\n disabled,\n onSubmit,\n}: ConditionRendererProps<TLocale>): ReactElement {\n const [value, setValue] = useState(\"\");\n const submit = (event: FormEvent): void => {\n event.preventDefault();\n onSubmit(\n condition.type === \"gps\"\n ? { latitude: Number(value.split(\",\")[0]), longitude: Number(value.split(\",\")[1]) }\n : condition.type === \"nfc\"\n ? { tagId: value }\n : value,\n );\n };\n return (\n <form onSubmit={submit}>\n <label>\n {label(dictionary, locale, \"proof\", \"Proof\")}\n <input\n aria-label={label(dictionary, locale, \"proof\", \"Proof\")}\n value={value}\n onChange={(event) => setValue(event.target.value)}\n />\n </label>\n <button type=\"submit\" disabled={disabled}>\n {condition.type === \"gps\"\n ? label(dictionary, locale, \"checkLocation\", \"Check location\")\n : label(dictionary, locale, \"checkIn\", \"Check in\")}\n </button>\n </form>\n );\n}\nexport function RallyViewer<TLocale extends string = string>({\n config: providedConfig,\n client,\n adapter,\n locale,\n dictionary,\n customConditionRenderers = {},\n}: RallyViewerProps<TLocale>): ReactElement {\n const config = providedConfig ?? client?.getConfig() ?? adapter?.config;\n const [state, setState] = useState<UserRallyState | null>(() => client?.getState() ?? null);\n const [busy, setBusy] = useState<string | null>(null);\n useEffect(() => {\n if (client === undefined) return;\n const unsubscribe = client.subscribe(setState);\n void client.init().then(setState);\n return unsubscribe;\n }, [client]);\n if (config === undefined) throw new Error(\"RallyViewer requires config, client, or adapter.\");\n const progress = useMemo<StampRallyProgress>(\n () =>\n calculateProgress(\n state ?? { rallyId: config.id, userId: null, records: [], rewards: [], updatedAt: \"\" },\n config,\n ),\n [config, state],\n );\n const checkIn =\n adapter?.onCheckIn ??\n (client === undefined\n ? undefined\n : (spotId: string, proof: unknown, options?: CheckInOptions) =>\n client.checkIn(spotId, proof, options));\n const claim =\n adapter?.onClaimReward ??\n (client === undefined\n ? undefined\n : (rewardId: string, options?: ClaimOptions) => client.claimReward(rewardId, options));\n const submit = useCallback(\n (spotId: string, proof: unknown) => {\n if (checkIn === undefined) return;\n setBusy(spotId);\n void checkIn(spotId, proof).finally(() => setBusy(null));\n },\n [checkIn],\n );\n return (\n <section aria-label={label(dictionary, locale, \"viewer\", \"Stamp rally\")}>\n <h1>{resolveLocalizedText(config.title, locale) || config.id}</h1>\n <progress\n aria-label={label(dictionary, locale, \"progress\", \"Progress\")}\n max={100}\n value={progress.percentage}\n />{\" \"}\n <span>\n {progress.acquired}/{progress.total}\n </span>\n <div>\n {config.spots.map((spot) =>\n spot.conditions.map((condition) => {\n const Renderer = customConditionRenderers[condition.type] ?? DefaultCondition;\n return (\n <article key={`${spot.id}-${condition.type}-${JSON.stringify(condition)}`}>\n <h2>{resolveLocalizedText(spot.name, locale)}</h2>\n <Renderer\n spot={spot}\n condition={condition}\n locale={locale}\n {...(dictionary === undefined ? {} : { dictionary })}\n disabled={busy !== null}\n onSubmit={(proof) => submit(spot.id, proof)}\n />\n </article>\n );\n }),\n )}\n </div>\n <section aria-label={label(dictionary, locale, \"rewards\", \"Rewards\")}>\n {config.rewards.map((reward) => (\n <RewardButton\n key={reward.id}\n reward={reward}\n state={state?.rewards.find((item) => item.rewardId === reward.id)}\n onClaim={claim}\n />\n ))}\n </section>\n </section>\n );\n}\nfunction RewardButton({\n reward,\n state,\n onClaim,\n}: {\n readonly reward: Reward;\n readonly state: RewardState | undefined;\n readonly onClaim:\n | ((rewardId: string, options?: ClaimOptions) => Promise<ClaimResult>)\n | undefined;\n}): ReactElement {\n return (\n <button\n type=\"button\"\n disabled={state?.status !== \"AVAILABLE\" || onClaim === undefined}\n onClick={() => {\n if (onClaim !== undefined) void onClaim(reward.id);\n }}\n >\n {resolveLocalizedText(reward.title, \"en\")} ({state?.status ?? \"LOCKED\"})\n </button>\n );\n}\nexport interface StampSheetProps<TLocale extends string = string> {\n readonly config: RallyConfig<TLocale>;\n readonly state?: StampRallyState | null;\n readonly title?: string;\n readonly progress?: StampRallyProgress;\n readonly locale?: TLocale;\n}\nexport function StampSheet<TLocale extends string = string>({\n config,\n state,\n title,\n progress,\n}: StampSheetProps<TLocale>): ReactElement {\n const current =\n progress ??\n calculateProgress(\n state ?? { rallyId: config.id, userId: null, records: [], rewards: [], updatedAt: \"\" },\n config,\n );\n return (\n <section aria-label={title ?? \"Stamp sheet\"}>\n <h2>{title ?? resolveLocalizedText(config.title, \"en\")}</h2>\n <progress max={100} value={current.percentage} />\n <div>\n {config.spots.map((spot) => (\n <span key={spot.id}>\n {state?.records.some((record) => record.stampId === spot.id) ? \"✓\" : \"○\"}\n </span>\n ))}\n </div>\n </section>\n );\n}\n"]}
1
+ {"version":3,"sources":["../src/index.tsx"],"names":["useState","useRef","readQrContext","getCurrentGeoContext","readNfcContext","jsxs","Fragment","jsx","isQrSupported","isNfcSupported","useEffect","useMemo","calculateProgress","useCallback","resolveLocalizedText"],"mappings":"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","file":"index.cjs","sourcesContent":["import type {\n CheckInOptions,\n CheckInResult,\n ClaimOptions,\n ClaimResult,\n LocaleDictionary,\n PublicCheckInCondition,\n PublicRallyConfig,\n PublicSpotItem,\n RallyConfig,\n Reward,\n RewardState,\n StampRallyClient,\n StampRallyProgress,\n StampRallyState,\n UserRallyState,\n} from \"@stamprally/core\";\nimport {\n calculateProgress,\n getCurrentGeoContext,\n isNfcSupported,\n isQrSupported,\n readNfcContext,\n readQrContext,\n resolveLocalizedText,\n} from \"@stamprally/core\";\nimport {\n type ComponentType,\n type CSSProperties,\n type FormEvent,\n type ReactElement,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\n\nexport interface ConditionRendererProps<TLocale extends string = string> {\n readonly spot: PublicSpotItem<TLocale>;\n readonly condition: PublicCheckInCondition;\n readonly locale: TLocale;\n readonly dictionary?: LocaleDictionary<TLocale>;\n readonly disabled: boolean;\n // biome-ignore lint/suspicious/noConfusingVoidType: custom renderers may fire-and-forget while built-ins await feedback.\n readonly onSubmit: (proof: unknown) => void | Promise<unknown>;\n}\nexport type ConditionRenderer<TLocale extends string = string> = ComponentType<\n ConditionRendererProps<TLocale>\n>;\nexport type ViewerClassName = \"root\" | \"condition\" | \"action\" | \"feedback\" | \"reward\";\nexport type ViewerStyle = CSSProperties & {\n readonly [key: `--${string}`]: string | number | undefined;\n};\n\nexport interface RallyViewerAdapter<TLocale extends string = string> {\n readonly config: PublicRallyConfig<TLocale>;\n readonly onCheckIn: (\n spotId: string,\n proof: unknown,\n options?: CheckInOptions,\n ) => Promise<CheckInResult>;\n readonly onClaimReward?: (rewardId: string, options?: ClaimOptions) => Promise<ClaimResult>;\n}\nexport interface RallyViewerProps<TLocale extends string = string> {\n readonly config?: PublicRallyConfig<TLocale>;\n readonly client?: StampRallyClient;\n readonly adapter?: RallyViewerAdapter<TLocale>;\n readonly locale: TLocale;\n readonly dictionary?: LocaleDictionary<TLocale>;\n readonly classNames?: Partial<Record<ViewerClassName, string>>;\n readonly style?: ViewerStyle;\n readonly customConditionRenderers?: Partial<\n Record<PublicCheckInCondition[\"type\"], ConditionRenderer<TLocale>>\n >;\n}\n\nconst label = <TLocale extends string>(\n dictionary: LocaleDictionary<TLocale> | undefined,\n locale: TLocale,\n key: string,\n fallback: string,\n): string => dictionary?.[locale]?.[key] ?? fallback;\n\ntype VerificationStatus = \"idle\" | \"loading\" | \"success\" | \"error\";\n\nfunction DefaultCondition<TLocale extends string>({\n condition,\n dictionary,\n locale,\n disabled,\n onSubmit,\n}: ConditionRendererProps<TLocale>): ReactElement {\n const [value, setValue] = useState(\"\");\n const [status, setStatus] = useState<VerificationStatus>(\"idle\");\n const [error, setError] = useState<string | null>(null);\n const videoRef = useRef<HTMLVideoElement>(null);\n const text = (key: string, fallback: string): string => label(dictionary, locale, key, fallback);\n const verify = async (proof: unknown): Promise<void> => {\n setStatus(\"loading\");\n setError(null);\n try {\n const result = await onSubmit(proof);\n if (\n result !== undefined &&\n typeof result === \"object\" &&\n result !== null &&\n \"ok\" in result &&\n result.ok === false\n ) {\n setStatus(\"error\");\n setError(text(\"verificationFailed\", \"Verification failed.\"));\n } else setStatus(\"success\");\n } catch {\n setStatus(\"error\");\n setError(text(\"verificationFailed\", \"Verification failed.\"));\n }\n };\n const submit = (event: FormEvent): void => {\n event.preventDefault();\n if (condition.type === \"gps\") return;\n if (condition.type === \"nfc\") return;\n void verify(condition.type === \"passcode\" ? value : value);\n };\n const scanQr = (): void => {\n if (videoRef.current === null) return;\n setStatus(\"loading\");\n setError(null);\n void readQrContext(videoRef.current).then((result) => {\n if (result.ok) void verify(result.value);\n else {\n setStatus(\"error\");\n setError(result.error.message);\n }\n });\n };\n const readLocation = (): void => {\n setStatus(\"loading\");\n setError(null);\n void getCurrentGeoContext().then((result) => {\n if (result.ok) void verify(result.value);\n else {\n setStatus(\"error\");\n setError(result.error.message);\n }\n });\n };\n const readNfc = (): void => {\n setStatus(\"loading\");\n setError(null);\n void readNfcContext().then((result) => {\n if (result.ok) void verify(result.value);\n else {\n setStatus(\"error\");\n setError(result.error.message);\n }\n });\n };\n return (\n <div>\n {condition.type === \"qr\" && (\n <>\n <label>\n {text(\"qrValue\", \"QR value\")}\n <input\n aria-label={text(\"qrValue\", \"QR value\")}\n placeholder={condition.qrEntryUrl ?? text(\"qrPlaceholder\", \"Paste a QR value\")}\n value={value}\n onChange={(event) => setValue(event.target.value)}\n />\n </label>\n <video\n ref={videoRef}\n aria-label={text(\"qrCamera\", \"QR camera\")}\n hidden={!isQrSupported()}\n >\n <track kind=\"captions\" />\n </video>\n <button\n type=\"button\"\n className=\"sry-action\"\n disabled={disabled || !isQrSupported()}\n onClick={scanQr}\n >\n {text(\"scanQr\", \"Scan QR\")}\n </button>\n </>\n )}\n {condition.type === \"gps\" && (\n <button type=\"button\" className=\"sry-action\" disabled={disabled} onClick={readLocation}>\n {text(\"checkLocation\", \"Check location\")}\n </button>\n )}\n {condition.type === \"nfc\" && (\n <button\n type=\"button\"\n className=\"sry-action\"\n disabled={disabled || !isNfcSupported()}\n onClick={readNfc}\n >\n {text(\"readNfc\", \"Read NFC\")}\n </button>\n )}\n {(condition.type === \"passcode\" ||\n condition.type === \"custom\" ||\n condition.type === \"qr\") && (\n <form onSubmit={submit}>\n <label>\n {text(\n condition.type === \"passcode\" ? \"passcode\" : \"proof\",\n condition.type === \"passcode\" ? \"Passcode\" : \"Proof\",\n )}\n <input\n aria-label={text(\"proof\", \"Proof\")}\n value={value}\n onChange={(event) => setValue(event.target.value)}\n />\n </label>\n <button type=\"submit\" className=\"sry-action\" disabled={disabled || value.trim() === \"\"}>\n {text(\"checkIn\", \"Check in\")}\n </button>\n </form>\n )}\n <div\n className=\"sry-feedback\"\n aria-live=\"polite\"\n role={status === \"error\" ? \"alert\" : undefined}\n >\n {status === \"loading\" && text(\"verifying\", \"Verifying…\")}\n {status === \"success\" && text(\"verified\", \"Verified\")}\n {status === \"error\" && (error ?? text(\"verificationFailed\", \"Verification failed.\"))}\n </div>\n </div>\n );\n}\n\nexport function RallyViewer<TLocale extends string = string>({\n config: providedConfig,\n client,\n adapter,\n locale,\n dictionary,\n classNames = {},\n style,\n customConditionRenderers = {},\n}: RallyViewerProps<TLocale>): ReactElement {\n const config = providedConfig ?? client?.getConfig() ?? adapter?.config;\n const [state, setState] = useState<UserRallyState | null>(() => client?.getState() ?? null);\n const [busy, setBusy] = useState<string | null>(null);\n useEffect(() => {\n if (client === undefined) return;\n const unsubscribe = client.subscribe(setState);\n void client.init().then(setState);\n return unsubscribe;\n }, [client]);\n if (config === undefined) throw new Error(\"RallyViewer requires config, client, or adapter.\");\n const progress = useMemo<StampRallyProgress>(\n () =>\n calculateProgress(\n state ?? { rallyId: config.id, userId: null, records: [], rewards: [], updatedAt: \"\" },\n config,\n ),\n [config, state],\n );\n const checkIn =\n adapter?.onCheckIn ??\n (client === undefined\n ? undefined\n : (spotId: string, proof: unknown, options?: CheckInOptions) =>\n client.checkIn(spotId, proof, options));\n const claim =\n adapter?.onClaimReward ??\n (client === undefined\n ? undefined\n : (rewardId: string, options?: ClaimOptions) => client.claimReward(rewardId, options));\n const submit = useCallback(\n (spotId: string, proof: unknown): Promise<CheckInResult | undefined> => {\n if (checkIn === undefined) return Promise.resolve(undefined);\n setBusy(spotId);\n return checkIn(spotId, proof).finally(() => setBusy(null));\n },\n [checkIn],\n );\n return (\n <section\n className={classNames.root}\n style={style}\n aria-label={label(dictionary, locale, \"viewer\", \"Stamp rally\")}\n >\n <h1>{resolveLocalizedText(config.title, locale) || config.id}</h1>\n <progress\n aria-label={label(dictionary, locale, \"progress\", \"Progress\")}\n max={100}\n value={progress.percentage}\n />{\" \"}\n <span>\n {progress.acquired}/{progress.total}\n </span>\n <div>\n {config.spots.map((spot) =>\n spot.conditions.map((condition) => {\n const Renderer = customConditionRenderers[condition.type] ?? DefaultCondition;\n return (\n <article\n className={classNames.condition}\n key={`${spot.id}-${condition.type}-${JSON.stringify(condition)}`}\n >\n <h2>{resolveLocalizedText(spot.name, locale)}</h2>\n <Renderer\n spot={spot}\n condition={condition}\n locale={locale}\n {...(dictionary === undefined ? {} : { dictionary })}\n disabled={busy !== null}\n onSubmit={(proof) => submit(spot.id, proof)}\n />\n </article>\n );\n }),\n )}\n </div>\n <section aria-label={label(dictionary, locale, \"rewards\", \"Rewards\")}>\n {config.rewards.map((reward) => (\n <RewardButton\n key={reward.id}\n reward={reward}\n state={state?.rewards.find((item) => item.rewardId === reward.id)}\n locale={locale}\n {...(dictionary === undefined ? {} : { dictionary })}\n {...(classNames.reward === undefined ? {} : { className: classNames.reward })}\n onClaim={claim}\n />\n ))}\n </section>\n </section>\n );\n}\n\nfunction RewardButton<TLocale extends string>({\n reward,\n state,\n locale,\n dictionary,\n className,\n onClaim,\n}: {\n readonly reward: Reward<TLocale>;\n readonly state: RewardState | undefined;\n readonly locale: TLocale;\n readonly dictionary?: LocaleDictionary<TLocale>;\n readonly className?: string;\n readonly onClaim:\n | ((rewardId: string, options?: ClaimOptions) => Promise<ClaimResult>)\n | undefined;\n}): ReactElement {\n const status = state?.status ?? \"LOCKED\";\n return (\n <button\n type=\"button\"\n className={className}\n disabled={status !== \"AVAILABLE\" || onClaim === undefined}\n onClick={() => {\n if (onClaim !== undefined) void onClaim(reward.id);\n }}\n >\n {resolveLocalizedText(reward.title, locale)} (\n {label(dictionary, locale, `status.${status.toLowerCase()}`, status)})\n </button>\n );\n}\n\nexport interface StampSheetProps<TLocale extends string = string> {\n readonly config: RallyConfig<TLocale>;\n readonly state?: StampRallyState | null;\n readonly title?: string;\n readonly progress?: StampRallyProgress;\n readonly locale?: TLocale;\n readonly dictionary?: LocaleDictionary<TLocale>;\n}\nexport function StampSheet<TLocale extends string = string>({\n config,\n state,\n title,\n progress,\n locale = \"en\" as TLocale,\n dictionary,\n}: StampSheetProps<TLocale>): ReactElement {\n const current =\n progress ??\n calculateProgress(\n state ?? { rallyId: config.id, userId: null, records: [], rewards: [], updatedAt: \"\" },\n config,\n );\n return (\n <section aria-label={title ?? label(dictionary, locale, \"stampSheet\", \"Stamp sheet\")}>\n <h2>{title ?? resolveLocalizedText(config.title, locale)}</h2>\n <progress max={100} value={current.percentage} />\n <div>\n {config.spots.map((spot) => (\n <span key={spot.id}>\n {state?.records.some((record) => record.stampId === spot.id) ? \"✓\" : \"○\"}\n </span>\n ))}\n </div>\n </section>\n );\n}\n"]}
package/dist/index.css CHANGED
@@ -1,3 +1,11 @@
1
+ .sry-feedback {
2
+ min-height: 1.5rem;
3
+ color: var(--stamprally-feedback-color, currentColor);
4
+ }
5
+ .sry-action {
6
+ margin-block: 0.35rem;
7
+ }
8
+
1
9
  .stamp-sheet,
2
10
  :where(.stamprally-sheet, .stamprally-rewards) {
3
11
  color: var(--stamprally-text, #352f25);
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { PublicSpotItem, PublicCheckInCondition, LocaleDictionary, PublicRallyConfig, StampRallyClient, CheckInOptions, CheckInResult, ClaimOptions, ClaimResult, RallyConfig, StampRallyState, StampRallyProgress } from '@stamprally/core';
2
- import { ComponentType, ReactElement } from 'react';
2
+ import { ComponentType, CSSProperties, ReactElement } from 'react';
3
3
 
4
4
  interface ConditionRendererProps<TLocale extends string = string> {
5
5
  readonly spot: PublicSpotItem<TLocale>;
@@ -7,9 +7,13 @@ interface ConditionRendererProps<TLocale extends string = string> {
7
7
  readonly locale: TLocale;
8
8
  readonly dictionary?: LocaleDictionary<TLocale>;
9
9
  readonly disabled: boolean;
10
- readonly onSubmit: (proof: unknown) => void;
10
+ readonly onSubmit: (proof: unknown) => void | Promise<unknown>;
11
11
  }
12
12
  type ConditionRenderer<TLocale extends string = string> = ComponentType<ConditionRendererProps<TLocale>>;
13
+ type ViewerClassName = "root" | "condition" | "action" | "feedback" | "reward";
14
+ type ViewerStyle = CSSProperties & {
15
+ readonly [key: `--${string}`]: string | number | undefined;
16
+ };
13
17
  interface RallyViewerAdapter<TLocale extends string = string> {
14
18
  readonly config: PublicRallyConfig<TLocale>;
15
19
  readonly onCheckIn: (spotId: string, proof: unknown, options?: CheckInOptions) => Promise<CheckInResult>;
@@ -21,16 +25,19 @@ interface RallyViewerProps<TLocale extends string = string> {
21
25
  readonly adapter?: RallyViewerAdapter<TLocale>;
22
26
  readonly locale: TLocale;
23
27
  readonly dictionary?: LocaleDictionary<TLocale>;
28
+ readonly classNames?: Partial<Record<ViewerClassName, string>>;
29
+ readonly style?: ViewerStyle;
24
30
  readonly customConditionRenderers?: Partial<Record<PublicCheckInCondition["type"], ConditionRenderer<TLocale>>>;
25
31
  }
26
- declare function RallyViewer<TLocale extends string = string>({ config: providedConfig, client, adapter, locale, dictionary, customConditionRenderers, }: RallyViewerProps<TLocale>): ReactElement;
32
+ declare function RallyViewer<TLocale extends string = string>({ config: providedConfig, client, adapter, locale, dictionary, classNames, style, customConditionRenderers, }: RallyViewerProps<TLocale>): ReactElement;
27
33
  interface StampSheetProps<TLocale extends string = string> {
28
34
  readonly config: RallyConfig<TLocale>;
29
35
  readonly state?: StampRallyState | null;
30
36
  readonly title?: string;
31
37
  readonly progress?: StampRallyProgress;
32
38
  readonly locale?: TLocale;
39
+ readonly dictionary?: LocaleDictionary<TLocale>;
33
40
  }
34
- declare function StampSheet<TLocale extends string = string>({ config, state, title, progress, }: StampSheetProps<TLocale>): ReactElement;
41
+ declare function StampSheet<TLocale extends string = string>({ config, state, title, progress, locale, dictionary, }: StampSheetProps<TLocale>): ReactElement;
35
42
 
36
- export { type ConditionRenderer, type ConditionRendererProps, RallyViewer, type RallyViewerAdapter, type RallyViewerProps, StampSheet, type StampSheetProps };
43
+ export { type ConditionRenderer, type ConditionRendererProps, RallyViewer, type RallyViewerAdapter, type RallyViewerProps, StampSheet, type StampSheetProps, type ViewerClassName, type ViewerStyle };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { PublicSpotItem, PublicCheckInCondition, LocaleDictionary, PublicRallyConfig, StampRallyClient, CheckInOptions, CheckInResult, ClaimOptions, ClaimResult, RallyConfig, StampRallyState, StampRallyProgress } from '@stamprally/core';
2
- import { ComponentType, ReactElement } from 'react';
2
+ import { ComponentType, CSSProperties, ReactElement } from 'react';
3
3
 
4
4
  interface ConditionRendererProps<TLocale extends string = string> {
5
5
  readonly spot: PublicSpotItem<TLocale>;
@@ -7,9 +7,13 @@ interface ConditionRendererProps<TLocale extends string = string> {
7
7
  readonly locale: TLocale;
8
8
  readonly dictionary?: LocaleDictionary<TLocale>;
9
9
  readonly disabled: boolean;
10
- readonly onSubmit: (proof: unknown) => void;
10
+ readonly onSubmit: (proof: unknown) => void | Promise<unknown>;
11
11
  }
12
12
  type ConditionRenderer<TLocale extends string = string> = ComponentType<ConditionRendererProps<TLocale>>;
13
+ type ViewerClassName = "root" | "condition" | "action" | "feedback" | "reward";
14
+ type ViewerStyle = CSSProperties & {
15
+ readonly [key: `--${string}`]: string | number | undefined;
16
+ };
13
17
  interface RallyViewerAdapter<TLocale extends string = string> {
14
18
  readonly config: PublicRallyConfig<TLocale>;
15
19
  readonly onCheckIn: (spotId: string, proof: unknown, options?: CheckInOptions) => Promise<CheckInResult>;
@@ -21,16 +25,19 @@ interface RallyViewerProps<TLocale extends string = string> {
21
25
  readonly adapter?: RallyViewerAdapter<TLocale>;
22
26
  readonly locale: TLocale;
23
27
  readonly dictionary?: LocaleDictionary<TLocale>;
28
+ readonly classNames?: Partial<Record<ViewerClassName, string>>;
29
+ readonly style?: ViewerStyle;
24
30
  readonly customConditionRenderers?: Partial<Record<PublicCheckInCondition["type"], ConditionRenderer<TLocale>>>;
25
31
  }
26
- declare function RallyViewer<TLocale extends string = string>({ config: providedConfig, client, adapter, locale, dictionary, customConditionRenderers, }: RallyViewerProps<TLocale>): ReactElement;
32
+ declare function RallyViewer<TLocale extends string = string>({ config: providedConfig, client, adapter, locale, dictionary, classNames, style, customConditionRenderers, }: RallyViewerProps<TLocale>): ReactElement;
27
33
  interface StampSheetProps<TLocale extends string = string> {
28
34
  readonly config: RallyConfig<TLocale>;
29
35
  readonly state?: StampRallyState | null;
30
36
  readonly title?: string;
31
37
  readonly progress?: StampRallyProgress;
32
38
  readonly locale?: TLocale;
39
+ readonly dictionary?: LocaleDictionary<TLocale>;
33
40
  }
34
- declare function StampSheet<TLocale extends string = string>({ config, state, title, progress, }: StampSheetProps<TLocale>): ReactElement;
41
+ declare function StampSheet<TLocale extends string = string>({ config, state, title, progress, locale, dictionary, }: StampSheetProps<TLocale>): ReactElement;
35
42
 
36
- export { type ConditionRenderer, type ConditionRendererProps, RallyViewer, type RallyViewerAdapter, type RallyViewerProps, StampSheet, type StampSheetProps };
43
+ export { type ConditionRenderer, type ConditionRendererProps, RallyViewer, type RallyViewerAdapter, type RallyViewerProps, StampSheet, type StampSheetProps, type ViewerClassName, type ViewerStyle };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import { calculateProgress, resolveLocalizedText } from '@stamprally/core';
2
- import { useState, useEffect, useMemo, useCallback } from 'react';
3
- import { jsxs, jsx } from 'react/jsx-runtime';
1
+ import { calculateProgress, resolveLocalizedText, isQrSupported, isNfcSupported, readQrContext, getCurrentGeoContext, readNfcContext } from '@stamprally/core';
2
+ import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
3
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
4
4
 
5
5
  // src/index.tsx
6
6
  var label = (dictionary, locale, key, fallback) => dictionary?.[locale]?.[key] ?? fallback;
@@ -12,25 +12,139 @@ function DefaultCondition({
12
12
  onSubmit
13
13
  }) {
14
14
  const [value, setValue] = useState("");
15
+ const [status, setStatus] = useState("idle");
16
+ const [error, setError] = useState(null);
17
+ const videoRef = useRef(null);
18
+ const text = (key, fallback) => label(dictionary, locale, key, fallback);
19
+ const verify = async (proof) => {
20
+ setStatus("loading");
21
+ setError(null);
22
+ try {
23
+ const result = await onSubmit(proof);
24
+ if (result !== void 0 && typeof result === "object" && result !== null && "ok" in result && result.ok === false) {
25
+ setStatus("error");
26
+ setError(text("verificationFailed", "Verification failed."));
27
+ } else setStatus("success");
28
+ } catch {
29
+ setStatus("error");
30
+ setError(text("verificationFailed", "Verification failed."));
31
+ }
32
+ };
15
33
  const submit = (event) => {
16
34
  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
- );
35
+ if (condition.type === "gps") return;
36
+ if (condition.type === "nfc") return;
37
+ void verify(condition.type === "passcode" ? value : value);
38
+ };
39
+ const scanQr = () => {
40
+ if (videoRef.current === null) return;
41
+ setStatus("loading");
42
+ setError(null);
43
+ void readQrContext(videoRef.current).then((result) => {
44
+ if (result.ok) void verify(result.value);
45
+ else {
46
+ setStatus("error");
47
+ setError(result.error.message);
48
+ }
49
+ });
50
+ };
51
+ const readLocation = () => {
52
+ setStatus("loading");
53
+ setError(null);
54
+ void getCurrentGeoContext().then((result) => {
55
+ if (result.ok) void verify(result.value);
56
+ else {
57
+ setStatus("error");
58
+ setError(result.error.message);
59
+ }
60
+ });
61
+ };
62
+ const readNfc = () => {
63
+ setStatus("loading");
64
+ setError(null);
65
+ void readNfcContext().then((result) => {
66
+ if (result.ok) void verify(result.value);
67
+ else {
68
+ setStatus("error");
69
+ setError(result.error.message);
70
+ }
71
+ });
20
72
  };
21
- return /* @__PURE__ */ jsxs("form", { onSubmit: submit, children: [
22
- /* @__PURE__ */ jsxs("label", { children: [
23
- label(dictionary, locale, "proof", "Proof"),
73
+ return /* @__PURE__ */ jsxs("div", { children: [
74
+ condition.type === "qr" && /* @__PURE__ */ jsxs(Fragment, { children: [
75
+ /* @__PURE__ */ jsxs("label", { children: [
76
+ text("qrValue", "QR value"),
77
+ /* @__PURE__ */ jsx(
78
+ "input",
79
+ {
80
+ "aria-label": text("qrValue", "QR value"),
81
+ placeholder: condition.qrEntryUrl ?? text("qrPlaceholder", "Paste a QR value"),
82
+ value,
83
+ onChange: (event) => setValue(event.target.value)
84
+ }
85
+ )
86
+ ] }),
24
87
  /* @__PURE__ */ jsx(
25
- "input",
88
+ "video",
26
89
  {
27
- "aria-label": label(dictionary, locale, "proof", "Proof"),
28
- value,
29
- onChange: (event) => setValue(event.target.value)
90
+ ref: videoRef,
91
+ "aria-label": text("qrCamera", "QR camera"),
92
+ hidden: !isQrSupported(),
93
+ children: /* @__PURE__ */ jsx("track", { kind: "captions" })
94
+ }
95
+ ),
96
+ /* @__PURE__ */ jsx(
97
+ "button",
98
+ {
99
+ type: "button",
100
+ className: "sry-action",
101
+ disabled: disabled || !isQrSupported(),
102
+ onClick: scanQr,
103
+ children: text("scanQr", "Scan QR")
30
104
  }
31
105
  )
32
106
  ] }),
33
- /* @__PURE__ */ jsx("button", { type: "submit", disabled, children: condition.type === "gps" ? label(dictionary, locale, "checkLocation", "Check location") : label(dictionary, locale, "checkIn", "Check in") })
107
+ condition.type === "gps" && /* @__PURE__ */ jsx("button", { type: "button", className: "sry-action", disabled, onClick: readLocation, children: text("checkLocation", "Check location") }),
108
+ condition.type === "nfc" && /* @__PURE__ */ jsx(
109
+ "button",
110
+ {
111
+ type: "button",
112
+ className: "sry-action",
113
+ disabled: disabled || !isNfcSupported(),
114
+ onClick: readNfc,
115
+ children: text("readNfc", "Read NFC")
116
+ }
117
+ ),
118
+ (condition.type === "passcode" || condition.type === "custom" || condition.type === "qr") && /* @__PURE__ */ jsxs("form", { onSubmit: submit, children: [
119
+ /* @__PURE__ */ jsxs("label", { children: [
120
+ text(
121
+ condition.type === "passcode" ? "passcode" : "proof",
122
+ condition.type === "passcode" ? "Passcode" : "Proof"
123
+ ),
124
+ /* @__PURE__ */ jsx(
125
+ "input",
126
+ {
127
+ "aria-label": text("proof", "Proof"),
128
+ value,
129
+ onChange: (event) => setValue(event.target.value)
130
+ }
131
+ )
132
+ ] }),
133
+ /* @__PURE__ */ jsx("button", { type: "submit", className: "sry-action", disabled: disabled || value.trim() === "", children: text("checkIn", "Check in") })
134
+ ] }),
135
+ /* @__PURE__ */ jsxs(
136
+ "div",
137
+ {
138
+ className: "sry-feedback",
139
+ "aria-live": "polite",
140
+ role: status === "error" ? "alert" : void 0,
141
+ children: [
142
+ status === "loading" && text("verifying", "Verifying\u2026"),
143
+ status === "success" && text("verified", "Verified"),
144
+ status === "error" && (error ?? text("verificationFailed", "Verification failed."))
145
+ ]
146
+ }
147
+ )
34
148
  ] });
35
149
  }
36
150
  function RallyViewer({
@@ -39,6 +153,8 @@ function RallyViewer({
39
153
  adapter,
40
154
  locale,
41
155
  dictionary,
156
+ classNames = {},
157
+ style,
42
158
  customConditionRenderers = {}
43
159
  }) {
44
160
  const config = providedConfig ?? client?.getConfig() ?? adapter?.config;
@@ -62,75 +178,98 @@ function RallyViewer({
62
178
  const claim = adapter?.onClaimReward ?? (client === void 0 ? void 0 : (rewardId, options) => client.claimReward(rewardId, options));
63
179
  const submit = useCallback(
64
180
  (spotId, proof) => {
65
- if (checkIn === void 0) return;
181
+ if (checkIn === void 0) return Promise.resolve(void 0);
66
182
  setBusy(spotId);
67
- void checkIn(spotId, proof).finally(() => setBusy(null));
183
+ return checkIn(spotId, proof).finally(() => setBusy(null));
68
184
  },
69
185
  [checkIn]
70
186
  );
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
86
- ] }),
87
- /* @__PURE__ */ jsx("div", { children: config.spots.map(
88
- (spot) => spot.conditions.map((condition) => {
89
- const Renderer = customConditionRenderers[condition.type] ?? DefaultCondition;
90
- return /* @__PURE__ */ jsxs("article", { children: [
91
- /* @__PURE__ */ jsx("h2", { children: resolveLocalizedText(spot.name, locale) }),
92
- /* @__PURE__ */ jsx(
93
- Renderer,
94
- {
95
- spot,
96
- condition,
97
- locale,
98
- ...dictionary === void 0 ? {} : { dictionary },
99
- disabled: busy !== null,
100
- onSubmit: (proof) => submit(spot.id, proof)
101
- }
102
- )
103
- ] }, `${spot.id}-${condition.type}-${JSON.stringify(condition)}`);
104
- })
105
- ) }),
106
- /* @__PURE__ */ jsx("section", { "aria-label": label(dictionary, locale, "rewards", "Rewards"), children: config.rewards.map((reward) => /* @__PURE__ */ jsx(
107
- RewardButton,
108
- {
109
- reward,
110
- state: state?.rewards.find((item) => item.rewardId === reward.id),
111
- onClaim: claim
112
- },
113
- reward.id
114
- )) })
115
- ] });
187
+ return /* @__PURE__ */ jsxs(
188
+ "section",
189
+ {
190
+ className: classNames.root,
191
+ style,
192
+ "aria-label": label(dictionary, locale, "viewer", "Stamp rally"),
193
+ children: [
194
+ /* @__PURE__ */ jsx("h1", { children: resolveLocalizedText(config.title, locale) || config.id }),
195
+ /* @__PURE__ */ jsx(
196
+ "progress",
197
+ {
198
+ "aria-label": label(dictionary, locale, "progress", "Progress"),
199
+ max: 100,
200
+ value: progress.percentage
201
+ }
202
+ ),
203
+ " ",
204
+ /* @__PURE__ */ jsxs("span", { children: [
205
+ progress.acquired,
206
+ "/",
207
+ progress.total
208
+ ] }),
209
+ /* @__PURE__ */ jsx("div", { children: config.spots.map(
210
+ (spot) => spot.conditions.map((condition) => {
211
+ const Renderer = customConditionRenderers[condition.type] ?? DefaultCondition;
212
+ return /* @__PURE__ */ jsxs(
213
+ "article",
214
+ {
215
+ className: classNames.condition,
216
+ children: [
217
+ /* @__PURE__ */ jsx("h2", { children: resolveLocalizedText(spot.name, locale) }),
218
+ /* @__PURE__ */ jsx(
219
+ Renderer,
220
+ {
221
+ spot,
222
+ condition,
223
+ locale,
224
+ ...dictionary === void 0 ? {} : { dictionary },
225
+ disabled: busy !== null,
226
+ onSubmit: (proof) => submit(spot.id, proof)
227
+ }
228
+ )
229
+ ]
230
+ },
231
+ `${spot.id}-${condition.type}-${JSON.stringify(condition)}`
232
+ );
233
+ })
234
+ ) }),
235
+ /* @__PURE__ */ jsx("section", { "aria-label": label(dictionary, locale, "rewards", "Rewards"), children: config.rewards.map((reward) => /* @__PURE__ */ jsx(
236
+ RewardButton,
237
+ {
238
+ reward,
239
+ state: state?.rewards.find((item) => item.rewardId === reward.id),
240
+ locale,
241
+ ...dictionary === void 0 ? {} : { dictionary },
242
+ ...classNames.reward === void 0 ? {} : { className: classNames.reward },
243
+ onClaim: claim
244
+ },
245
+ reward.id
246
+ )) })
247
+ ]
248
+ }
249
+ );
116
250
  }
117
251
  function RewardButton({
118
252
  reward,
119
253
  state,
254
+ locale,
255
+ dictionary,
256
+ className,
120
257
  onClaim
121
258
  }) {
259
+ const status = state?.status ?? "LOCKED";
122
260
  return /* @__PURE__ */ jsxs(
123
261
  "button",
124
262
  {
125
263
  type: "button",
126
- disabled: state?.status !== "AVAILABLE" || onClaim === void 0,
264
+ className,
265
+ disabled: status !== "AVAILABLE" || onClaim === void 0,
127
266
  onClick: () => {
128
267
  if (onClaim !== void 0) void onClaim(reward.id);
129
268
  },
130
269
  children: [
131
- resolveLocalizedText(reward.title, "en"),
270
+ resolveLocalizedText(reward.title, locale),
132
271
  " (",
133
- state?.status ?? "LOCKED",
272
+ label(dictionary, locale, `status.${status.toLowerCase()}`, status),
134
273
  ")"
135
274
  ]
136
275
  }
@@ -140,14 +279,16 @@ function StampSheet({
140
279
  config,
141
280
  state,
142
281
  title,
143
- progress
282
+ progress,
283
+ locale = "en",
284
+ dictionary
144
285
  }) {
145
286
  const current = progress ?? calculateProgress(
146
287
  state ?? { rallyId: config.id, userId: null, records: [], rewards: [], updatedAt: "" },
147
288
  config
148
289
  );
149
- return /* @__PURE__ */ jsxs("section", { "aria-label": title ?? "Stamp sheet", children: [
150
- /* @__PURE__ */ jsx("h2", { children: title ?? resolveLocalizedText(config.title, "en") }),
290
+ return /* @__PURE__ */ jsxs("section", { "aria-label": title ?? label(dictionary, locale, "stampSheet", "Stamp sheet"), children: [
291
+ /* @__PURE__ */ jsx("h2", { children: title ?? resolveLocalizedText(config.title, locale) }),
151
292
  /* @__PURE__ */ jsx("progress", { max: 100, value: current.percentage }),
152
293
  /* @__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
294
  ] });
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.tsx"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type {\n CheckInOptions,\n CheckInResult,\n ClaimOptions,\n ClaimResult,\n LocaleDictionary,\n PublicCheckInCondition,\n PublicRallyConfig,\n PublicSpotItem,\n RallyConfig,\n Reward,\n RewardState,\n StampRallyClient,\n StampRallyProgress,\n StampRallyState,\n UserRallyState,\n} from \"@stamprally/core\";\nimport { calculateProgress, resolveLocalizedText } from \"@stamprally/core\";\nimport {\n type ComponentType,\n type FormEvent,\n type ReactElement,\n useCallback,\n useEffect,\n useMemo,\n useState,\n} from \"react\";\n\nexport interface ConditionRendererProps<TLocale extends string = string> {\n readonly spot: PublicSpotItem<TLocale>;\n readonly condition: PublicCheckInCondition;\n readonly locale: TLocale;\n readonly dictionary?: LocaleDictionary<TLocale>;\n readonly disabled: boolean;\n readonly onSubmit: (proof: unknown) => void;\n}\nexport type ConditionRenderer<TLocale extends string = string> = ComponentType<\n ConditionRendererProps<TLocale>\n>;\nexport interface RallyViewerAdapter<TLocale extends string = string> {\n readonly config: PublicRallyConfig<TLocale>;\n readonly onCheckIn: (\n spotId: string,\n proof: unknown,\n options?: CheckInOptions,\n ) => Promise<CheckInResult>;\n readonly onClaimReward?: (rewardId: string, options?: ClaimOptions) => Promise<ClaimResult>;\n}\nexport interface RallyViewerProps<TLocale extends string = string> {\n readonly config?: PublicRallyConfig<TLocale>;\n readonly client?: StampRallyClient;\n readonly adapter?: RallyViewerAdapter<TLocale>;\n readonly locale: TLocale;\n readonly dictionary?: LocaleDictionary<TLocale>;\n readonly customConditionRenderers?: Partial<\n Record<PublicCheckInCondition[\"type\"], ConditionRenderer<TLocale>>\n >;\n}\nconst label = (\n dictionary: LocaleDictionary<string> | undefined,\n locale: string,\n key: string,\n fallback: string,\n): string => dictionary?.[locale]?.[key] ?? fallback;\nfunction DefaultCondition<TLocale extends string>({\n condition,\n dictionary,\n locale,\n disabled,\n onSubmit,\n}: ConditionRendererProps<TLocale>): ReactElement {\n const [value, setValue] = useState(\"\");\n const submit = (event: FormEvent): void => {\n event.preventDefault();\n onSubmit(\n condition.type === \"gps\"\n ? { latitude: Number(value.split(\",\")[0]), longitude: Number(value.split(\",\")[1]) }\n : condition.type === \"nfc\"\n ? { tagId: value }\n : value,\n );\n };\n return (\n <form onSubmit={submit}>\n <label>\n {label(dictionary, locale, \"proof\", \"Proof\")}\n <input\n aria-label={label(dictionary, locale, \"proof\", \"Proof\")}\n value={value}\n onChange={(event) => setValue(event.target.value)}\n />\n </label>\n <button type=\"submit\" disabled={disabled}>\n {condition.type === \"gps\"\n ? label(dictionary, locale, \"checkLocation\", \"Check location\")\n : label(dictionary, locale, \"checkIn\", \"Check in\")}\n </button>\n </form>\n );\n}\nexport function RallyViewer<TLocale extends string = string>({\n config: providedConfig,\n client,\n adapter,\n locale,\n dictionary,\n customConditionRenderers = {},\n}: RallyViewerProps<TLocale>): ReactElement {\n const config = providedConfig ?? client?.getConfig() ?? adapter?.config;\n const [state, setState] = useState<UserRallyState | null>(() => client?.getState() ?? null);\n const [busy, setBusy] = useState<string | null>(null);\n useEffect(() => {\n if (client === undefined) return;\n const unsubscribe = client.subscribe(setState);\n void client.init().then(setState);\n return unsubscribe;\n }, [client]);\n if (config === undefined) throw new Error(\"RallyViewer requires config, client, or adapter.\");\n const progress = useMemo<StampRallyProgress>(\n () =>\n calculateProgress(\n state ?? { rallyId: config.id, userId: null, records: [], rewards: [], updatedAt: \"\" },\n config,\n ),\n [config, state],\n );\n const checkIn =\n adapter?.onCheckIn ??\n (client === undefined\n ? undefined\n : (spotId: string, proof: unknown, options?: CheckInOptions) =>\n client.checkIn(spotId, proof, options));\n const claim =\n adapter?.onClaimReward ??\n (client === undefined\n ? undefined\n : (rewardId: string, options?: ClaimOptions) => client.claimReward(rewardId, options));\n const submit = useCallback(\n (spotId: string, proof: unknown) => {\n if (checkIn === undefined) return;\n setBusy(spotId);\n void checkIn(spotId, proof).finally(() => setBusy(null));\n },\n [checkIn],\n );\n return (\n <section aria-label={label(dictionary, locale, \"viewer\", \"Stamp rally\")}>\n <h1>{resolveLocalizedText(config.title, locale) || config.id}</h1>\n <progress\n aria-label={label(dictionary, locale, \"progress\", \"Progress\")}\n max={100}\n value={progress.percentage}\n />{\" \"}\n <span>\n {progress.acquired}/{progress.total}\n </span>\n <div>\n {config.spots.map((spot) =>\n spot.conditions.map((condition) => {\n const Renderer = customConditionRenderers[condition.type] ?? DefaultCondition;\n return (\n <article key={`${spot.id}-${condition.type}-${JSON.stringify(condition)}`}>\n <h2>{resolveLocalizedText(spot.name, locale)}</h2>\n <Renderer\n spot={spot}\n condition={condition}\n locale={locale}\n {...(dictionary === undefined ? {} : { dictionary })}\n disabled={busy !== null}\n onSubmit={(proof) => submit(spot.id, proof)}\n />\n </article>\n );\n }),\n )}\n </div>\n <section aria-label={label(dictionary, locale, \"rewards\", \"Rewards\")}>\n {config.rewards.map((reward) => (\n <RewardButton\n key={reward.id}\n reward={reward}\n state={state?.rewards.find((item) => item.rewardId === reward.id)}\n onClaim={claim}\n />\n ))}\n </section>\n </section>\n );\n}\nfunction RewardButton({\n reward,\n state,\n onClaim,\n}: {\n readonly reward: Reward;\n readonly state: RewardState | undefined;\n readonly onClaim:\n | ((rewardId: string, options?: ClaimOptions) => Promise<ClaimResult>)\n | undefined;\n}): ReactElement {\n return (\n <button\n type=\"button\"\n disabled={state?.status !== \"AVAILABLE\" || onClaim === undefined}\n onClick={() => {\n if (onClaim !== undefined) void onClaim(reward.id);\n }}\n >\n {resolveLocalizedText(reward.title, \"en\")} ({state?.status ?? \"LOCKED\"})\n </button>\n );\n}\nexport interface StampSheetProps<TLocale extends string = string> {\n readonly config: RallyConfig<TLocale>;\n readonly state?: StampRallyState | null;\n readonly title?: string;\n readonly progress?: StampRallyProgress;\n readonly locale?: TLocale;\n}\nexport function StampSheet<TLocale extends string = string>({\n config,\n state,\n title,\n progress,\n}: StampSheetProps<TLocale>): ReactElement {\n const current =\n progress ??\n calculateProgress(\n state ?? { rallyId: config.id, userId: null, records: [], rewards: [], updatedAt: \"\" },\n config,\n );\n return (\n <section aria-label={title ?? \"Stamp sheet\"}>\n <h2>{title ?? resolveLocalizedText(config.title, \"en\")}</h2>\n <progress max={100} value={current.percentage} />\n <div>\n {config.spots.map((spot) => (\n <span key={spot.id}>\n {state?.records.some((record) => record.stampId === spot.id) ? \"✓\" : \"○\"}\n </span>\n ))}\n </div>\n </section>\n );\n}\n"]}
1
+ {"version":3,"sources":["../src/index.tsx"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type {\n CheckInOptions,\n CheckInResult,\n ClaimOptions,\n ClaimResult,\n LocaleDictionary,\n PublicCheckInCondition,\n PublicRallyConfig,\n PublicSpotItem,\n RallyConfig,\n Reward,\n RewardState,\n StampRallyClient,\n StampRallyProgress,\n StampRallyState,\n UserRallyState,\n} from \"@stamprally/core\";\nimport {\n calculateProgress,\n getCurrentGeoContext,\n isNfcSupported,\n isQrSupported,\n readNfcContext,\n readQrContext,\n resolveLocalizedText,\n} from \"@stamprally/core\";\nimport {\n type ComponentType,\n type CSSProperties,\n type FormEvent,\n type ReactElement,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\n\nexport interface ConditionRendererProps<TLocale extends string = string> {\n readonly spot: PublicSpotItem<TLocale>;\n readonly condition: PublicCheckInCondition;\n readonly locale: TLocale;\n readonly dictionary?: LocaleDictionary<TLocale>;\n readonly disabled: boolean;\n // biome-ignore lint/suspicious/noConfusingVoidType: custom renderers may fire-and-forget while built-ins await feedback.\n readonly onSubmit: (proof: unknown) => void | Promise<unknown>;\n}\nexport type ConditionRenderer<TLocale extends string = string> = ComponentType<\n ConditionRendererProps<TLocale>\n>;\nexport type ViewerClassName = \"root\" | \"condition\" | \"action\" | \"feedback\" | \"reward\";\nexport type ViewerStyle = CSSProperties & {\n readonly [key: `--${string}`]: string | number | undefined;\n};\n\nexport interface RallyViewerAdapter<TLocale extends string = string> {\n readonly config: PublicRallyConfig<TLocale>;\n readonly onCheckIn: (\n spotId: string,\n proof: unknown,\n options?: CheckInOptions,\n ) => Promise<CheckInResult>;\n readonly onClaimReward?: (rewardId: string, options?: ClaimOptions) => Promise<ClaimResult>;\n}\nexport interface RallyViewerProps<TLocale extends string = string> {\n readonly config?: PublicRallyConfig<TLocale>;\n readonly client?: StampRallyClient;\n readonly adapter?: RallyViewerAdapter<TLocale>;\n readonly locale: TLocale;\n readonly dictionary?: LocaleDictionary<TLocale>;\n readonly classNames?: Partial<Record<ViewerClassName, string>>;\n readonly style?: ViewerStyle;\n readonly customConditionRenderers?: Partial<\n Record<PublicCheckInCondition[\"type\"], ConditionRenderer<TLocale>>\n >;\n}\n\nconst label = <TLocale extends string>(\n dictionary: LocaleDictionary<TLocale> | undefined,\n locale: TLocale,\n key: string,\n fallback: string,\n): string => dictionary?.[locale]?.[key] ?? fallback;\n\ntype VerificationStatus = \"idle\" | \"loading\" | \"success\" | \"error\";\n\nfunction DefaultCondition<TLocale extends string>({\n condition,\n dictionary,\n locale,\n disabled,\n onSubmit,\n}: ConditionRendererProps<TLocale>): ReactElement {\n const [value, setValue] = useState(\"\");\n const [status, setStatus] = useState<VerificationStatus>(\"idle\");\n const [error, setError] = useState<string | null>(null);\n const videoRef = useRef<HTMLVideoElement>(null);\n const text = (key: string, fallback: string): string => label(dictionary, locale, key, fallback);\n const verify = async (proof: unknown): Promise<void> => {\n setStatus(\"loading\");\n setError(null);\n try {\n const result = await onSubmit(proof);\n if (\n result !== undefined &&\n typeof result === \"object\" &&\n result !== null &&\n \"ok\" in result &&\n result.ok === false\n ) {\n setStatus(\"error\");\n setError(text(\"verificationFailed\", \"Verification failed.\"));\n } else setStatus(\"success\");\n } catch {\n setStatus(\"error\");\n setError(text(\"verificationFailed\", \"Verification failed.\"));\n }\n };\n const submit = (event: FormEvent): void => {\n event.preventDefault();\n if (condition.type === \"gps\") return;\n if (condition.type === \"nfc\") return;\n void verify(condition.type === \"passcode\" ? value : value);\n };\n const scanQr = (): void => {\n if (videoRef.current === null) return;\n setStatus(\"loading\");\n setError(null);\n void readQrContext(videoRef.current).then((result) => {\n if (result.ok) void verify(result.value);\n else {\n setStatus(\"error\");\n setError(result.error.message);\n }\n });\n };\n const readLocation = (): void => {\n setStatus(\"loading\");\n setError(null);\n void getCurrentGeoContext().then((result) => {\n if (result.ok) void verify(result.value);\n else {\n setStatus(\"error\");\n setError(result.error.message);\n }\n });\n };\n const readNfc = (): void => {\n setStatus(\"loading\");\n setError(null);\n void readNfcContext().then((result) => {\n if (result.ok) void verify(result.value);\n else {\n setStatus(\"error\");\n setError(result.error.message);\n }\n });\n };\n return (\n <div>\n {condition.type === \"qr\" && (\n <>\n <label>\n {text(\"qrValue\", \"QR value\")}\n <input\n aria-label={text(\"qrValue\", \"QR value\")}\n placeholder={condition.qrEntryUrl ?? text(\"qrPlaceholder\", \"Paste a QR value\")}\n value={value}\n onChange={(event) => setValue(event.target.value)}\n />\n </label>\n <video\n ref={videoRef}\n aria-label={text(\"qrCamera\", \"QR camera\")}\n hidden={!isQrSupported()}\n >\n <track kind=\"captions\" />\n </video>\n <button\n type=\"button\"\n className=\"sry-action\"\n disabled={disabled || !isQrSupported()}\n onClick={scanQr}\n >\n {text(\"scanQr\", \"Scan QR\")}\n </button>\n </>\n )}\n {condition.type === \"gps\" && (\n <button type=\"button\" className=\"sry-action\" disabled={disabled} onClick={readLocation}>\n {text(\"checkLocation\", \"Check location\")}\n </button>\n )}\n {condition.type === \"nfc\" && (\n <button\n type=\"button\"\n className=\"sry-action\"\n disabled={disabled || !isNfcSupported()}\n onClick={readNfc}\n >\n {text(\"readNfc\", \"Read NFC\")}\n </button>\n )}\n {(condition.type === \"passcode\" ||\n condition.type === \"custom\" ||\n condition.type === \"qr\") && (\n <form onSubmit={submit}>\n <label>\n {text(\n condition.type === \"passcode\" ? \"passcode\" : \"proof\",\n condition.type === \"passcode\" ? \"Passcode\" : \"Proof\",\n )}\n <input\n aria-label={text(\"proof\", \"Proof\")}\n value={value}\n onChange={(event) => setValue(event.target.value)}\n />\n </label>\n <button type=\"submit\" className=\"sry-action\" disabled={disabled || value.trim() === \"\"}>\n {text(\"checkIn\", \"Check in\")}\n </button>\n </form>\n )}\n <div\n className=\"sry-feedback\"\n aria-live=\"polite\"\n role={status === \"error\" ? \"alert\" : undefined}\n >\n {status === \"loading\" && text(\"verifying\", \"Verifying…\")}\n {status === \"success\" && text(\"verified\", \"Verified\")}\n {status === \"error\" && (error ?? text(\"verificationFailed\", \"Verification failed.\"))}\n </div>\n </div>\n );\n}\n\nexport function RallyViewer<TLocale extends string = string>({\n config: providedConfig,\n client,\n adapter,\n locale,\n dictionary,\n classNames = {},\n style,\n customConditionRenderers = {},\n}: RallyViewerProps<TLocale>): ReactElement {\n const config = providedConfig ?? client?.getConfig() ?? adapter?.config;\n const [state, setState] = useState<UserRallyState | null>(() => client?.getState() ?? null);\n const [busy, setBusy] = useState<string | null>(null);\n useEffect(() => {\n if (client === undefined) return;\n const unsubscribe = client.subscribe(setState);\n void client.init().then(setState);\n return unsubscribe;\n }, [client]);\n if (config === undefined) throw new Error(\"RallyViewer requires config, client, or adapter.\");\n const progress = useMemo<StampRallyProgress>(\n () =>\n calculateProgress(\n state ?? { rallyId: config.id, userId: null, records: [], rewards: [], updatedAt: \"\" },\n config,\n ),\n [config, state],\n );\n const checkIn =\n adapter?.onCheckIn ??\n (client === undefined\n ? undefined\n : (spotId: string, proof: unknown, options?: CheckInOptions) =>\n client.checkIn(spotId, proof, options));\n const claim =\n adapter?.onClaimReward ??\n (client === undefined\n ? undefined\n : (rewardId: string, options?: ClaimOptions) => client.claimReward(rewardId, options));\n const submit = useCallback(\n (spotId: string, proof: unknown): Promise<CheckInResult | undefined> => {\n if (checkIn === undefined) return Promise.resolve(undefined);\n setBusy(spotId);\n return checkIn(spotId, proof).finally(() => setBusy(null));\n },\n [checkIn],\n );\n return (\n <section\n className={classNames.root}\n style={style}\n aria-label={label(dictionary, locale, \"viewer\", \"Stamp rally\")}\n >\n <h1>{resolveLocalizedText(config.title, locale) || config.id}</h1>\n <progress\n aria-label={label(dictionary, locale, \"progress\", \"Progress\")}\n max={100}\n value={progress.percentage}\n />{\" \"}\n <span>\n {progress.acquired}/{progress.total}\n </span>\n <div>\n {config.spots.map((spot) =>\n spot.conditions.map((condition) => {\n const Renderer = customConditionRenderers[condition.type] ?? DefaultCondition;\n return (\n <article\n className={classNames.condition}\n key={`${spot.id}-${condition.type}-${JSON.stringify(condition)}`}\n >\n <h2>{resolveLocalizedText(spot.name, locale)}</h2>\n <Renderer\n spot={spot}\n condition={condition}\n locale={locale}\n {...(dictionary === undefined ? {} : { dictionary })}\n disabled={busy !== null}\n onSubmit={(proof) => submit(spot.id, proof)}\n />\n </article>\n );\n }),\n )}\n </div>\n <section aria-label={label(dictionary, locale, \"rewards\", \"Rewards\")}>\n {config.rewards.map((reward) => (\n <RewardButton\n key={reward.id}\n reward={reward}\n state={state?.rewards.find((item) => item.rewardId === reward.id)}\n locale={locale}\n {...(dictionary === undefined ? {} : { dictionary })}\n {...(classNames.reward === undefined ? {} : { className: classNames.reward })}\n onClaim={claim}\n />\n ))}\n </section>\n </section>\n );\n}\n\nfunction RewardButton<TLocale extends string>({\n reward,\n state,\n locale,\n dictionary,\n className,\n onClaim,\n}: {\n readonly reward: Reward<TLocale>;\n readonly state: RewardState | undefined;\n readonly locale: TLocale;\n readonly dictionary?: LocaleDictionary<TLocale>;\n readonly className?: string;\n readonly onClaim:\n | ((rewardId: string, options?: ClaimOptions) => Promise<ClaimResult>)\n | undefined;\n}): ReactElement {\n const status = state?.status ?? \"LOCKED\";\n return (\n <button\n type=\"button\"\n className={className}\n disabled={status !== \"AVAILABLE\" || onClaim === undefined}\n onClick={() => {\n if (onClaim !== undefined) void onClaim(reward.id);\n }}\n >\n {resolveLocalizedText(reward.title, locale)} (\n {label(dictionary, locale, `status.${status.toLowerCase()}`, status)})\n </button>\n );\n}\n\nexport interface StampSheetProps<TLocale extends string = string> {\n readonly config: RallyConfig<TLocale>;\n readonly state?: StampRallyState | null;\n readonly title?: string;\n readonly progress?: StampRallyProgress;\n readonly locale?: TLocale;\n readonly dictionary?: LocaleDictionary<TLocale>;\n}\nexport function StampSheet<TLocale extends string = string>({\n config,\n state,\n title,\n progress,\n locale = \"en\" as TLocale,\n dictionary,\n}: StampSheetProps<TLocale>): ReactElement {\n const current =\n progress ??\n calculateProgress(\n state ?? { rallyId: config.id, userId: null, records: [], rewards: [], updatedAt: \"\" },\n config,\n );\n return (\n <section aria-label={title ?? label(dictionary, locale, \"stampSheet\", \"Stamp sheet\")}>\n <h2>{title ?? resolveLocalizedText(config.title, locale)}</h2>\n <progress max={100} value={current.percentage} />\n <div>\n {config.spots.map((spot) => (\n <span key={spot.id}>\n {state?.records.some((record) => record.stampId === spot.id) ? \"✓\" : \"○\"}\n </span>\n ))}\n </div>\n </section>\n );\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamprally/ui",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Accessible participant UI components for stamp rallies.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -29,8 +29,8 @@
29
29
  "./styles.css": "./dist/index.css"
30
30
  },
31
31
  "dependencies": {
32
- "@stamprally/react": "0.9.0",
33
- "@stamprally/core": "0.9.0"
32
+ "@stamprally/core": "0.10.0",
33
+ "@stamprally/react": "0.10.0"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "react": "^18.0.0 || ^19.0.0",