@stamprally/admin-ui 0.9.0 → 0.11.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,6 +5,94 @@ var react = require('react');
5
5
  var jsxRuntime = require('react/jsx-runtime');
6
6
 
7
7
  // src/index.tsx
8
+ function useAdminRallyEditor(initialConfig, options = {}) {
9
+ const [config, setConfig] = react.useState(initialConfig);
10
+ const update = (patch) => {
11
+ setConfig((current) => {
12
+ const next = { ...current, ...patch };
13
+ options.onChange?.(next);
14
+ return next;
15
+ });
16
+ };
17
+ const updateSpot = (spotId, patch) => {
18
+ update({
19
+ spots: config.spots.map((spot) => spot.id === spotId ? { ...spot, ...patch } : spot)
20
+ });
21
+ };
22
+ const updateReward = (rewardId, patch) => {
23
+ update({
24
+ rewards: config.rewards.map(
25
+ (reward) => reward.id === rewardId ? { ...reward, ...patch } : reward
26
+ )
27
+ });
28
+ };
29
+ return {
30
+ config,
31
+ setConfig,
32
+ update,
33
+ updateSpot,
34
+ updateReward,
35
+ reset: () => setConfig(initialConfig),
36
+ isDirty: config !== initialConfig
37
+ };
38
+ }
39
+ function useSpotEditor(spotId, options = {}) {
40
+ const [config, setConfig] = react.useState(
41
+ options.config ?? options.initialConfig
42
+ );
43
+ const commit = (next) => {
44
+ setConfig(next);
45
+ options.onChange?.(next);
46
+ };
47
+ const spot = config?.spots.find((item) => item.id === spotId);
48
+ return {
49
+ config,
50
+ spot,
51
+ setConfig,
52
+ update: (patch) => {
53
+ if (config === void 0 || spot === void 0) return;
54
+ commit({
55
+ ...config,
56
+ spots: config.spots.map((item) => item.id === spotId ? { ...item, ...patch } : item)
57
+ });
58
+ },
59
+ remove: () => {
60
+ if (config === void 0 || spot === void 0) return;
61
+ commit({ ...config, spots: config.spots.filter((item) => item.id !== spotId) });
62
+ }
63
+ };
64
+ }
65
+ function useRewardEditor(rewardId, options = {}) {
66
+ const [config, setConfig] = react.useState(
67
+ options.config ?? options.initialConfig
68
+ );
69
+ const reward = config?.rewards.find((item) => item.id === rewardId);
70
+ const commit = (next) => {
71
+ setConfig(next);
72
+ options.onChange?.(next);
73
+ };
74
+ return {
75
+ config,
76
+ reward,
77
+ setConfig,
78
+ update: (patch) => {
79
+ if (config === void 0 || reward === void 0) return;
80
+ commit({
81
+ ...config,
82
+ rewards: config.rewards.map(
83
+ (item) => item.id === rewardId ? { ...item, ...patch } : item
84
+ )
85
+ });
86
+ },
87
+ remove: () => {
88
+ if (config === void 0 || reward === void 0) return;
89
+ commit({ ...config, rewards: config.rewards.filter((item) => item.id !== rewardId) });
90
+ }
91
+ };
92
+ }
93
+ function text(dictionary, locale, key, fallback) {
94
+ return dictionary?.[locale]?.[key] ?? fallback;
95
+ }
8
96
  var condition = () => ({ type: "passcode", code: "" });
9
97
  var newSpot = (index) => ({
10
98
  id: `spot-${index + 1}`,
@@ -19,98 +107,609 @@ var newReward = (index) => ({
19
107
  redemptionMethod: "server_claim",
20
108
  requiredStampCount: index + 1
21
109
  });
22
- function AdminRallyEditor({
23
- config,
110
+ function move(items, index, direction) {
111
+ const target = index + direction;
112
+ if (target < 0 || target >= items.length) return items;
113
+ const next = [...items];
114
+ const [item] = next.splice(index, 1);
115
+ if (item !== void 0) next.splice(target, 0, item);
116
+ return next;
117
+ }
118
+ function ConditionEditor({
119
+ condition: condition2,
120
+ onChange,
121
+ onRemove,
122
+ locale,
123
+ dictionary
124
+ }) {
125
+ const activeLocale = locale ?? "en";
126
+ const field = (key, fallback) => text(dictionary, activeLocale, key, fallback);
127
+ return /* @__PURE__ */ jsxRuntime.jsxs("fieldset", { children: [
128
+ /* @__PURE__ */ jsxRuntime.jsx("legend", { children: field("condition", "Condition") }),
129
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
130
+ field("conditionType", "Type"),
131
+ /* @__PURE__ */ jsxRuntime.jsx(
132
+ "select",
133
+ {
134
+ value: condition2.type,
135
+ onChange: (event) => {
136
+ const type = event.target.value;
137
+ onChange(
138
+ type === "qr" ? { type, secretToken: "" } : type === "passcode" ? { type, code: "", caseSensitive: false } : type === "gps" ? { type, latitude: 0, longitude: 0, radiusMeters: 100 } : type === "nfc" ? { type, tagId: "" } : { type, validatorName: "" }
139
+ );
140
+ },
141
+ children: ["qr", "passcode", "gps", "nfc", "custom"].map((type) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: type, children: field(`condition.${type}`, type.toUpperCase()) }, type))
142
+ }
143
+ )
144
+ ] }),
145
+ condition2.type === "qr" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
146
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
147
+ field("secretToken", "QR token"),
148
+ /* @__PURE__ */ jsxRuntime.jsx(
149
+ "input",
150
+ {
151
+ value: condition2.secretToken,
152
+ onChange: (event) => onChange({ ...condition2, secretToken: event.target.value })
153
+ }
154
+ )
155
+ ] }),
156
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
157
+ field("qrEntryUrl", "QR entry URL"),
158
+ /* @__PURE__ */ jsxRuntime.jsx(
159
+ "input",
160
+ {
161
+ value: condition2.qrEntryUrl ?? "",
162
+ onChange: (event) => onChange({ ...condition2, qrEntryUrl: event.target.value })
163
+ }
164
+ )
165
+ ] })
166
+ ] }),
167
+ condition2.type === "passcode" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
168
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
169
+ field("passcode", "Passcode"),
170
+ /* @__PURE__ */ jsxRuntime.jsx(
171
+ "input",
172
+ {
173
+ value: condition2.code,
174
+ onChange: (event) => onChange({ ...condition2, code: event.target.value })
175
+ }
176
+ )
177
+ ] }),
178
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
179
+ /* @__PURE__ */ jsxRuntime.jsx(
180
+ "input",
181
+ {
182
+ type: "checkbox",
183
+ checked: condition2.caseSensitive ?? false,
184
+ onChange: (event) => onChange({ ...condition2, caseSensitive: event.target.checked })
185
+ }
186
+ ),
187
+ field("caseSensitive", "Case sensitive")
188
+ ] })
189
+ ] }),
190
+ condition2.type === "gps" && /* @__PURE__ */ jsxRuntime.jsx("div", { children: ["latitude", "longitude", "radiusMeters"].map((key) => /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
191
+ field(key, key),
192
+ /* @__PURE__ */ jsxRuntime.jsx(
193
+ "input",
194
+ {
195
+ type: "number",
196
+ value: condition2[key],
197
+ onChange: (event) => onChange({ ...condition2, [key]: Number(event.target.value) })
198
+ }
199
+ )
200
+ ] }, key)) }),
201
+ condition2.type === "nfc" && /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
202
+ field("tagId", "NFC tag ID"),
203
+ /* @__PURE__ */ jsxRuntime.jsx(
204
+ "input",
205
+ {
206
+ value: condition2.tagId,
207
+ onChange: (event) => onChange({ ...condition2, tagId: event.target.value })
208
+ }
209
+ )
210
+ ] }),
211
+ condition2.type === "custom" && /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
212
+ field("validatorName", "Validator name"),
213
+ /* @__PURE__ */ jsxRuntime.jsx(
214
+ "input",
215
+ {
216
+ value: condition2.validatorName,
217
+ onChange: (event) => onChange({ ...condition2, validatorName: event.target.value })
218
+ }
219
+ )
220
+ ] }),
221
+ onRemove !== void 0 && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: onRemove, children: field("removeCondition", "Remove condition") })
222
+ ] });
223
+ }
224
+ function SpotItemForm({
225
+ spot,
24
226
  onChange,
25
- locale = "en"
227
+ onRemove,
228
+ locale,
229
+ dictionary
26
230
  }) {
231
+ const activeLocale = locale ?? "en";
232
+ const field = (key, fallback) => text(dictionary, activeLocale, key, fallback);
233
+ const update = (patch) => onChange({ ...spot, ...patch });
234
+ return /* @__PURE__ */ jsxRuntime.jsxs("fieldset", { children: [
235
+ /* @__PURE__ */ jsxRuntime.jsx("legend", { children: core.resolveLocalizedText(spot.name, activeLocale) }),
236
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
237
+ field("spotName", "Spot name"),
238
+ /* @__PURE__ */ jsxRuntime.jsx(
239
+ "input",
240
+ {
241
+ value: core.resolveLocalizedText(spot.name, activeLocale),
242
+ onChange: (event) => update({ name: core.updateLocalizedField(spot.name, activeLocale, event.target.value) })
243
+ }
244
+ )
245
+ ] }),
246
+ ["description", "hint"].map((key) => /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
247
+ field(key, key),
248
+ /* @__PURE__ */ jsxRuntime.jsx(
249
+ "textarea",
250
+ {
251
+ value: core.resolveLocalizedText(spot[key] ?? "", activeLocale),
252
+ onChange: (event) => update({
253
+ [key]: core.updateLocalizedField(spot[key] ?? "", activeLocale, event.target.value)
254
+ })
255
+ }
256
+ )
257
+ ] }, key)),
258
+ ["imageUrl", "iconUrl", "redirectUrlAfterClaim"].map((key) => /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
259
+ field(key, key),
260
+ /* @__PURE__ */ jsxRuntime.jsx(
261
+ "input",
262
+ {
263
+ value: spot[key] ?? "",
264
+ onChange: (event) => update({ [key]: event.target.value })
265
+ }
266
+ )
267
+ ] }, key)),
268
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
269
+ field("prerequisites", "Prerequisite spots"),
270
+ /* @__PURE__ */ jsxRuntime.jsx(
271
+ "input",
272
+ {
273
+ value: spot.prerequisites?.join(", ") ?? "",
274
+ onChange: (event) => update({
275
+ prerequisites: event.target.value.split(",").map((item) => item.trim()).filter(Boolean)
276
+ })
277
+ }
278
+ )
279
+ ] }),
280
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
281
+ field("externalReferences", "External references"),
282
+ /* @__PURE__ */ jsxRuntime.jsx(
283
+ "textarea",
284
+ {
285
+ value: JSON.stringify(spot.externalReferences ?? [], null, 2),
286
+ onChange: (event) => {
287
+ try {
288
+ const parsed = JSON.parse(event.target.value);
289
+ if (Array.isArray(parsed)) update({ externalReferences: parsed });
290
+ } catch {
291
+ }
292
+ }
293
+ }
294
+ )
295
+ ] }),
296
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
297
+ field("metadata", "Metadata"),
298
+ /* @__PURE__ */ jsxRuntime.jsx(
299
+ "textarea",
300
+ {
301
+ value: JSON.stringify(spot.metadata ?? {}, null, 2),
302
+ onChange: (event) => {
303
+ try {
304
+ const parsed = JSON.parse(event.target.value);
305
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed))
306
+ update({ metadata: parsed });
307
+ } catch {
308
+ }
309
+ }
310
+ }
311
+ )
312
+ ] }),
313
+ spot.conditions.map((item, index) => /* @__PURE__ */ jsxRuntime.jsx(
314
+ ConditionEditor,
315
+ {
316
+ condition: item,
317
+ locale: activeLocale,
318
+ ...dictionary === void 0 ? {} : { dictionary },
319
+ onChange: (next) => update({
320
+ conditions: spot.conditions.map(
321
+ (current, itemIndex) => itemIndex === index ? next : current
322
+ )
323
+ }),
324
+ ...spot.conditions.length <= 1 ? {} : {
325
+ onRemove: () => update({
326
+ conditions: spot.conditions.filter((_, itemIndex) => itemIndex !== index)
327
+ })
328
+ }
329
+ },
330
+ `${spot.id}-condition-${JSON.stringify(item)}`
331
+ )),
332
+ /* @__PURE__ */ jsxRuntime.jsx(
333
+ "button",
334
+ {
335
+ type: "button",
336
+ onClick: () => update({ conditions: [...spot.conditions, condition()] }),
337
+ children: field("addCondition", "Add condition")
338
+ }
339
+ ),
340
+ onRemove !== void 0 && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: onRemove, children: field("removeSpot", "Remove spot") })
341
+ ] });
342
+ }
343
+ function RewardItemForm({
344
+ reward,
345
+ locale,
346
+ dictionary,
347
+ onChange,
348
+ onRemove
349
+ }) {
350
+ const field = (key, fallback) => text(dictionary, locale, key, fallback);
351
+ return /* @__PURE__ */ jsxRuntime.jsxs("fieldset", { children: [
352
+ /* @__PURE__ */ jsxRuntime.jsx("legend", { children: core.resolveLocalizedText(reward.title, locale) }),
353
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
354
+ field("rewardTitle", "Reward title"),
355
+ /* @__PURE__ */ jsxRuntime.jsx(
356
+ "input",
357
+ {
358
+ value: core.resolveLocalizedText(reward.title, locale),
359
+ onChange: (event) => onChange({
360
+ ...reward,
361
+ title: core.updateLocalizedField(reward.title, locale, event.target.value)
362
+ })
363
+ }
364
+ )
365
+ ] }),
366
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
367
+ field("description", "Description"),
368
+ /* @__PURE__ */ jsxRuntime.jsx(
369
+ "textarea",
370
+ {
371
+ value: core.resolveLocalizedText(reward.description ?? "", locale),
372
+ onChange: (event) => onChange({
373
+ ...reward,
374
+ description: core.updateLocalizedField(
375
+ reward.description ?? "",
376
+ locale,
377
+ event.target.value
378
+ )
379
+ })
380
+ }
381
+ )
382
+ ] }),
383
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
384
+ field("requiredSpotCount", "Required spot count"),
385
+ /* @__PURE__ */ jsxRuntime.jsx(
386
+ "input",
387
+ {
388
+ type: "number",
389
+ min: 0,
390
+ value: reward.requiredStampCount,
391
+ onChange: (event) => onChange({ ...reward, requiredStampCount: Number(event.target.value) })
392
+ }
393
+ )
394
+ ] }),
395
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
396
+ field("rewardType", "Reward type"),
397
+ /* @__PURE__ */ jsxRuntime.jsxs(
398
+ "select",
399
+ {
400
+ value: reward.type,
401
+ onChange: (event) => onChange({ ...reward, type: event.target.value }),
402
+ children: [
403
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "digital", children: field("rewardType.digital", "Digital") }),
404
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "in_person", children: field("rewardType.inPerson", "In person") })
405
+ ]
406
+ }
407
+ )
408
+ ] }),
409
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
410
+ field("redemptionMethod", "Redemption method"),
411
+ /* @__PURE__ */ jsxRuntime.jsxs(
412
+ "select",
413
+ {
414
+ value: reward.redemptionMethod,
415
+ onChange: (event) => onChange({
416
+ ...reward,
417
+ redemptionMethod: event.target.value
418
+ }),
419
+ children: [
420
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "server_claim", children: field("redemption.serverClaim", "Server claim") }),
421
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "manual_slide", children: field("redemption.manualSlide", "Manual slide") }),
422
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "staff_passcode", children: field("redemption.staffPasscode", "Staff passcode") }),
423
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "view_only", children: field("redemption.viewOnly", "View only") })
424
+ ]
425
+ }
426
+ )
427
+ ] }),
428
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
429
+ field("unlockConditions", "Unlock conditions"),
430
+ /* @__PURE__ */ jsxRuntime.jsx(
431
+ "textarea",
432
+ {
433
+ value: JSON.stringify(reward.conditions ?? [], null, 2),
434
+ onChange: (event) => {
435
+ try {
436
+ const parsed = JSON.parse(event.target.value);
437
+ if (Array.isArray(parsed))
438
+ onChange({ ...reward, conditions: parsed });
439
+ } catch {
440
+ }
441
+ }
442
+ }
443
+ )
444
+ ] }),
445
+ ["stockLimit", "userClaimLimit"].map((key) => /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
446
+ field(key, key),
447
+ /* @__PURE__ */ jsxRuntime.jsx(
448
+ "input",
449
+ {
450
+ type: "number",
451
+ min: 0,
452
+ value: reward[key] ?? "",
453
+ onChange: (event) => {
454
+ if (event.target.value === "") {
455
+ const { [key]: _removed, ...withoutLimit } = reward;
456
+ onChange(withoutLimit);
457
+ } else onChange({ ...reward, [key]: Number(event.target.value) });
458
+ }
459
+ }
460
+ )
461
+ ] }, key)),
462
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
463
+ field("digitalContentUrl", "Digital content URL"),
464
+ /* @__PURE__ */ jsxRuntime.jsx(
465
+ "input",
466
+ {
467
+ value: reward.digitalContentUrl ?? "",
468
+ onChange: (event) => onChange({ ...reward, digitalContentUrl: event.target.value })
469
+ }
470
+ )
471
+ ] }),
472
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
473
+ field("staffPasscode", "Staff passcode"),
474
+ /* @__PURE__ */ jsxRuntime.jsx(
475
+ "input",
476
+ {
477
+ value: reward.staffPasscode ?? "",
478
+ onChange: (event) => onChange({ ...reward, staffPasscode: event.target.value })
479
+ }
480
+ )
481
+ ] }),
482
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
483
+ field("validUntil", "Valid until"),
484
+ /* @__PURE__ */ jsxRuntime.jsx(
485
+ "input",
486
+ {
487
+ type: "datetime-local",
488
+ value: reward.validUntil?.slice(0, 16) ?? "",
489
+ onChange: (event) => {
490
+ if (event.target.value === "") {
491
+ const { validUntil: _removed, ...withoutDate } = reward;
492
+ onChange(withoutDate);
493
+ } else onChange({ ...reward, validUntil: new Date(event.target.value).toISOString() });
494
+ }
495
+ }
496
+ )
497
+ ] }),
498
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: onRemove, children: field("removeReward", "Remove reward") })
499
+ ] });
500
+ }
501
+ function AdminRallyEditor({ config, onChange, locale, dictionary }) {
502
+ const activeLocale = locale ?? "en";
503
+ const field = (key, fallback) => text(dictionary, activeLocale, key, fallback);
27
504
  const update = (next) => onChange({ ...config, ...next });
28
- return /* @__PURE__ */ jsxRuntime.jsxs("section", { "aria-label": "Rally editor", children: [
29
- /* @__PURE__ */ jsxRuntime.jsx("h1", { children: core.resolveLocalizedText(config.title, locale) }),
505
+ const updateSpots = (spots) => update({ spots: spots.map((spot, index) => ({ ...spot, orderIndex: index })) });
506
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { "aria-label": field("rallyEditor", "Rally editor"), children: [
507
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { children: core.resolveLocalizedText(config.title, activeLocale) }),
30
508
  /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
31
- "Title",
509
+ field("title", "Title"),
32
510
  /* @__PURE__ */ jsxRuntime.jsx(
33
511
  "input",
34
512
  {
35
- value: core.resolveLocalizedText(config.title, locale),
36
- onChange: (event) => update({ title: event.target.value })
513
+ value: core.resolveLocalizedText(config.title, activeLocale),
514
+ onChange: (event) => update({ title: core.updateLocalizedField(config.title, activeLocale, event.target.value) })
515
+ }
516
+ )
517
+ ] }),
518
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
519
+ field("description", "Description"),
520
+ /* @__PURE__ */ jsxRuntime.jsx(
521
+ "textarea",
522
+ {
523
+ value: core.resolveLocalizedText(config.description ?? "", activeLocale),
524
+ onChange: (event) => update({
525
+ description: core.updateLocalizedField(
526
+ config.description ?? "",
527
+ activeLocale,
528
+ event.target.value
529
+ )
530
+ })
37
531
  }
38
532
  )
39
533
  ] }),
534
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
535
+ field("theme", "Theme (JSON)"),
536
+ /* @__PURE__ */ jsxRuntime.jsx(
537
+ "textarea",
538
+ {
539
+ "aria-label": field("theme", "Theme (JSON)"),
540
+ value: JSON.stringify(config.theme ?? {}, null, 2),
541
+ onChange: (event) => {
542
+ try {
543
+ const parsed = JSON.parse(event.target.value);
544
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed))
545
+ update({ theme: parsed });
546
+ } catch {
547
+ }
548
+ }
549
+ }
550
+ )
551
+ ] }),
552
+ ["serverEndpoint", "publicMetadata", "serverMetadata"].map((key) => /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
553
+ field(key, key),
554
+ /* @__PURE__ */ jsxRuntime.jsx(
555
+ "textarea",
556
+ {
557
+ value: key === "serverEndpoint" ? config.serverEndpoint ?? "" : JSON.stringify(config[key] ?? {}, null, 2),
558
+ onChange: (event) => {
559
+ if (key === "serverEndpoint") {
560
+ update({ serverEndpoint: event.target.value });
561
+ return;
562
+ }
563
+ try {
564
+ const parsed = JSON.parse(event.target.value);
565
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed))
566
+ update({ [key]: parsed });
567
+ } catch {
568
+ }
569
+ }
570
+ }
571
+ )
572
+ ] }, key)),
40
573
  /* @__PURE__ */ jsxRuntime.jsx(
41
574
  "button",
42
575
  {
43
576
  type: "button",
44
- onClick: () => update({ spots: [...config.spots, newSpot(config.spots.length)] }),
45
- children: "Add spot"
577
+ onClick: () => updateSpots([...config.spots, newSpot(config.spots.length)]),
578
+ children: field("addSpot", "Add spot")
46
579
  }
47
580
  ),
581
+ /* @__PURE__ */ jsxRuntime.jsx("div", { children: config.spots.map((spot, index) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
582
+ /* @__PURE__ */ jsxRuntime.jsx(
583
+ SpotItemForm,
584
+ {
585
+ spot,
586
+ locale: activeLocale,
587
+ ...dictionary === void 0 ? {} : { dictionary },
588
+ onChange: (next) => updateSpots(
589
+ config.spots.map((current) => current.id === spot.id ? next : current)
590
+ ),
591
+ onRemove: () => updateSpots(config.spots.filter((current) => current.id !== spot.id))
592
+ }
593
+ ),
594
+ /* @__PURE__ */ jsxRuntime.jsx(
595
+ "button",
596
+ {
597
+ type: "button",
598
+ disabled: index === 0,
599
+ onClick: () => updateSpots(move(config.spots, index, -1)),
600
+ children: field("moveUp", "Move up")
601
+ }
602
+ ),
603
+ /* @__PURE__ */ jsxRuntime.jsx(
604
+ "button",
605
+ {
606
+ type: "button",
607
+ disabled: index === config.spots.length - 1,
608
+ onClick: () => updateSpots(move(config.spots, index, 1)),
609
+ children: field("moveDown", "Move down")
610
+ }
611
+ )
612
+ ] }, spot.id)) }),
48
613
  /* @__PURE__ */ jsxRuntime.jsx(
49
614
  "button",
50
615
  {
51
616
  type: "button",
52
617
  onClick: () => update({ rewards: [...config.rewards, newReward(config.rewards.length)] }),
53
- children: "Add reward"
618
+ children: field("addReward", "Add reward")
54
619
  }
55
620
  ),
56
- /* @__PURE__ */ jsxRuntime.jsx("ul", { children: config.spots.map((spot) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: core.resolveLocalizedText(spot.name, locale) }, spot.id)) })
621
+ /* @__PURE__ */ jsxRuntime.jsx("div", { children: config.rewards.map((reward) => /* @__PURE__ */ jsxRuntime.jsx(
622
+ RewardItemForm,
623
+ {
624
+ reward,
625
+ locale: activeLocale,
626
+ ...dictionary === void 0 ? {} : { dictionary },
627
+ onChange: (next) => update({
628
+ rewards: config.rewards.map(
629
+ (current) => current.id === reward.id ? next : current
630
+ )
631
+ }),
632
+ onRemove: () => update({ rewards: config.rewards.filter((current) => current.id !== reward.id) })
633
+ },
634
+ reward.id
635
+ )) })
57
636
  ] });
58
637
  }
59
638
  var RallyEditor = AdminRallyEditor;
60
- function SpotItemForm({
61
- spot,
62
- onChange
639
+ function GeneralSettingsForm({
640
+ config,
641
+ onChange,
642
+ locale,
643
+ dictionary
63
644
  }) {
645
+ const activeLocale = locale ?? "en";
64
646
  return /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
65
- "Spot name",
647
+ text(dictionary, activeLocale, "title", "Rally title"),
66
648
  /* @__PURE__ */ jsxRuntime.jsx(
67
649
  "input",
68
650
  {
69
- value: core.resolveLocalizedText(spot.name, "en"),
70
- onChange: (event) => onChange({ ...spot, name: event.target.value })
651
+ value: core.resolveLocalizedText(config.title, activeLocale),
652
+ onChange: (event) => onChange({
653
+ ...config,
654
+ title: core.updateLocalizedField(config.title, activeLocale, event.target.value)
655
+ })
71
656
  }
72
657
  )
73
658
  ] });
74
659
  }
75
- function GeneralSettingsForm({ config, onChange }) {
76
- return /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
77
- "Rally title",
660
+ function JsonConfigIO({ config, onImport, locale, dictionary }) {
661
+ const activeLocale = locale ?? "en";
662
+ const [value, setValue] = react.useState(() => JSON.stringify(config, null, 2));
663
+ const [errors, setErrors] = react.useState([]);
664
+ const field = (key, fallback) => text(dictionary, activeLocale, key, fallback);
665
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
78
666
  /* @__PURE__ */ jsxRuntime.jsx(
79
- "input",
667
+ "textarea",
80
668
  {
81
- value: core.resolveLocalizedText(config.title, "en"),
82
- onChange: (event) => onChange({ ...config, title: event.target.value })
669
+ "aria-label": field("jsonConfig", "JSON configuration"),
670
+ value,
671
+ onChange: (event) => {
672
+ setValue(event.target.value);
673
+ setErrors([]);
674
+ }
83
675
  }
84
- )
85
- ] });
86
- }
87
- function JsonConfigIO({ config, onImport }) {
88
- const [value, setValue] = react.useState(() => JSON.stringify(config, null, 2));
89
- const parse = (event) => {
90
- setValue(event.target.value);
91
- };
92
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
93
- /* @__PURE__ */ jsxRuntime.jsx("textarea", { value, onChange: parse }),
676
+ ),
94
677
  /* @__PURE__ */ jsxRuntime.jsx(
95
678
  "button",
96
679
  {
97
680
  type: "button",
98
681
  onClick: () => {
99
682
  try {
100
- onImport(JSON.parse(value));
683
+ const result = core.safeParseAdminConfig(JSON.parse(value));
684
+ if (!result.success) {
685
+ setErrors(result.errors);
686
+ return;
687
+ }
688
+ onImport(result.data);
689
+ setErrors([]);
101
690
  } catch {
691
+ setErrors([{ path: "$", message: field("invalidJson", "Invalid JSON.") }]);
102
692
  }
103
693
  },
104
- children: "Import"
694
+ children: field("import", "Import")
105
695
  }
106
- )
696
+ ),
697
+ errors.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("ul", { role: "alert", children: errors.map((error) => /* @__PURE__ */ jsxRuntime.jsxs("li", { children: [
698
+ error.path,
699
+ ": ",
700
+ error.message
701
+ ] }, `${error.path}-${error.message}`)) })
107
702
  ] });
108
703
  }
109
704
 
110
705
  exports.AdminRallyEditor = AdminRallyEditor;
706
+ exports.ConditionEditor = ConditionEditor;
111
707
  exports.GeneralSettingsForm = GeneralSettingsForm;
112
708
  exports.JsonConfigIO = JsonConfigIO;
113
709
  exports.RallyEditor = RallyEditor;
114
710
  exports.SpotItemForm = SpotItemForm;
711
+ exports.useAdminRallyEditor = useAdminRallyEditor;
712
+ exports.useRewardEditor = useRewardEditor;
713
+ exports.useSpotEditor = useSpotEditor;
115
714
  //# sourceMappingURL=index.cjs.map
116
715
  //# sourceMappingURL=index.cjs.map