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