@stamprally/admin-ui 0.7.0 → 0.8.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,37 +1,751 @@
1
- import { toPublicRallyConfig, resolveLocalizedText, THEME_PRESETS, DEFAULT_SHEET_THEME, CURRENT_RALLY_CONFIG_VERSION, stripSensitiveConfig, validateAdminRallyConfig, migrateRallyConfig, validateRallyConfig } from '@stamprally/core';
1
+ import { DEFAULT_SHEET_THEME, THEME_PRESETS, resolveLocalizedText, CURRENT_RALLY_CONFIG_VERSION, stripSensitiveConfig, migrateRallyConfig, validateRallyConfig } 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 AdminRallyEditor({ config, onChange }) {
7
- const [value, setValue] = useState(() => JSON.stringify(config, null, 2));
8
- const [error, setError] = useState(null);
9
- const apply = () => {
10
- try {
11
- const candidate = JSON.parse(value);
12
- const result = validateAdminRallyConfig(candidate);
13
- if (!result.valid) {
14
- setError(result.errors.map((item) => `${item.path}: ${item.message}`).join(" "));
15
- return;
16
- }
17
- onChange(candidate);
18
- setError(null);
19
- } catch {
20
- setError("Invalid rally JSON.");
21
- }
6
+ function universalText(value, locale) {
7
+ return typeof value === "string" ? value : value?.[locale] ?? "";
8
+ }
9
+ function updateUniversalText(value, locale, next) {
10
+ return { ...typeof value === "string" ? {} : value, [locale]: next };
11
+ }
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()]
22
19
  };
23
- return /* @__PURE__ */ jsxs("section", { "aria-label": "Universal rally editor", className: "stamprally-admin-card", children: [
24
- /* @__PURE__ */ jsx("textarea", { value, onChange: (event) => setValue(event.target.value) }),
25
- /* @__PURE__ */ jsx("button", { type: "button", onClick: apply, children: "Apply configuration" }),
26
- /* @__PURE__ */ jsx(
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,
39
+ onChange,
40
+ onDelete,
41
+ onMoveUp,
42
+ onMoveDown
43
+ }) {
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
+ ] }),
67
+ /* @__PURE__ */ jsxs("label", { children: [
68
+ "Name (en)",
69
+ " ",
70
+ /* @__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
+ "select",
105
+ {
106
+ value: "",
107
+ onChange: (event) => {
108
+ if (event.target.value !== "")
109
+ update({ prerequisites: [...spot.prerequisites ?? [], event.target.value] });
110
+ },
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(
27
119
  "button",
28
120
  {
29
121
  type: "button",
30
- onClick: () => navigator.clipboard?.writeText(JSON.stringify(toPublicRallyConfig(config), null, 2)),
31
- children: "Copy public configuration"
32
- }
33
- ),
34
- error !== null && /* @__PURE__ */ jsx("p", { role: "alert", children: error })
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
+ ]
489
+ }
490
+ )
491
+ ] }),
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" }),
585
+ /* @__PURE__ */ jsxs("label", { children: [
586
+ "Title (",
587
+ locale,
588
+ ")",
589
+ " ",
590
+ /* @__PURE__ */ jsx(
591
+ "input",
592
+ {
593
+ value: universalText(config.title, locale),
594
+ onChange: (event) => setConfig({
595
+ title: updateUniversalText(
596
+ config.title,
597
+ locale,
598
+ event.target.value
599
+ )
600
+ })
601
+ }
602
+ )
603
+ ] }),
604
+ /* @__PURE__ */ jsxs("label", { children: [
605
+ "Description (",
606
+ locale,
607
+ ")",
608
+ " ",
609
+ /* @__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",
630
+ {
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
+ ]
640
+ }
641
+ )
642
+ ] }),
643
+ /* @__PURE__ */ jsxs("label", { children: [
644
+ "Primary color",
645
+ " ",
646
+ /* @__PURE__ */ jsx(
647
+ "input",
648
+ {
649
+ type: "color",
650
+ value: theme.primaryColor,
651
+ onChange: (event) => setConfig({ theme: { ...theme, primaryColor: event.target.value } })
652
+ }
653
+ )
654
+ ] }),
655
+ /* @__PURE__ */ jsxs("label", { children: [
656
+ "Font family",
657
+ " ",
658
+ /* @__PURE__ */ jsxs(
659
+ "select",
660
+ {
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
+ ]
675
+ }
676
+ )
677
+ ] })
678
+ ] }),
679
+ /* @__PURE__ */ jsxs("section", { "aria-label": "Universal spots", children: [
680
+ /* @__PURE__ */ jsx("h2", { children: "Spots" }),
681
+ spots.map((spot, index) => /* @__PURE__ */ jsx(
682
+ UniversalSpotForm,
683
+ {
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
+ )),
717
+ /* @__PURE__ */ jsx(
718
+ "button",
719
+ {
720
+ type: "button",
721
+ onClick: () => setConfig({ spots: [...spots, defaultUniversalSpot(spots.length)] }),
722
+ children: "Add spot"
723
+ }
724
+ )
725
+ ] }),
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
+ )),
740
+ /* @__PURE__ */ jsx(
741
+ "button",
742
+ {
743
+ type: "button",
744
+ onClick: () => setConfig({ rewards: [...rewards, defaultUniversalReward(rewards.length)] }),
745
+ children: "Add reward"
746
+ }
747
+ )
748
+ ] })
35
749
  ] });
36
750
  }
37
751
  var localePair = (value) => {