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