@stamprally/admin-ui 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -5,1474 +5,521 @@ var react = require('react');
5
5
  var jsxRuntime = require('react/jsx-runtime');
6
6
 
7
7
  // src/index.tsx
8
- function universalText(value, locale) {
9
- return typeof value === "string" ? value : value?.[locale] ?? "";
8
+ function text(dictionary, locale, key, fallback) {
9
+ return dictionary?.[locale]?.[key] ?? fallback;
10
10
  }
11
- function updateUniversalText(value, locale, next) {
12
- return { ...typeof value === "string" ? {} : value, [locale]: next };
11
+ var condition = () => ({ type: "passcode", code: "" });
12
+ var newSpot = (index) => ({
13
+ id: `spot-${index + 1}`,
14
+ orderIndex: index,
15
+ name: `Spot ${index + 1}`,
16
+ conditions: [condition()]
17
+ });
18
+ var newReward = (index) => ({
19
+ id: `reward-${index + 1}`,
20
+ title: `Reward ${index + 1}`,
21
+ type: "digital",
22
+ redemptionMethod: "server_claim",
23
+ requiredStampCount: index + 1
24
+ });
25
+ function move(items, index, direction) {
26
+ const target = index + direction;
27
+ if (target < 0 || target >= items.length) return items;
28
+ const next = [...items];
29
+ const [item] = next.splice(index, 1);
30
+ if (item !== void 0) next.splice(target, 0, item);
31
+ return next;
13
32
  }
14
- var newUniversalCondition = () => ({ type: "passcode", code: "" });
15
- function defaultUniversalSpot(index) {
16
- return {
17
- id: `spot-${index + 1}`,
18
- orderIndex: index,
19
- name: { ja: `\u30B9\u30DD\u30C3\u30C8 ${index + 1}`, en: `Spot ${index + 1}` },
20
- conditions: [newUniversalCondition()]
21
- };
22
- }
23
- function defaultUniversalReward(index) {
24
- return {
25
- id: `reward-${index + 1}`,
26
- title: { ja: `\u666F\u54C1 ${index + 1}`, en: `Reward ${index + 1}` },
27
- type: "digital",
28
- redemptionMethod: "manual_slide",
29
- requiredStampCount: 1
30
- };
31
- }
32
- function updateUniversalCondition(condition, field, value) {
33
- if (condition.type === "gps") {
34
- return { ...condition, [field]: Number(value) };
35
- }
36
- return { ...condition, [field]: value };
37
- }
38
- function UniversalSpotForm({
39
- spot,
40
- spots,
33
+ function ConditionEditor({
34
+ condition: condition2,
41
35
  onChange,
42
- onDelete,
43
- onMoveUp,
44
- onMoveDown
36
+ onRemove,
37
+ locale,
38
+ dictionary
45
39
  }) {
46
- const update = (next) => onChange({ ...spot, ...next });
47
- const references = spot.externalReferences ?? [];
48
- const conditions = spot.conditions;
49
- return /* @__PURE__ */ jsxRuntime.jsxs("fieldset", { className: "stamprally-admin-card", "aria-label": `Spot ${spot.id}`, children: [
50
- /* @__PURE__ */ jsxRuntime.jsxs("legend", { children: [
51
- "Spot ",
52
- spot.orderIndex + 1
53
- ] }),
54
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
55
- "ID ",
56
- /* @__PURE__ */ jsxRuntime.jsx("input", { value: spot.id, onChange: (event) => update({ id: event.target.value }) })
57
- ] }),
58
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
59
- "Name (ja)",
60
- " ",
61
- /* @__PURE__ */ jsxRuntime.jsx(
62
- "input",
63
- {
64
- value: universalText(spot.name, "ja"),
65
- onChange: (event) => update({ name: updateUniversalText(spot.name, "ja", event.target.value) })
66
- }
67
- )
68
- ] }),
40
+ const activeLocale = locale ?? "en";
41
+ const field = (key, fallback) => text(dictionary, activeLocale, key, fallback);
42
+ return /* @__PURE__ */ jsxRuntime.jsxs("fieldset", { children: [
43
+ /* @__PURE__ */ jsxRuntime.jsx("legend", { children: field("condition", "Condition") }),
69
44
  /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
70
- "Name (en)",
71
- " ",
45
+ field("conditionType", "Type"),
72
46
  /* @__PURE__ */ jsxRuntime.jsx(
73
- "input",
74
- {
75
- value: universalText(spot.name, "en"),
76
- onChange: (event) => update({ name: updateUniversalText(spot.name, "en", event.target.value) })
77
- }
78
- )
79
- ] }),
80
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
81
- "Description (ja)",
82
- " ",
83
- /* @__PURE__ */ jsxRuntime.jsx(
84
- "textarea",
85
- {
86
- value: universalText(spot.description, "ja"),
87
- onChange: (event) => update({ description: updateUniversalText(spot.description, "ja", event.target.value) })
88
- }
89
- )
90
- ] }),
91
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
92
- "Order index",
93
- " ",
94
- /* @__PURE__ */ jsxRuntime.jsx(
95
- "input",
96
- {
97
- type: "number",
98
- value: spot.orderIndex,
99
- onChange: (event) => update({ orderIndex: Number(event.target.value) })
100
- }
101
- )
102
- ] }),
103
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
104
- "Prerequisite spot",
105
- /* @__PURE__ */ jsxRuntime.jsxs(
106
47
  "select",
107
48
  {
108
- value: "",
49
+ value: condition2.type,
109
50
  onChange: (event) => {
110
- if (event.target.value !== "")
111
- update({ prerequisites: [...spot.prerequisites ?? [], event.target.value] });
51
+ const type = event.target.value;
52
+ onChange(
53
+ 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: "" }
54
+ );
112
55
  },
113
- children: [
114
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "Select a prerequisite" }),
115
- spots.filter((item) => item.id !== spot.id && !spot.prerequisites?.includes(item.id)).map((item) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: item.id, children: item.id }, item.id))
116
- ]
117
- }
118
- )
119
- ] }),
120
- (spot.prerequisites ?? []).map((id) => /* @__PURE__ */ jsxRuntime.jsxs(
121
- "button",
122
- {
123
- type: "button",
124
- onClick: () => update({ prerequisites: (spot.prerequisites ?? []).filter((item) => item !== id) }),
125
- children: [
126
- "Remove prerequisite: ",
127
- id
128
- ]
129
- },
130
- id
131
- )),
132
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
133
- /* @__PURE__ */ jsxRuntime.jsx("h4", { children: "External references" }),
134
- references.map((reference, index) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
135
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
136
- "Reference type",
137
- " ",
138
- /* @__PURE__ */ jsxRuntime.jsx(
139
- "input",
140
- {
141
- value: reference.type,
142
- onChange: (event) => update({
143
- externalReferences: references.map(
144
- (item, itemIndex) => itemIndex === index ? { ...item, type: event.target.value } : item
145
- )
146
- })
147
- }
148
- )
149
- ] }),
150
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
151
- "Reference ID",
152
- " ",
153
- /* @__PURE__ */ jsxRuntime.jsx(
154
- "input",
155
- {
156
- value: reference.id,
157
- onChange: (event) => update({
158
- externalReferences: references.map(
159
- (item, itemIndex) => itemIndex === index ? { ...item, id: event.target.value } : item
160
- )
161
- })
162
- }
163
- )
164
- ] }),
165
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
166
- "Reference URL",
167
- " ",
168
- /* @__PURE__ */ jsxRuntime.jsx(
169
- "input",
170
- {
171
- value: reference.url ?? "",
172
- onChange: (event) => update({
173
- externalReferences: references.map(
174
- (item, itemIndex) => itemIndex === index ? { ...item, url: event.target.value } : item
175
- )
176
- })
177
- }
178
- )
179
- ] }),
180
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
181
- "Reference metadata",
182
- " ",
183
- /* @__PURE__ */ jsxRuntime.jsx(
184
- "textarea",
185
- {
186
- value: JSON.stringify(reference.metadata ?? {}, null, 2),
187
- onChange: (event) => {
188
- try {
189
- const metadata = JSON.parse(event.target.value);
190
- if (typeof metadata === "object" && metadata !== null && !Array.isArray(metadata))
191
- update({
192
- externalReferences: references.map(
193
- (item, itemIndex) => itemIndex === index ? { ...item, metadata } : item
194
- )
195
- });
196
- } catch {
197
- }
198
- }
199
- }
200
- )
201
- ] }),
202
- /* @__PURE__ */ jsxRuntime.jsx(
203
- "button",
204
- {
205
- type: "button",
206
- onClick: () => update({
207
- externalReferences: references.filter((_, itemIndex) => itemIndex !== index)
208
- }),
209
- children: "Remove reference"
210
- }
211
- )
212
- ] }, `${reference.type}:${reference.id}`)),
213
- /* @__PURE__ */ jsxRuntime.jsx(
214
- "button",
215
- {
216
- type: "button",
217
- onClick: () => update({
218
- externalReferences: [...references, { type: "", id: "" }]
219
- }),
220
- children: "Add external reference"
221
- }
222
- )
223
- ] }),
224
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
225
- /* @__PURE__ */ jsxRuntime.jsx("h4", { children: "Conditions" }),
226
- conditions.map((condition, index) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
227
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
228
- "Condition type",
229
- /* @__PURE__ */ jsxRuntime.jsxs(
230
- "select",
231
- {
232
- value: condition.type,
233
- onChange: (event) => {
234
- const next = event.target.value === "qr" ? { type: "qr", secretToken: "" } : event.target.value === "gps" ? { type: "gps", latitude: 0, longitude: 0, radiusMeters: 100 } : event.target.value === "custom" ? { type: "custom", validatorName: "" } : { type: "passcode", code: "" };
235
- onChange({
236
- ...spot,
237
- conditions: conditions.map(
238
- (item, itemIndex) => itemIndex === index ? next : item
239
- )
240
- });
241
- },
242
- children: [
243
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "qr", children: "QR" }),
244
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "passcode", children: "Passcode" }),
245
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "gps", children: "GPS" }),
246
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "custom", children: "Custom" })
247
- ]
248
- }
249
- )
250
- ] }),
251
- condition.type === "qr" && /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
252
- "QR secret token",
253
- " ",
254
- /* @__PURE__ */ jsxRuntime.jsx(
255
- "input",
256
- {
257
- value: condition.secretToken,
258
- onChange: (event) => onChange({
259
- ...spot,
260
- conditions: conditions.map(
261
- (item, itemIndex) => itemIndex === index ? updateUniversalCondition(item, "secretToken", event.target.value) : item
262
- )
263
- })
264
- }
265
- )
266
- ] }),
267
- condition.type === "passcode" && /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
268
- "Passcode",
269
- " ",
270
- /* @__PURE__ */ jsxRuntime.jsx(
271
- "input",
272
- {
273
- value: condition.code,
274
- onChange: (event) => onChange({
275
- ...spot,
276
- conditions: conditions.map(
277
- (item, itemIndex) => itemIndex === index ? updateUniversalCondition(item, "code", event.target.value) : item
278
- )
279
- })
280
- }
281
- )
282
- ] }),
283
- condition.type === "custom" && /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
284
- "Validator name",
285
- " ",
286
- /* @__PURE__ */ jsxRuntime.jsx(
287
- "input",
288
- {
289
- value: condition.validatorName,
290
- onChange: (event) => onChange({
291
- ...spot,
292
- conditions: conditions.map(
293
- (item, itemIndex) => itemIndex === index ? updateUniversalCondition(item, "validatorName", event.target.value) : item
294
- )
295
- })
296
- }
297
- )
298
- ] }),
299
- condition.type === "gps" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
300
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
301
- "Latitude",
302
- " ",
303
- /* @__PURE__ */ jsxRuntime.jsx(
304
- "input",
305
- {
306
- type: "number",
307
- value: condition.latitude,
308
- onChange: (event) => onChange({
309
- ...spot,
310
- conditions: conditions.map(
311
- (item, itemIndex) => itemIndex === index ? updateUniversalCondition(item, "latitude", event.target.value) : item
312
- )
313
- })
314
- }
315
- )
316
- ] }),
317
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
318
- "Longitude",
319
- " ",
320
- /* @__PURE__ */ jsxRuntime.jsx(
321
- "input",
322
- {
323
- type: "number",
324
- value: condition.longitude,
325
- onChange: (event) => onChange({
326
- ...spot,
327
- conditions: conditions.map(
328
- (item, itemIndex) => itemIndex === index ? updateUniversalCondition(item, "longitude", event.target.value) : item
329
- )
330
- })
331
- }
332
- )
333
- ] }),
334
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
335
- "Radius meters",
336
- " ",
337
- /* @__PURE__ */ jsxRuntime.jsx(
338
- "input",
339
- {
340
- type: "number",
341
- value: condition.radiusMeters,
342
- onChange: (event) => onChange({
343
- ...spot,
344
- conditions: conditions.map(
345
- (item, itemIndex) => itemIndex === index ? updateUniversalCondition(item, "radiusMeters", event.target.value) : item
346
- )
347
- })
348
- }
349
- )
350
- ] })
351
- ] }),
352
- conditions.length > 1 && /* @__PURE__ */ jsxRuntime.jsx(
353
- "button",
354
- {
355
- type: "button",
356
- onClick: () => update({ conditions: conditions.filter((_, itemIndex) => itemIndex !== index) }),
357
- children: "Remove condition"
358
- }
359
- )
360
- ] }, JSON.stringify(condition))),
361
- /* @__PURE__ */ jsxRuntime.jsx(
362
- "button",
363
- {
364
- type: "button",
365
- onClick: () => update({ conditions: [...conditions, newUniversalCondition()] }),
366
- children: "Add condition"
367
- }
368
- )
369
- ] }),
370
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: onMoveUp, disabled: spot.orderIndex === 0, children: "Move spot up" }),
371
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: onMoveDown, disabled: spot.orderIndex === spots.length - 1, children: "Move spot down" }),
372
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: onDelete, children: "Remove spot" })
373
- ] });
374
- }
375
- function UniversalRewardForm({
376
- reward,
377
- spots,
378
- onChange,
379
- onDelete
380
- }) {
381
- const update = (next) => onChange({ ...reward, ...next });
382
- const conditions = reward.conditions ?? [];
383
- return /* @__PURE__ */ jsxRuntime.jsxs("fieldset", { className: "stamprally-admin-card", "aria-label": `Reward ${reward.id}`, children: [
384
- /* @__PURE__ */ jsxRuntime.jsx("legend", { children: "Reward" }),
385
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
386
- "ID ",
387
- /* @__PURE__ */ jsxRuntime.jsx("input", { value: reward.id, onChange: (event) => update({ id: event.target.value }) })
388
- ] }),
389
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
390
- "Title (ja)",
391
- " ",
392
- /* @__PURE__ */ jsxRuntime.jsx(
393
- "input",
394
- {
395
- value: universalText(reward.title, "ja"),
396
- onChange: (event) => update({ title: updateUniversalText(reward.title, "ja", event.target.value) })
397
- }
398
- )
399
- ] }),
400
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
401
- "Title (en)",
402
- " ",
403
- /* @__PURE__ */ jsxRuntime.jsx(
404
- "input",
405
- {
406
- value: universalText(reward.title, "en"),
407
- onChange: (event) => update({ title: updateUniversalText(reward.title, "en", event.target.value) })
408
- }
409
- )
410
- ] }),
411
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
412
- "Required stamps",
413
- " ",
414
- /* @__PURE__ */ jsxRuntime.jsx(
415
- "input",
416
- {
417
- type: "number",
418
- min: 0,
419
- value: reward.requiredStampCount,
420
- onChange: (event) => update({ requiredStampCount: Number(event.target.value) })
421
- }
422
- )
423
- ] }),
424
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
425
- "Stock limit",
426
- " ",
427
- /* @__PURE__ */ jsxRuntime.jsx(
428
- "input",
429
- {
430
- type: "number",
431
- min: 0,
432
- value: reward.stockLimit ?? "",
433
- onChange: (event) => {
434
- if (event.target.value === "") {
435
- const { stockLimit: _removed, ...rest } = reward;
436
- onChange(rest);
437
- } else update({ stockLimit: Number(event.target.value) });
438
- }
439
- }
440
- )
441
- ] }),
442
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
443
- "User claim limit",
444
- " ",
445
- /* @__PURE__ */ jsxRuntime.jsx(
446
- "input",
447
- {
448
- type: "number",
449
- min: 0,
450
- value: reward.userClaimLimit ?? "",
451
- onChange: (event) => {
452
- if (event.target.value === "") {
453
- const { userClaimLimit: _removed, ...rest } = reward;
454
- onChange(rest);
455
- } else update({ userClaimLimit: Number(event.target.value) });
456
- }
457
- }
458
- )
459
- ] }),
460
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
461
- "Staff passcode",
462
- " ",
463
- /* @__PURE__ */ jsxRuntime.jsx(
464
- "input",
465
- {
466
- type: "password",
467
- value: reward.staffPasscode ?? "",
468
- onChange: (event) => {
469
- if (event.target.value === "") {
470
- const { staffPasscode: _removed, ...rest } = reward;
471
- onChange(rest);
472
- } else update({ staffPasscode: event.target.value });
473
- }
474
- }
475
- )
476
- ] }),
477
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
478
- "Redemption method",
479
- " ",
480
- /* @__PURE__ */ jsxRuntime.jsxs(
481
- "select",
482
- {
483
- value: reward.redemptionMethod,
484
- onChange: (event) => update({ redemptionMethod: event.target.value }),
485
- children: [
486
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "manual_slide", children: "manual_slide" }),
487
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "staff_passcode", children: "staff_passcode" }),
488
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "view_only", children: "view_only" }),
489
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "server_claim", children: "server_claim" })
490
- ]
56
+ children: ["qr", "passcode", "gps", "nfc", "custom"].map((type) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: type, children: field(`condition.${type}`, type.toUpperCase()) }, type))
491
57
  }
492
58
  )
493
59
  ] }),
494
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
495
- /* @__PURE__ */ jsxRuntime.jsx("h4", { children: "Unlock conditions" }),
496
- conditions.map((condition, index) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
497
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
498
- "Reward condition type",
499
- " ",
500
- /* @__PURE__ */ jsxRuntime.jsxs(
501
- "select",
502
- {
503
- value: condition.type,
504
- onChange: (event) => {
505
- const type = event.target.value;
506
- const next = type === "stamps" ? { type: "stamps", stampIds: [] } : type === "all" || type === "any" ? { type, conditions: [{ type: "stamp_count", count: 1 }] } : { type: "stamp_count", count: 1 };
507
- update({
508
- conditions: conditions.map(
509
- (item, itemIndex) => itemIndex === index ? next : item
510
- )
511
- });
512
- },
513
- children: [
514
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "stamp_count", children: "Stamp count" }),
515
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "stamps", children: "Specific spots" }),
516
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "all", children: "All" }),
517
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "any", children: "Any" })
518
- ]
519
- }
520
- )
521
- ] }),
522
- condition.type === "stamp_count" && /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
523
- "Required stamp count",
524
- " ",
525
- /* @__PURE__ */ jsxRuntime.jsx(
526
- "input",
527
- {
528
- type: "number",
529
- min: 0,
530
- value: condition.count,
531
- onChange: (event) => update({
532
- conditions: conditions.map(
533
- (item, itemIndex) => itemIndex === index ? { ...condition, count: Number(event.target.value) } : item
534
- )
535
- })
536
- }
537
- )
538
- ] }),
539
- condition.type === "stamps" && /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
540
- "Specific spot IDs",
541
- " ",
542
- /* @__PURE__ */ jsxRuntime.jsx(
543
- "input",
544
- {
545
- value: condition.stampIds.join(", "),
546
- placeholder: spots.map((item) => item.id).join(", "),
547
- onChange: (event) => update({
548
- conditions: conditions.map(
549
- (item, itemIndex) => itemIndex === index ? {
550
- ...condition,
551
- stampIds: event.target.value.split(",").map((id) => id.trim()).filter(Boolean)
552
- } : item
553
- )
554
- })
555
- }
556
- )
557
- ] }),
558
- /* @__PURE__ */ jsxRuntime.jsx(
559
- "button",
560
- {
561
- type: "button",
562
- onClick: () => update({ conditions: conditions.filter((_, itemIndex) => itemIndex !== index) }),
563
- children: "Remove reward condition"
564
- }
565
- )
566
- ] }, JSON.stringify(condition))),
567
- /* @__PURE__ */ jsxRuntime.jsx(
568
- "button",
569
- {
570
- type: "button",
571
- onClick: () => update({ conditions: [...conditions, { type: "stamp_count", count: 1 }] }),
572
- children: "Add reward condition"
573
- }
574
- )
575
- ] }),
576
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: onDelete, children: "Remove reward" })
577
- ] });
578
- }
579
- function AdminRallyEditor({ config, locale = "ja", onChange }) {
580
- const spots = config.spots;
581
- const rewards = config.rewards;
582
- const theme = config.theme ?? core.DEFAULT_SHEET_THEME;
583
- const setConfig = (next) => onChange({ ...config, ...next });
584
- return /* @__PURE__ */ jsxRuntime.jsxs("section", { "aria-label": "Universal rally editor", className: "stamprally-admin-card", children: [
585
- /* @__PURE__ */ jsxRuntime.jsxs("fieldset", { children: [
586
- /* @__PURE__ */ jsxRuntime.jsx("legend", { children: "Basic settings" }),
60
+ condition2.type === "qr" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
587
61
  /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
588
- "Title (",
589
- locale,
590
- ")",
591
- " ",
62
+ field("secretToken", "QR token"),
592
63
  /* @__PURE__ */ jsxRuntime.jsx(
593
64
  "input",
594
65
  {
595
- value: universalText(config.title, locale),
596
- onChange: (event) => setConfig({
597
- title: updateUniversalText(
598
- config.title,
599
- locale,
600
- event.target.value
601
- )
602
- })
66
+ value: condition2.secretToken,
67
+ onChange: (event) => onChange({ ...condition2, secretToken: event.target.value })
603
68
  }
604
69
  )
605
70
  ] }),
606
71
  /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
607
- "Description (",
608
- locale,
609
- ")",
610
- " ",
72
+ field("qrEntryUrl", "QR entry URL"),
611
73
  /* @__PURE__ */ jsxRuntime.jsx(
612
- "textarea",
613
- {
614
- value: universalText(config.description, locale),
615
- onChange: (event) => {
616
- if (event.target.value === "") {
617
- const { description: _removed, ...rest } = config;
618
- onChange(rest);
619
- } else
620
- setConfig({
621
- description: updateUniversalText(config.description, locale, event.target.value)
622
- });
623
- }
624
- }
625
- )
626
- ] }),
627
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
628
- "Theme preset",
629
- " ",
630
- /* @__PURE__ */ jsxRuntime.jsxs(
631
- "select",
74
+ "input",
632
75
  {
633
- value: core.THEME_PRESETS.find((preset) => JSON.stringify(preset.theme) === JSON.stringify(theme))?.id ?? "custom",
634
- onChange: (event) => {
635
- const preset = core.THEME_PRESETS.find((item) => item.id === event.target.value);
636
- if (preset !== void 0) setConfig({ theme: preset.theme });
637
- },
638
- children: [
639
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "custom", children: "custom" }),
640
- core.THEME_PRESETS.map((preset) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: preset.id, children: core.resolveLocalizedText(preset.name, locale) }, preset.id))
641
- ]
76
+ value: condition2.qrEntryUrl ?? "",
77
+ onChange: (event) => onChange({ ...condition2, qrEntryUrl: event.target.value })
642
78
  }
643
79
  )
644
- ] }),
80
+ ] })
81
+ ] }),
82
+ condition2.type === "passcode" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
645
83
  /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
646
- "Primary color",
647
- " ",
84
+ field("passcode", "Passcode"),
648
85
  /* @__PURE__ */ jsxRuntime.jsx(
649
86
  "input",
650
87
  {
651
- type: "color",
652
- value: theme.primaryColor,
653
- onChange: (event) => setConfig({ theme: { ...theme, primaryColor: event.target.value } })
88
+ value: condition2.code,
89
+ onChange: (event) => onChange({ ...condition2, code: event.target.value })
654
90
  }
655
91
  )
656
92
  ] }),
657
93
  /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
658
- "Font family",
659
- " ",
660
- /* @__PURE__ */ jsxRuntime.jsxs(
661
- "select",
94
+ /* @__PURE__ */ jsxRuntime.jsx(
95
+ "input",
662
96
  {
663
- value: theme.fontFamily ?? "serif",
664
- onChange: (event) => setConfig({
665
- theme: {
666
- ...theme,
667
- fontFamily: event.target.value
668
- }
669
- }),
670
- children: [
671
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "system-ui", children: "system-ui" }),
672
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "serif", children: "serif" }),
673
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "rounded-sans", children: "rounded-sans" }),
674
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "monospace", children: "monospace" }),
675
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "handwritten", children: "handwritten" })
676
- ]
97
+ type: "checkbox",
98
+ checked: condition2.caseSensitive ?? false,
99
+ onChange: (event) => onChange({ ...condition2, caseSensitive: event.target.checked })
677
100
  }
678
- )
101
+ ),
102
+ field("caseSensitive", "Case sensitive")
679
103
  ] })
680
104
  ] }),
681
- /* @__PURE__ */ jsxRuntime.jsxs("section", { "aria-label": "Universal spots", children: [
682
- /* @__PURE__ */ jsxRuntime.jsx("h2", { children: "Spots" }),
683
- spots.map((spot, index) => /* @__PURE__ */ jsxRuntime.jsx(
684
- UniversalSpotForm,
105
+ condition2.type === "gps" && /* @__PURE__ */ jsxRuntime.jsx("div", { children: ["latitude", "longitude", "radiusMeters"].map((key) => /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
106
+ field(key, key),
107
+ /* @__PURE__ */ jsxRuntime.jsx(
108
+ "input",
685
109
  {
686
- spot,
687
- spots,
688
- onChange: (next) => setConfig({
689
- spots: spots.map(
690
- (item, itemIndex) => itemIndex === index ? { ...next, orderIndex: next.orderIndex } : item
691
- )
692
- }),
693
- onMoveUp: () => {
694
- if (index === 0) return;
695
- const current = spots[index];
696
- const previous = spots[index - 1];
697
- if (current === void 0 || previous === void 0) return;
698
- const reordered = spots.map(
699
- (item, itemIndex) => itemIndex === index - 1 ? { ...current, orderIndex: index - 1 } : itemIndex === index ? { ...previous, orderIndex: index } : item
700
- );
701
- setConfig({ spots: reordered });
702
- },
703
- onMoveDown: () => {
704
- if (index === spots.length - 1) return;
705
- const current = spots[index];
706
- const next = spots[index + 1];
707
- if (current === void 0 || next === void 0) return;
708
- const reordered = spots.map(
709
- (item, itemIndex) => itemIndex === index ? { ...next, orderIndex: index } : itemIndex === index + 1 ? { ...current, orderIndex: index + 1 } : item
710
- );
711
- setConfig({ spots: reordered });
712
- },
713
- onDelete: () => setConfig({
714
- spots: spots.filter((_, itemIndex) => itemIndex !== index).map((item, itemIndex) => ({ ...item, orderIndex: itemIndex }))
715
- })
716
- },
717
- spot.id
718
- )),
110
+ type: "number",
111
+ value: condition2[key],
112
+ onChange: (event) => onChange({ ...condition2, [key]: Number(event.target.value) })
113
+ }
114
+ )
115
+ ] }, key)) }),
116
+ condition2.type === "nfc" && /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
117
+ field("tagId", "NFC tag ID"),
719
118
  /* @__PURE__ */ jsxRuntime.jsx(
720
- "button",
119
+ "input",
721
120
  {
722
- type: "button",
723
- onClick: () => setConfig({ spots: [...spots, defaultUniversalSpot(spots.length)] }),
724
- children: "Add spot"
121
+ value: condition2.tagId,
122
+ onChange: (event) => onChange({ ...condition2, tagId: event.target.value })
725
123
  }
726
124
  )
727
125
  ] }),
728
- /* @__PURE__ */ jsxRuntime.jsxs("section", { "aria-label": "Universal rewards", children: [
729
- /* @__PURE__ */ jsxRuntime.jsx("h2", { children: "Rewards" }),
730
- rewards.map((reward, index) => /* @__PURE__ */ jsxRuntime.jsx(
731
- UniversalRewardForm,
732
- {
733
- reward,
734
- spots,
735
- onChange: (next) => setConfig({
736
- rewards: rewards.map((item, itemIndex) => itemIndex === index ? next : item)
737
- }),
738
- onDelete: () => setConfig({ rewards: rewards.filter((_, itemIndex) => itemIndex !== index) })
739
- },
740
- reward.id
741
- )),
126
+ condition2.type === "custom" && /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
127
+ field("validatorName", "Validator name"),
742
128
  /* @__PURE__ */ jsxRuntime.jsx(
743
- "button",
129
+ "input",
744
130
  {
745
- type: "button",
746
- onClick: () => setConfig({ rewards: [...rewards, defaultUniversalReward(rewards.length)] }),
747
- children: "Add reward"
131
+ value: condition2.validatorName,
132
+ onChange: (event) => onChange({ ...condition2, validatorName: event.target.value })
748
133
  }
749
134
  )
750
- ] })
135
+ ] }),
136
+ onRemove !== void 0 && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: onRemove, children: field("removeCondition", "Remove condition") })
751
137
  ] });
752
138
  }
753
- var localePair = (value) => {
754
- if (typeof value === "string") return { ja: value, en: "" };
755
- const result = {};
756
- for (const [key, text] of Object.entries(value ?? {})) {
757
- if (text !== void 0) result[key] = text;
758
- }
759
- return result;
760
- };
761
- function updateText(value, locale, next) {
762
- const current = localePair(value);
763
- return { ...current, [locale]: next };
764
- }
765
- function GeneralSettingsForm({
766
- config,
767
- locale = "ja",
768
- onChange
139
+ function SpotItemForm({
140
+ spot,
141
+ onChange,
142
+ onRemove,
143
+ locale,
144
+ dictionary
769
145
  }) {
770
- const pair = localePair(config.title);
771
- return /* @__PURE__ */ jsxRuntime.jsxs("fieldset", { className: "stamprally-admin-card", children: [
772
- /* @__PURE__ */ jsxRuntime.jsx("legend", { children: "General settings" }),
773
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
774
- "ID ",
775
- /* @__PURE__ */ jsxRuntime.jsx("input", { value: config.id, readOnly: true })
776
- ] }),
146
+ const activeLocale = locale ?? "en";
147
+ const field = (key, fallback) => text(dictionary, activeLocale, key, fallback);
148
+ const update = (patch) => onChange({ ...spot, ...patch });
149
+ return /* @__PURE__ */ jsxRuntime.jsxs("fieldset", { children: [
150
+ /* @__PURE__ */ jsxRuntime.jsx("legend", { children: core.resolveLocalizedText(spot.name, activeLocale) }),
777
151
  /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
778
- "Title (",
779
- locale,
780
- ")",
781
- " ",
152
+ field("spotName", "Spot name"),
782
153
  /* @__PURE__ */ jsxRuntime.jsx(
783
154
  "input",
784
155
  {
785
- value: pair[locale],
786
- required: true,
787
- onChange: (event) => onChange({ ...config, title: updateText(config.title, locale, event.target.value) })
156
+ value: core.resolveLocalizedText(spot.name, activeLocale),
157
+ onChange: (event) => update({ name: core.updateLocalizedField(spot.name, activeLocale, event.target.value) })
788
158
  }
789
159
  )
790
160
  ] }),
791
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
792
- "Start date",
793
- " ",
161
+ ["imageUrl", "iconUrl", "redirectUrlAfterClaim"].map((key) => /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
162
+ field(key, key),
794
163
  /* @__PURE__ */ jsxRuntime.jsx(
795
164
  "input",
796
165
  {
797
- type: "datetime-local",
798
- value: config.startDate?.slice(0, 16) ?? "",
799
- onChange: (event) => {
800
- if (event.target.value === "") {
801
- const { startDate: _startDate, ...rest } = config;
802
- onChange(rest);
803
- } else onChange({ ...config, startDate: new Date(event.target.value).toISOString() });
804
- }
166
+ value: spot[key] ?? "",
167
+ onChange: (event) => update({ [key]: event.target.value })
805
168
  }
806
169
  )
807
- ] }),
170
+ ] }, key)),
808
171
  /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
809
- "End date",
810
- " ",
172
+ field("prerequisites", "Prerequisite spots"),
811
173
  /* @__PURE__ */ jsxRuntime.jsx(
812
174
  "input",
813
175
  {
814
- type: "datetime-local",
815
- value: config.endDate?.slice(0, 16) ?? "",
816
- onChange: (event) => {
817
- if (event.target.value === "") {
818
- const { endDate: _endDate, ...rest } = config;
819
- onChange(rest);
820
- } else onChange({ ...config, endDate: new Date(event.target.value).toISOString() });
821
- }
176
+ value: spot.prerequisites?.join(", ") ?? "",
177
+ onChange: (event) => update({
178
+ prerequisites: event.target.value.split(",").map((item) => item.trim()).filter(Boolean)
179
+ })
822
180
  }
823
181
  )
824
182
  ] }),
825
183
  /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
184
+ field("externalReferences", "External references"),
826
185
  /* @__PURE__ */ jsxRuntime.jsx(
827
- "input",
828
- {
829
- type: "checkbox",
830
- checked: config.isSequential === true,
831
- onChange: (event) => onChange({ ...config, isSequential: event.target.checked })
832
- }
833
- ),
834
- " ",
835
- "Sequential check-in"
836
- ] })
837
- ] });
838
- }
839
- function ConditionEditor({ condition, onChange }) {
840
- const type = condition.type;
841
- return /* @__PURE__ */ jsxRuntime.jsxs("fieldset", { className: "stamprally-admin-card", children: [
842
- /* @__PURE__ */ jsxRuntime.jsx("legend", { children: "Check-in condition" }),
843
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
844
- "Type",
845
- " ",
846
- /* @__PURE__ */ jsxRuntime.jsxs(
847
- "select",
186
+ "textarea",
848
187
  {
849
- value: type,
188
+ value: JSON.stringify(spot.externalReferences ?? [], null, 2),
850
189
  onChange: (event) => {
851
- const next = event.target.value;
852
- if (next === "token") onChange({ type: "token", token: "" });
853
- else if (next === "geo")
854
- onChange({ type: "geo", latitude: 0, longitude: 0, radiusMeters: 100 });
855
- else onChange({ type: "instant" });
856
- },
857
- children: [
858
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "instant", children: "Instant" }),
859
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "token", children: "QR / passcode" }),
860
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "geo", children: "GPS" })
861
- ]
190
+ try {
191
+ const parsed = JSON.parse(event.target.value);
192
+ if (Array.isArray(parsed)) update({ externalReferences: parsed });
193
+ } catch {
194
+ }
195
+ }
862
196
  }
863
197
  )
864
198
  ] }),
865
- condition.type === "token" && /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
866
- "Token",
867
- " ",
199
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
200
+ field("metadata", "Metadata"),
868
201
  /* @__PURE__ */ jsxRuntime.jsx(
869
- "input",
202
+ "textarea",
870
203
  {
871
- value: condition.token,
872
- onChange: (event) => onChange({ ...condition, token: event.target.value })
204
+ value: JSON.stringify(spot.metadata ?? {}, null, 2),
205
+ onChange: (event) => {
206
+ try {
207
+ const parsed = JSON.parse(event.target.value);
208
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed))
209
+ update({ metadata: parsed });
210
+ } catch {
211
+ }
212
+ }
873
213
  }
874
214
  )
875
215
  ] }),
876
- condition.type === "geo" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
877
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
878
- "Latitude",
879
- " ",
880
- /* @__PURE__ */ jsxRuntime.jsx(
881
- "input",
882
- {
883
- type: "number",
884
- min: -90,
885
- max: 90,
886
- value: condition.latitude,
887
- onChange: (event) => onChange({ ...condition, latitude: Number(event.target.value) })
888
- }
889
- )
890
- ] }),
891
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
892
- "Longitude",
893
- " ",
894
- /* @__PURE__ */ jsxRuntime.jsx(
895
- "input",
896
- {
897
- type: "number",
898
- min: -180,
899
- max: 180,
900
- value: condition.longitude,
901
- onChange: (event) => onChange({ ...condition, longitude: Number(event.target.value) })
902
- }
903
- )
904
- ] }),
905
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
906
- "Radius (m)",
907
- " ",
908
- /* @__PURE__ */ jsxRuntime.jsx(
909
- "input",
910
- {
911
- type: "number",
912
- min: 1,
913
- value: condition.radiusMeters,
914
- onChange: (event) => onChange({ ...condition, radiusMeters: Number(event.target.value) })
915
- }
916
- )
917
- ] })
918
- ] })
216
+ spot.conditions.map((item, index) => /* @__PURE__ */ jsxRuntime.jsx(
217
+ ConditionEditor,
218
+ {
219
+ condition: item,
220
+ locale: activeLocale,
221
+ ...dictionary === void 0 ? {} : { dictionary },
222
+ onChange: (next) => update({
223
+ conditions: spot.conditions.map(
224
+ (current, itemIndex) => itemIndex === index ? next : current
225
+ )
226
+ }),
227
+ ...spot.conditions.length <= 1 ? {} : {
228
+ onRemove: () => update({
229
+ conditions: spot.conditions.filter((_, itemIndex) => itemIndex !== index)
230
+ })
231
+ }
232
+ },
233
+ `${spot.id}-condition-${JSON.stringify(item)}`
234
+ )),
235
+ /* @__PURE__ */ jsxRuntime.jsx(
236
+ "button",
237
+ {
238
+ type: "button",
239
+ onClick: () => update({ conditions: [...spot.conditions, condition()] }),
240
+ children: field("addCondition", "Add condition")
241
+ }
242
+ ),
243
+ onRemove !== void 0 && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: onRemove, children: field("removeSpot", "Remove spot") })
919
244
  ] });
920
245
  }
921
- function SpotItemForm({
922
- spot,
923
- index = 0,
924
- locale = "ja",
246
+ function RewardItemForm({
247
+ reward,
248
+ locale,
249
+ dictionary,
925
250
  onChange,
926
- onDelete
251
+ onRemove
927
252
  }) {
928
- const pair = localePair(spot.name);
929
- const setField = (field, event) => {
930
- if (event.target.value === "") {
931
- const { [field]: _removed, ...rest } = spot;
932
- onChange(rest);
933
- } else onChange({ ...spot, [field]: event.target.value });
934
- };
935
- return /* @__PURE__ */ jsxRuntime.jsxs("article", { className: "stamprally-admin-card", children: [
936
- /* @__PURE__ */ jsxRuntime.jsxs("header", { children: [
937
- /* @__PURE__ */ jsxRuntime.jsxs("strong", { children: [
938
- "Spot ",
939
- index + 1
940
- ] }),
941
- onDelete !== void 0 && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: onDelete, children: "Delete" })
942
- ] }),
253
+ const field = (key, fallback) => text(dictionary, locale, key, fallback);
254
+ return /* @__PURE__ */ jsxRuntime.jsxs("fieldset", { children: [
255
+ /* @__PURE__ */ jsxRuntime.jsx("legend", { children: core.resolveLocalizedText(reward.title, locale) }),
943
256
  /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
944
- "ID",
945
- " ",
257
+ field("rewardTitle", "Reward title"),
946
258
  /* @__PURE__ */ jsxRuntime.jsx(
947
259
  "input",
948
260
  {
949
- value: spot.id,
950
- onChange: (event) => onChange({ ...spot, id: event.target.value })
261
+ value: core.resolveLocalizedText(reward.title, locale),
262
+ onChange: (event) => onChange({
263
+ ...reward,
264
+ title: core.updateLocalizedField(reward.title, locale, event.target.value)
265
+ })
951
266
  }
952
267
  )
953
268
  ] }),
954
269
  /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
955
- "Name (",
956
- locale,
957
- ")",
958
- " ",
270
+ field("requiredSpotCount", "Required spot count"),
959
271
  /* @__PURE__ */ jsxRuntime.jsx(
960
272
  "input",
961
273
  {
962
- value: pair[locale],
963
- required: true,
964
- onChange: (event) => onChange({ ...spot, name: updateText(spot.name, locale, event.target.value) })
274
+ type: "number",
275
+ min: 0,
276
+ value: reward.requiredStampCount,
277
+ onChange: (event) => onChange({ ...reward, requiredStampCount: Number(event.target.value) })
965
278
  }
966
279
  )
967
280
  ] }),
968
281
  /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
969
- "Description (",
970
- locale,
971
- ")",
972
- " ",
973
- /* @__PURE__ */ jsxRuntime.jsx(
974
- "textarea",
282
+ field("rewardType", "Reward type"),
283
+ /* @__PURE__ */ jsxRuntime.jsxs(
284
+ "select",
975
285
  {
976
- value: core.resolveLocalizedText(spot.description, locale),
977
- onChange: (event) => onChange({
978
- ...spot,
979
- description: updateText(spot.description, locale, event.target.value)
980
- })
286
+ value: reward.type,
287
+ onChange: (event) => onChange({ ...reward, type: event.target.value }),
288
+ children: [
289
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "digital", children: field("rewardType.digital", "Digital") }),
290
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "in_person", children: field("rewardType.inPerson", "In person") })
291
+ ]
981
292
  }
982
293
  )
983
294
  ] }),
984
295
  /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
985
- "Hint (",
986
- locale,
987
- ")",
988
- " ",
989
- /* @__PURE__ */ jsxRuntime.jsx(
990
- "textarea",
296
+ field("redemptionMethod", "Redemption method"),
297
+ /* @__PURE__ */ jsxRuntime.jsxs(
298
+ "select",
991
299
  {
992
- value: core.resolveLocalizedText(spot.hint, locale),
993
- onChange: (event) => onChange({ ...spot, hint: updateText(spot.hint, locale, event.target.value) })
300
+ value: reward.redemptionMethod,
301
+ onChange: (event) => onChange({
302
+ ...reward,
303
+ redemptionMethod: event.target.value
304
+ }),
305
+ children: [
306
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "server_claim", children: field("redemption.serverClaim", "Server claim") }),
307
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "manual_slide", children: field("redemption.manualSlide", "Manual slide") }),
308
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "staff_passcode", children: field("redemption.staffPasscode", "Staff passcode") }),
309
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "view_only", children: field("redemption.viewOnly", "View only") })
310
+ ]
994
311
  }
995
312
  )
996
313
  ] }),
997
314
  /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
998
- "Order index",
999
- " ",
315
+ field("unlockConditions", "Unlock conditions"),
1000
316
  /* @__PURE__ */ jsxRuntime.jsx(
1001
- "input",
317
+ "textarea",
1002
318
  {
1003
- type: "number",
1004
- value: spot.orderIndex ?? spot.order ?? index + 1,
1005
- onChange: (event) => onChange({ ...spot, orderIndex: Number(event.target.value) })
319
+ value: JSON.stringify(reward.conditions ?? [], null, 2),
320
+ onChange: (event) => {
321
+ try {
322
+ const parsed = JSON.parse(event.target.value);
323
+ if (Array.isArray(parsed))
324
+ onChange({ ...reward, conditions: parsed });
325
+ } catch {
326
+ }
327
+ }
1006
328
  }
1007
329
  )
1008
330
  ] }),
1009
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
1010
- "Icon URL",
1011
- " ",
1012
- /* @__PURE__ */ jsxRuntime.jsx("input", { value: spot.iconUrl ?? "", onChange: (event) => setField("iconUrl", event) })
1013
- ] }),
1014
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
1015
- "Image URL",
1016
- " ",
1017
- /* @__PURE__ */ jsxRuntime.jsx("input", { value: spot.imageUrl ?? "", onChange: (event) => setField("imageUrl", event) })
1018
- ] }),
1019
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
1020
- "External URL",
1021
- " ",
331
+ ["stockLimit", "userClaimLimit"].map((key) => /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
332
+ field(key, key),
1022
333
  /* @__PURE__ */ jsxRuntime.jsx(
1023
334
  "input",
1024
335
  {
1025
- value: spot.externalUrl ?? "",
1026
- onChange: (event) => setField("externalUrl", event)
336
+ type: "number",
337
+ min: 0,
338
+ value: reward[key] ?? "",
339
+ onChange: (event) => {
340
+ if (event.target.value === "") {
341
+ const { [key]: _removed, ...withoutLimit } = reward;
342
+ onChange(withoutLimit);
343
+ } else onChange({ ...reward, [key]: Number(event.target.value) });
344
+ }
1027
345
  }
1028
346
  )
1029
- ] }),
347
+ ] }, key)),
1030
348
  /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
1031
- "Redirect URL after claim",
1032
- " ",
349
+ field("staffPasscode", "Staff passcode"),
1033
350
  /* @__PURE__ */ jsxRuntime.jsx(
1034
351
  "input",
1035
352
  {
1036
- value: spot.redirectUrlAfterClaim ?? "",
1037
- onChange: (event) => setField("redirectUrlAfterClaim", event)
353
+ value: reward.staffPasscode ?? "",
354
+ onChange: (event) => onChange({ ...reward, staffPasscode: event.target.value })
1038
355
  }
1039
356
  )
1040
357
  ] }),
1041
358
  /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
1042
- "Prerequisites (comma-separated stamp IDs)",
1043
- " ",
359
+ field("validUntil", "Valid until"),
1044
360
  /* @__PURE__ */ jsxRuntime.jsx(
1045
361
  "input",
1046
362
  {
1047
- value: (spot.requiresStampIds ?? spot.dependsOn ?? []).join(", "),
363
+ type: "datetime-local",
364
+ value: reward.validUntil?.slice(0, 16) ?? "",
1048
365
  onChange: (event) => {
1049
- const ids = event.target.value.split(",").map((item) => item.trim()).filter(Boolean);
1050
- if (ids.length === 0) {
1051
- const { requiresStampIds: _requiresStampIds, ...rest } = spot;
1052
- onChange(rest);
1053
- } else onChange({ ...spot, requiresStampIds: ids });
366
+ if (event.target.value === "") {
367
+ const { validUntil: _removed, ...withoutDate } = reward;
368
+ onChange(withoutDate);
369
+ } else onChange({ ...reward, validUntil: new Date(event.target.value).toISOString() });
1054
370
  }
1055
371
  }
1056
372
  )
1057
373
  ] }),
1058
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
1059
- "Metadata (JSON)",
1060
- " ",
374
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: onRemove, children: field("removeReward", "Remove reward") })
375
+ ] });
376
+ }
377
+ function AdminRallyEditor({ config, onChange, locale, dictionary }) {
378
+ const activeLocale = locale ?? "en";
379
+ const field = (key, fallback) => text(dictionary, activeLocale, key, fallback);
380
+ const update = (next) => onChange({ ...config, ...next });
381
+ const updateSpots = (spots) => update({ spots: spots.map((spot, index) => ({ ...spot, orderIndex: index })) });
382
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { "aria-label": field("rallyEditor", "Rally editor"), children: [
383
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { children: core.resolveLocalizedText(config.title, activeLocale) }),
384
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
385
+ field("title", "Title"),
1061
386
  /* @__PURE__ */ jsxRuntime.jsx(
1062
- "textarea",
387
+ "input",
1063
388
  {
1064
- value: JSON.stringify(spot.metadata ?? {}, null, 2),
1065
- onChange: (event) => {
1066
- try {
1067
- const parsed = JSON.parse(event.target.value);
1068
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
1069
- onChange({ ...spot, metadata: parsed });
1070
- }
1071
- } catch {
1072
- }
1073
- }
389
+ value: core.resolveLocalizedText(config.title, activeLocale),
390
+ onChange: (event) => update({ title: core.updateLocalizedField(config.title, activeLocale, event.target.value) })
1074
391
  }
1075
392
  )
1076
393
  ] }),
1077
394
  /* @__PURE__ */ jsxRuntime.jsx(
1078
- ConditionEditor,
1079
- {
1080
- condition: spot.condition,
1081
- onChange: (condition) => onChange({ ...spot, condition }),
1082
- locale
1083
- }
1084
- )
1085
- ] });
1086
- }
1087
- function SpotListEditor({
1088
- spots,
1089
- locale = "ja",
1090
- onChange
1091
- }) {
1092
- return /* @__PURE__ */ jsxRuntime.jsxs("section", { "aria-label": "Spots", children: [
1093
- /* @__PURE__ */ jsxRuntime.jsx("h2", { children: "Spots" }),
1094
- spots.map((spot, index) => /* @__PURE__ */ jsxRuntime.jsx(
1095
- SpotItemForm,
1096
- {
1097
- spot,
1098
- index,
1099
- locale,
1100
- onChange: (next) => onChange(spots.map((item, itemIndex) => itemIndex === index ? next : item)),
1101
- onDelete: () => onChange(spots.filter((_, itemIndex) => itemIndex !== index))
1102
- },
1103
- spot.id
1104
- ))
1105
- ] });
1106
- }
1107
- function RewardListEditor({
1108
- rewards,
1109
- locale = "ja",
1110
- onChange
1111
- }) {
1112
- return /* @__PURE__ */ jsxRuntime.jsxs("section", { "aria-label": "Rewards", children: [
1113
- /* @__PURE__ */ jsxRuntime.jsx("h2", { children: "Rewards" }),
1114
- rewards.map((reward, index) => /* @__PURE__ */ jsxRuntime.jsxs("article", { className: "stamprally-admin-card", children: [
1115
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
1116
- "ID",
1117
- " ",
1118
- /* @__PURE__ */ jsxRuntime.jsx(
1119
- "input",
1120
- {
1121
- value: reward.id,
1122
- onChange: (event) => onChange(
1123
- rewards.map(
1124
- (item, itemIndex) => itemIndex === index ? { ...item, id: event.target.value } : item
1125
- )
1126
- )
1127
- }
1128
- )
1129
- ] }),
1130
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
1131
- "Title (",
1132
- locale,
1133
- ")",
1134
- " ",
1135
- /* @__PURE__ */ jsxRuntime.jsx(
1136
- "input",
1137
- {
1138
- value: core.resolveLocalizedText(reward.title, locale),
1139
- onChange: (event) => onChange(
1140
- rewards.map(
1141
- (item, itemIndex) => itemIndex === index ? { ...item, title: updateText(item.title, locale, event.target.value) } : item
1142
- )
1143
- )
1144
- }
1145
- )
1146
- ] }),
1147
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
1148
- "Description (",
1149
- locale,
1150
- ")",
1151
- " ",
1152
- /* @__PURE__ */ jsxRuntime.jsx(
1153
- "textarea",
1154
- {
1155
- value: core.resolveLocalizedText(reward.description, locale),
1156
- onChange: (event) => onChange(
1157
- rewards.map(
1158
- (item, itemIndex) => itemIndex === index ? {
1159
- ...item,
1160
- description: updateText(item.description, locale, event.target.value)
1161
- } : item
1162
- )
1163
- )
1164
- }
1165
- )
1166
- ] }),
1167
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
1168
- "Required stamps",
1169
- " ",
1170
- /* @__PURE__ */ jsxRuntime.jsx(
1171
- "input",
1172
- {
1173
- type: "number",
1174
- min: 0,
1175
- value: reward.requiredStampCount,
1176
- onChange: (event) => onChange(
1177
- rewards.map(
1178
- (item, itemIndex) => itemIndex === index ? { ...item, requiredStampCount: Number(event.target.value) } : item
1179
- )
1180
- )
1181
- }
1182
- )
1183
- ] }),
1184
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
1185
- "Valid until",
1186
- " ",
1187
- /* @__PURE__ */ jsxRuntime.jsx(
1188
- "input",
1189
- {
1190
- type: "datetime-local",
1191
- value: reward.validUntil?.slice(0, 16) ?? "",
1192
- onChange: (event) => onChange(
1193
- rewards.map((item, itemIndex) => {
1194
- if (itemIndex !== index) return item;
1195
- if (event.target.value === "") {
1196
- const { validUntil: _validUntil, ...rest } = item;
1197
- return rest;
1198
- }
1199
- return { ...item, validUntil: new Date(event.target.value).toISOString() };
1200
- })
1201
- )
1202
- }
1203
- )
1204
- ] }),
1205
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
1206
- "Stock limit",
1207
- " ",
1208
- /* @__PURE__ */ jsxRuntime.jsx(
1209
- "input",
1210
- {
1211
- type: "number",
1212
- min: 1,
1213
- value: reward.stockLimit ?? reward.maxStock ?? "",
1214
- onChange: (event) => onChange(
1215
- rewards.map((item, itemIndex) => {
1216
- if (itemIndex !== index) return item;
1217
- if (event.target.value === "") {
1218
- const { stockLimit: _stockLimit, ...rest } = item;
1219
- return rest;
1220
- }
1221
- return { ...item, stockLimit: Number(event.target.value) };
1222
- })
1223
- )
1224
- }
1225
- )
1226
- ] }),
1227
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
1228
- "User claim limit",
1229
- " ",
1230
- /* @__PURE__ */ jsxRuntime.jsx(
1231
- "input",
1232
- {
1233
- type: "number",
1234
- min: 1,
1235
- value: reward.userClaimLimit ?? reward.limitPerUser ?? "",
1236
- onChange: (event) => onChange(
1237
- rewards.map((item, itemIndex) => {
1238
- if (itemIndex !== index) return item;
1239
- if (event.target.value === "") {
1240
- const { userClaimLimit: _userClaimLimit, ...rest } = item;
1241
- return rest;
1242
- }
1243
- return { ...item, userClaimLimit: Number(event.target.value) };
1244
- })
1245
- )
1246
- }
1247
- )
1248
- ] }),
1249
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
1250
- "Redemption method",
1251
- " ",
1252
- /* @__PURE__ */ jsxRuntime.jsxs(
1253
- "select",
1254
- {
1255
- value: reward.redemptionMethod,
1256
- onChange: (event) => onChange(
1257
- rewards.map(
1258
- (item, itemIndex) => itemIndex === index ? {
1259
- ...item,
1260
- redemptionMethod: event.target.value
1261
- } : item
1262
- )
1263
- ),
1264
- children: [
1265
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "manual_slide", children: "manual_slide" }),
1266
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "staff_passcode", children: "staff_passcode" }),
1267
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "view_only", children: "view_only" }),
1268
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "server_claim", children: "server_claim" })
1269
- ]
1270
- }
1271
- )
1272
- ] }),
1273
- reward.redemptionMethod === "staff_passcode" && /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
1274
- "Staff passcode",
1275
- " ",
1276
- /* @__PURE__ */ jsxRuntime.jsx(
1277
- "input",
1278
- {
1279
- type: "password",
1280
- value: reward.staffPasscode ?? "",
1281
- onChange: (event) => onChange(
1282
- rewards.map(
1283
- (item, itemIndex) => itemIndex === index ? { ...item, staffPasscode: event.target.value } : item
1284
- )
1285
- )
1286
- }
1287
- )
1288
- ] }),
1289
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
1290
- "Conditions (JSON)",
1291
- " ",
1292
- /* @__PURE__ */ jsxRuntime.jsx(
1293
- "textarea",
1294
- {
1295
- value: JSON.stringify(reward.conditions ?? [], null, 2),
1296
- onChange: (event) => {
1297
- try {
1298
- const parsed = JSON.parse(event.target.value);
1299
- if (Array.isArray(parsed)) {
1300
- onChange(
1301
- rewards.map(
1302
- (item, itemIndex) => itemIndex === index ? { ...item, conditions: parsed } : item
1303
- )
1304
- );
1305
- }
1306
- } catch {
1307
- }
1308
- }
1309
- }
1310
- )
1311
- ] })
1312
- ] }, reward.id))
1313
- ] });
1314
- }
1315
- function ThemePresetSelector({ value, locale = "ja", onChange }) {
1316
- const selected = core.THEME_PRESETS.find((preset) => JSON.stringify(preset.theme) === JSON.stringify(value))?.id ?? "custom";
1317
- return /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
1318
- "Theme preset",
1319
- " ",
1320
- /* @__PURE__ */ jsxRuntime.jsxs(
1321
- "select",
395
+ "button",
1322
396
  {
1323
- value: selected,
1324
- onChange: (event) => {
1325
- const preset = core.THEME_PRESETS.find((item) => item.id === event.target.value);
1326
- if (preset !== void 0) onChange(preset.theme);
1327
- },
1328
- children: [
1329
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "custom", children: "Custom" }),
1330
- core.THEME_PRESETS.map((preset) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: preset.id, children: core.resolveLocalizedText(preset.name, locale) }, preset.id))
1331
- ]
397
+ type: "button",
398
+ onClick: () => updateSpots([...config.spots, newSpot(config.spots.length)]),
399
+ children: field("addSpot", "Add spot")
1332
400
  }
1333
- )
1334
- ] });
1335
- }
1336
- function ThemeEditor({
1337
- theme = core.DEFAULT_SHEET_THEME,
1338
- locale = "ja",
1339
- onChange
1340
- }) {
1341
- const update = (field, value) => onChange({ ...theme, [field]: value });
1342
- return /* @__PURE__ */ jsxRuntime.jsxs("fieldset", { className: "stamprally-admin-card", children: [
1343
- /* @__PURE__ */ jsxRuntime.jsx("legend", { children: "Theme" }),
1344
- /* @__PURE__ */ jsxRuntime.jsx(ThemePresetSelector, { value: theme, locale, onChange }),
1345
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
1346
- "Primary color",
1347
- " ",
401
+ ),
402
+ /* @__PURE__ */ jsxRuntime.jsx("div", { children: config.spots.map((spot, index) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1348
403
  /* @__PURE__ */ jsxRuntime.jsx(
1349
- "input",
404
+ SpotItemForm,
1350
405
  {
1351
- type: "color",
1352
- value: theme.primaryColor,
1353
- onChange: (event) => update("primaryColor", event.target.value)
406
+ spot,
407
+ locale: activeLocale,
408
+ ...dictionary === void 0 ? {} : { dictionary },
409
+ onChange: (next) => updateSpots(
410
+ config.spots.map((current) => current.id === spot.id ? next : current)
411
+ ),
412
+ onRemove: () => updateSpots(config.spots.filter((current) => current.id !== spot.id))
1354
413
  }
1355
- )
1356
- ] }),
1357
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
1358
- "Grid columns",
1359
- " ",
414
+ ),
1360
415
  /* @__PURE__ */ jsxRuntime.jsx(
1361
- "input",
416
+ "button",
1362
417
  {
1363
- type: "number",
1364
- min: 1,
1365
- max: 6,
1366
- value: theme.gridColumns,
1367
- onChange: (event) => update("gridColumns", Number(event.target.value))
418
+ type: "button",
419
+ disabled: index === 0,
420
+ onClick: () => updateSpots(move(config.spots, index, -1)),
421
+ children: field("moveUp", "Move up")
422
+ }
423
+ ),
424
+ /* @__PURE__ */ jsxRuntime.jsx(
425
+ "button",
426
+ {
427
+ type: "button",
428
+ disabled: index === config.spots.length - 1,
429
+ onClick: () => updateSpots(move(config.spots, index, 1)),
430
+ children: field("moveDown", "Move down")
1368
431
  }
1369
432
  )
1370
- ] })
1371
- ] });
1372
- }
1373
- function QrExportModal({
1374
- open,
1375
- config,
1376
- locale = "ja",
1377
- onClose
1378
- }) {
1379
- if (!open) return null;
1380
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { role: "dialog", "aria-modal": "true", className: "stamprally-qr-modal", children: [
1381
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "stamprally-qr-modal__toolbar", children: [
1382
- /* @__PURE__ */ jsxRuntime.jsx("h2", { children: "QR print kit" }),
1383
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => window.print(), children: "Print" }),
1384
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: onClose, children: "Close" })
1385
- ] }),
1386
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "stamprally-qr-modal__grid", children: config.stamps.map((spot) => /* @__PURE__ */ jsxRuntime.jsxs("article", { children: [
1387
- /* @__PURE__ */ jsxRuntime.jsx("h3", { children: core.resolveLocalizedText(spot.name, locale) }),
1388
- /* @__PURE__ */ jsxRuntime.jsx("pre", { children: JSON.stringify({
1389
- version: core.CURRENT_RALLY_CONFIG_VERSION,
1390
- rallyId: config.id,
1391
- stampId: spot.id
1392
- }) })
1393
- ] }, spot.id)) })
1394
- ] });
1395
- }
1396
- function JsonConfigIO({
1397
- config,
1398
- onImport
1399
- }) {
1400
- const [value, setValue] = react.useState(() => JSON.stringify(core.stripSensitiveConfig(config), null, 2));
1401
- const [error, setError] = react.useState(null);
1402
- const importConfig = () => {
1403
- try {
1404
- const migrated = core.migrateRallyConfig(JSON.parse(value));
1405
- const validation = core.validateRallyConfig(migrated);
1406
- if (!validation.valid) {
1407
- setError(validation.errors.map((item) => `${item.path}: ${item.message}`).join(" "));
1408
- return;
1409
- }
1410
- onImport(migrated);
1411
- setError(null);
1412
- } catch {
1413
- setError("Invalid rally JSON.");
1414
- }
1415
- };
1416
- return /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "stamprally-admin-card", "aria-label": "JSON configuration", children: [
1417
- /* @__PURE__ */ jsxRuntime.jsx("textarea", { value, onChange: (event) => setValue(event.target.value) }),
1418
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: importConfig, children: "Import and migrate" }),
433
+ ] }, spot.id)) }),
1419
434
  /* @__PURE__ */ jsxRuntime.jsx(
1420
435
  "button",
1421
436
  {
1422
437
  type: "button",
1423
- onClick: () => navigator.clipboard?.writeText(JSON.stringify(core.stripSensitiveConfig(config), null, 2)),
1424
- children: "Copy public JSON"
438
+ onClick: () => update({ rewards: [...config.rewards, newReward(config.rewards.length)] }),
439
+ children: field("addReward", "Add reward")
1425
440
  }
1426
441
  ),
1427
- error !== null && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", children: error })
442
+ /* @__PURE__ */ jsxRuntime.jsx("div", { children: config.rewards.map((reward) => /* @__PURE__ */ jsxRuntime.jsx(
443
+ RewardItemForm,
444
+ {
445
+ reward,
446
+ locale: activeLocale,
447
+ ...dictionary === void 0 ? {} : { dictionary },
448
+ onChange: (next) => update({
449
+ rewards: config.rewards.map(
450
+ (current) => current.id === reward.id ? next : current
451
+ )
452
+ }),
453
+ onRemove: () => update({ rewards: config.rewards.filter((current) => current.id !== reward.id) })
454
+ },
455
+ reward.id
456
+ )) })
1428
457
  ] });
1429
458
  }
1430
- function RallyEditor({
459
+ var RallyEditor = AdminRallyEditor;
460
+ function GeneralSettingsForm({
1431
461
  config,
1432
- locale = "ja",
1433
- onChange
462
+ onChange,
463
+ locale,
464
+ dictionary
1434
465
  }) {
1435
- const [showQr, setShowQr] = react.useState(false);
1436
- return /* @__PURE__ */ jsxRuntime.jsxs("main", { className: "stamprally-admin", children: [
1437
- /* @__PURE__ */ jsxRuntime.jsxs("header", { children: [
1438
- /* @__PURE__ */ jsxRuntime.jsx("h1", { children: core.resolveLocalizedText(config.title, locale) || config.id }),
1439
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => setShowQr(true), children: "QR export" })
1440
- ] }),
1441
- /* @__PURE__ */ jsxRuntime.jsx(GeneralSettingsForm, { config, locale, onChange }),
1442
- config.theme === void 0 ? /* @__PURE__ */ jsxRuntime.jsx(ThemeEditor, { locale, onChange: (theme) => onChange({ ...config, theme }) }) : /* @__PURE__ */ jsxRuntime.jsx(
1443
- ThemeEditor,
1444
- {
1445
- theme: config.theme,
1446
- locale,
1447
- onChange: (theme) => onChange({ ...config, theme })
1448
- }
1449
- ),
466
+ const activeLocale = locale ?? "en";
467
+ return /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
468
+ text(dictionary, activeLocale, "title", "Rally title"),
1450
469
  /* @__PURE__ */ jsxRuntime.jsx(
1451
- SpotListEditor,
470
+ "input",
1452
471
  {
1453
- spots: config.stamps,
1454
- locale,
1455
- onChange: (stamps) => onChange({ ...config, stamps })
472
+ value: core.resolveLocalizedText(config.title, activeLocale),
473
+ onChange: (event) => onChange({
474
+ ...config,
475
+ title: core.updateLocalizedField(config.title, activeLocale, event.target.value)
476
+ })
1456
477
  }
1457
- ),
478
+ )
479
+ ] });
480
+ }
481
+ function JsonConfigIO({ config, onImport, locale, dictionary }) {
482
+ const activeLocale = locale ?? "en";
483
+ const [value, setValue] = react.useState(() => JSON.stringify(config, null, 2));
484
+ const [errors, setErrors] = react.useState([]);
485
+ const field = (key, fallback) => text(dictionary, activeLocale, key, fallback);
486
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1458
487
  /* @__PURE__ */ jsxRuntime.jsx(
1459
- RewardListEditor,
488
+ "textarea",
1460
489
  {
1461
- rewards: config.rewards ?? [],
1462
- locale,
1463
- onChange: (rewards) => onChange({ ...config, rewards })
490
+ "aria-label": field("jsonConfig", "JSON configuration"),
491
+ value,
492
+ onChange: (event) => {
493
+ setValue(event.target.value);
494
+ setErrors([]);
495
+ }
1464
496
  }
1465
497
  ),
1466
- /* @__PURE__ */ jsxRuntime.jsx(JsonConfigIO, { config, onImport: onChange }),
1467
498
  /* @__PURE__ */ jsxRuntime.jsx(
1468
- QrExportModal,
499
+ "button",
1469
500
  {
1470
- open: showQr,
1471
- config,
1472
- locale,
1473
- onClose: () => setShowQr(false)
501
+ type: "button",
502
+ onClick: () => {
503
+ try {
504
+ const result = core.safeParseAdminConfig(JSON.parse(value));
505
+ if (!result.success) {
506
+ setErrors(result.errors);
507
+ return;
508
+ }
509
+ onImport(result.data);
510
+ setErrors([]);
511
+ } catch {
512
+ setErrors([{ path: "$", message: field("invalidJson", "Invalid JSON.") }]);
513
+ }
514
+ },
515
+ children: field("import", "Import")
1474
516
  }
1475
- )
517
+ ),
518
+ errors.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("ul", { role: "alert", children: errors.map((error) => /* @__PURE__ */ jsxRuntime.jsxs("li", { children: [
519
+ error.path,
520
+ ": ",
521
+ error.message
522
+ ] }, `${error.path}-${error.message}`)) })
1476
523
  ] });
1477
524
  }
1478
525
 
@@ -1480,12 +527,7 @@ exports.AdminRallyEditor = AdminRallyEditor;
1480
527
  exports.ConditionEditor = ConditionEditor;
1481
528
  exports.GeneralSettingsForm = GeneralSettingsForm;
1482
529
  exports.JsonConfigIO = JsonConfigIO;
1483
- exports.QrExportModal = QrExportModal;
1484
530
  exports.RallyEditor = RallyEditor;
1485
- exports.RewardListEditor = RewardListEditor;
1486
531
  exports.SpotItemForm = SpotItemForm;
1487
- exports.SpotListEditor = SpotListEditor;
1488
- exports.ThemeEditor = ThemeEditor;
1489
- exports.ThemePresetSelector = ThemePresetSelector;
1490
532
  //# sourceMappingURL=index.cjs.map
1491
533
  //# sourceMappingURL=index.cjs.map