@stamprally/admin-ui 0.11.0 → 0.13.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,28 +1,183 @@
1
- import { resolveLocalizedText, updateLocalizedField, safeParseAdminConfig } from '@stamprally/core';
2
- import { useState } from 'react';
1
+ import { resolveLocalizedText, updateLocalizedField, DEFAULT_SHEET_THEME, safeParseAdminConfig } from '@stamprally/core';
2
+ import { useRef, useState, useEffect, useCallback } from 'react';
3
3
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
4
4
 
5
5
  // src/index.tsx
6
+ function nextEntityId(prefix, ids) {
7
+ let index = ids.size + 1;
8
+ let candidate = `${prefix}-${index}`;
9
+ while (ids.has(candidate)) {
10
+ index += 1;
11
+ candidate = `${prefix}-${index}`;
12
+ }
13
+ return candidate;
14
+ }
15
+ function pathParts(path) {
16
+ return path.replaceAll("[", ".").replaceAll("]", "").split(".").filter((part) => part !== "");
17
+ }
18
+ function localizedValue(value, locale, nextValue) {
19
+ return updateLocalizedField(
20
+ typeof value === "string" || typeof value === "object" && value !== null ? value : "",
21
+ locale,
22
+ nextValue
23
+ );
24
+ }
6
25
  function useAdminRallyEditor(initialConfig, options = {}) {
7
- const [config, setConfig] = useState(initialConfig);
8
- const update = (patch) => {
9
- setConfig((current) => {
10
- const next = { ...current, ...patch };
11
- options.onChange?.(next);
12
- return next;
13
- });
14
- };
15
- const updateSpot = (spotId, patch) => {
16
- update({
17
- spots: config.spots.map((spot) => spot.id === spotId ? { ...spot, ...patch } : spot)
18
- });
19
- };
20
- const updateReward = (rewardId, patch) => {
21
- update({
22
- rewards: config.rewards.map(
23
- (reward) => reward.id === rewardId ? { ...reward, ...patch } : reward
24
- )
25
- });
26
+ const initialRef = useRef(initialConfig);
27
+ const previousInitialRef = useRef(initialConfig);
28
+ const [history, setHistory] = useState({
29
+ past: [],
30
+ present: initialConfig,
31
+ future: []
32
+ });
33
+ const config = history.present;
34
+ useEffect(() => {
35
+ if (previousInitialRef.current === initialConfig) return;
36
+ previousInitialRef.current = initialConfig;
37
+ const isDirty = JSON.stringify(history.present) !== JSON.stringify(initialRef.current);
38
+ if (!isDirty) {
39
+ initialRef.current = initialConfig;
40
+ setHistory({ past: [], present: initialConfig, future: [] });
41
+ }
42
+ }, [history.present, initialConfig]);
43
+ const commit = useCallback(
44
+ (value) => {
45
+ setHistory((current) => {
46
+ const next = typeof value === "function" ? value(current.present) : value;
47
+ if (next === current.present) return current;
48
+ options.onChange?.(next);
49
+ return { past: [...current.past, current.present], present: next, future: [] };
50
+ });
51
+ },
52
+ [options.onChange]
53
+ );
54
+ const setConfig = commit;
55
+ const update = (patch) => commit((current) => ({ ...current, ...patch }));
56
+ const updateSpot = (spotId, patch) => commit((current) => ({
57
+ ...current,
58
+ spots: current.spots.map((spot) => spot.id === spotId ? { ...spot, ...patch } : spot)
59
+ }));
60
+ const updateReward = (rewardId, patch) => commit((current) => ({
61
+ ...current,
62
+ rewards: current.rewards.map(
63
+ (reward) => reward.id === rewardId ? { ...reward, ...patch } : reward
64
+ )
65
+ }));
66
+ const addSpot = (spotData = {}) => commit((current) => {
67
+ const id = spotData.id ?? nextEntityId("spot", new Set(current.spots.map((spot) => spot.id)));
68
+ const base = newSpot(current.spots.length);
69
+ return {
70
+ ...current,
71
+ spots: [...current.spots, { ...base, ...spotData, id, orderIndex: current.spots.length }]
72
+ };
73
+ });
74
+ const removeSpot = (spotId) => commit((current) => ({
75
+ ...current,
76
+ spots: current.spots.filter((spot) => spot.id !== spotId).map((spot, index) => ({ ...spot, orderIndex: index }))
77
+ }));
78
+ const reorderSpots = (fromIndex, toIndex) => commit((current) => ({ ...current, spots: moveTo(current.spots, fromIndex, toIndex) }));
79
+ const reorderRewards = (fromIndex, toIndex) => commit((current) => ({ ...current, rewards: moveTo(current.rewards, fromIndex, toIndex) }));
80
+ const duplicateSpot = (spotId) => commit((current) => {
81
+ const source = current.spots.find((spot) => spot.id === spotId);
82
+ if (source === void 0) return current;
83
+ const id = nextEntityId("spot-copy", new Set(current.spots.map((spot) => spot.id)));
84
+ const copy = { ...structuredClone(source), id, orderIndex: current.spots.length };
85
+ return { ...current, spots: [...current.spots, copy] };
86
+ });
87
+ const addReward = (rewardData = {}) => commit((current) => {
88
+ const id = rewardData.id ?? nextEntityId("reward", new Set(current.rewards.map((item) => item.id)));
89
+ return {
90
+ ...current,
91
+ rewards: [...current.rewards, { ...newReward(current.rewards.length), ...rewardData, id }]
92
+ };
93
+ });
94
+ const removeReward = (rewardId) => commit((current) => ({
95
+ ...current,
96
+ rewards: current.rewards.filter((item) => item.id !== rewardId)
97
+ }));
98
+ const duplicateReward = (rewardId) => commit((current) => {
99
+ const source = current.rewards.find((reward) => reward.id === rewardId);
100
+ if (source === void 0) return current;
101
+ const id = nextEntityId("reward-copy", new Set(current.rewards.map((item) => item.id)));
102
+ return { ...current, rewards: [...current.rewards, { ...structuredClone(source), id }] };
103
+ });
104
+ const addCondition = (spotId, nextCondition) => commit((current) => ({
105
+ ...current,
106
+ spots: current.spots.map(
107
+ (spot) => spot.id !== spotId ? spot : { ...spot, conditions: [...spot.conditions, structuredClone(nextCondition)] }
108
+ )
109
+ }));
110
+ const removeCondition = (spotId, conditionIndex) => commit((current) => ({
111
+ ...current,
112
+ spots: current.spots.map(
113
+ (spot) => spot.id !== spotId ? spot : { ...spot, conditions: spot.conditions.filter((_, index) => index !== conditionIndex) }
114
+ )
115
+ }));
116
+ const updateLocalized = (path, locale, value) => commit((current) => {
117
+ const parts = pathParts(path);
118
+ if (parts[0] === "spots" && parts.length >= 3) {
119
+ const requestedIndex = parts[1];
120
+ const index = requestedIndex !== void 0 && /^\d+$/.test(requestedIndex) ? Number(requestedIndex) : current.spots.findIndex((spot) => spot.id === requestedIndex);
121
+ const field2 = parts[2];
122
+ if (index < 0 || field2 === void 0 || current.spots[index] === void 0) return current;
123
+ return {
124
+ ...current,
125
+ spots: current.spots.map(
126
+ (spot, spotIndex) => spotIndex === index ? {
127
+ ...spot,
128
+ [field2]: localizedValue(spot[field2], locale, value)
129
+ } : spot
130
+ )
131
+ };
132
+ }
133
+ if (parts[0] === "rewards" && parts.length >= 3) {
134
+ const requestedIndex = parts[1];
135
+ const index = requestedIndex !== void 0 && /^\d+$/.test(requestedIndex) ? Number(requestedIndex) : current.rewards.findIndex((reward) => reward.id === requestedIndex);
136
+ const field2 = parts[2];
137
+ if (index < 0 || field2 === void 0 || current.rewards[index] === void 0)
138
+ return current;
139
+ return {
140
+ ...current,
141
+ rewards: current.rewards.map(
142
+ (reward, rewardIndex) => rewardIndex === index ? {
143
+ ...reward,
144
+ [field2]: localizedValue(reward[field2], locale, value)
145
+ } : reward
146
+ )
147
+ };
148
+ }
149
+ const field = parts[0];
150
+ if (field === void 0) return current;
151
+ return {
152
+ ...current,
153
+ [field]: localizedValue(current[field], locale, value)
154
+ };
155
+ });
156
+ const undo = () => setHistory((current) => {
157
+ const previous = current.past.at(-1);
158
+ if (previous === void 0) return current;
159
+ options.onChange?.(previous);
160
+ return {
161
+ past: current.past.slice(0, -1),
162
+ present: previous,
163
+ future: [current.present, ...current.future]
164
+ };
165
+ });
166
+ const redo = () => setHistory((current) => {
167
+ const next = current.future[0];
168
+ if (next === void 0) return current;
169
+ options.onChange?.(next);
170
+ return {
171
+ past: [...current.past, current.present],
172
+ present: next,
173
+ future: current.future.slice(1)
174
+ };
175
+ });
176
+ const resetConfig = (newConfig) => {
177
+ initialRef.current = newConfig;
178
+ previousInitialRef.current = newConfig;
179
+ setHistory({ past: [], present: newConfig, future: [] });
180
+ options.onChange?.(newConfig);
26
181
  };
27
182
  return {
28
183
  config,
@@ -30,17 +185,37 @@ function useAdminRallyEditor(initialConfig, options = {}) {
30
185
  update,
31
186
  updateSpot,
32
187
  updateReward,
33
- reset: () => setConfig(initialConfig),
34
- isDirty: config !== initialConfig
188
+ addSpot,
189
+ removeSpot,
190
+ reorderSpots: (fromIndex, toIndex) => reorderSpots(fromIndex, toIndex),
191
+ reorderRewards,
192
+ duplicateSpot,
193
+ addReward,
194
+ removeReward,
195
+ duplicateReward,
196
+ addCondition,
197
+ removeCondition,
198
+ updateLocalizedField: updateLocalized,
199
+ undo,
200
+ redo,
201
+ canUndo: history.past.length > 0,
202
+ canRedo: history.future.length > 0,
203
+ resetConfig,
204
+ reset: () => resetConfig(initialRef.current),
205
+ isDirty: config !== initialRef.current
35
206
  };
36
207
  }
37
208
  function useSpotEditor(spotId, options = {}) {
38
209
  const [config, setConfig] = useState(
39
210
  options.config ?? options.initialConfig
40
211
  );
41
- const commit = (next) => {
42
- setConfig(next);
43
- options.onChange?.(next);
212
+ const commit = (updateConfig) => {
213
+ setConfig((current) => {
214
+ if (current === void 0) return current;
215
+ const next = updateConfig(current);
216
+ options.onChange?.(next);
217
+ return next;
218
+ });
44
219
  };
45
220
  const spot = config?.spots.find((item) => item.id === spotId);
46
221
  return {
@@ -49,14 +224,17 @@ function useSpotEditor(spotId, options = {}) {
49
224
  setConfig,
50
225
  update: (patch) => {
51
226
  if (config === void 0 || spot === void 0) return;
52
- commit({
53
- ...config,
54
- spots: config.spots.map((item) => item.id === spotId ? { ...item, ...patch } : item)
55
- });
227
+ commit((current) => ({
228
+ ...current,
229
+ spots: current.spots.map((item) => item.id === spotId ? { ...item, ...patch } : item)
230
+ }));
56
231
  },
57
232
  remove: () => {
58
233
  if (config === void 0 || spot === void 0) return;
59
- commit({ ...config, spots: config.spots.filter((item) => item.id !== spotId) });
234
+ commit((current) => ({
235
+ ...current,
236
+ spots: current.spots.filter((item) => item.id !== spotId)
237
+ }));
60
238
  }
61
239
  };
62
240
  }
@@ -65,9 +243,13 @@ function useRewardEditor(rewardId, options = {}) {
65
243
  options.config ?? options.initialConfig
66
244
  );
67
245
  const reward = config?.rewards.find((item) => item.id === rewardId);
68
- const commit = (next) => {
69
- setConfig(next);
70
- options.onChange?.(next);
246
+ const commit = (updateConfig) => {
247
+ setConfig((current) => {
248
+ if (current === void 0) return current;
249
+ const next = updateConfig(current);
250
+ options.onChange?.(next);
251
+ return next;
252
+ });
71
253
  };
72
254
  return {
73
255
  config,
@@ -75,16 +257,19 @@ function useRewardEditor(rewardId, options = {}) {
75
257
  setConfig,
76
258
  update: (patch) => {
77
259
  if (config === void 0 || reward === void 0) return;
78
- commit({
79
- ...config,
80
- rewards: config.rewards.map(
260
+ commit((current) => ({
261
+ ...current,
262
+ rewards: current.rewards.map(
81
263
  (item) => item.id === rewardId ? { ...item, ...patch } : item
82
264
  )
83
- });
265
+ }));
84
266
  },
85
267
  remove: () => {
86
268
  if (config === void 0 || reward === void 0) return;
87
- commit({ ...config, rewards: config.rewards.filter((item) => item.id !== rewardId) });
269
+ commit((current) => ({
270
+ ...current,
271
+ rewards: current.rewards.filter((item) => item.id !== rewardId)
272
+ }));
88
273
  }
89
274
  };
90
275
  }
@@ -113,6 +298,16 @@ function move(items, index, direction) {
113
298
  if (item !== void 0) next.splice(target, 0, item);
114
299
  return next;
115
300
  }
301
+ function moveTo(items, fromIndex, toIndex) {
302
+ if (fromIndex < 0 || fromIndex >= items.length || toIndex < 0 || toIndex >= items.length || fromIndex === toIndex)
303
+ return items;
304
+ const next = [...items];
305
+ const [item] = next.splice(fromIndex, 1);
306
+ if (item !== void 0) next.splice(toIndex, 0, item);
307
+ return next.map(
308
+ (entry, index) => typeof entry === "object" && entry !== null && "orderIndex" in entry ? { ...entry, orderIndex: index } : entry
309
+ );
310
+ }
116
311
  function ConditionEditor({
117
312
  condition: condition2,
118
313
  onChange,
@@ -275,39 +470,23 @@ function SpotItemForm({
275
470
  }
276
471
  )
277
472
  ] }),
278
- /* @__PURE__ */ jsxs("label", { children: [
279
- field("externalReferences", "External references"),
280
- /* @__PURE__ */ jsx(
281
- "textarea",
282
- {
283
- value: JSON.stringify(spot.externalReferences ?? [], null, 2),
284
- onChange: (event) => {
285
- try {
286
- const parsed = JSON.parse(event.target.value);
287
- if (Array.isArray(parsed)) update({ externalReferences: parsed });
288
- } catch {
289
- }
290
- }
291
- }
292
- )
293
- ] }),
294
- /* @__PURE__ */ jsxs("label", { children: [
295
- field("metadata", "Metadata"),
296
- /* @__PURE__ */ jsx(
297
- "textarea",
298
- {
299
- value: JSON.stringify(spot.metadata ?? {}, null, 2),
300
- onChange: (event) => {
301
- try {
302
- const parsed = JSON.parse(event.target.value);
303
- if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed))
304
- update({ metadata: parsed });
305
- } catch {
306
- }
307
- }
308
- }
309
- )
310
- ] }),
473
+ /* @__PURE__ */ jsx(
474
+ ExternalReferencesEditor,
475
+ {
476
+ references: spot.externalReferences ?? [],
477
+ onChange: (externalReferences) => update({ externalReferences }),
478
+ label: field("externalReferences", "External references")
479
+ }
480
+ ),
481
+ /* @__PURE__ */ jsx(
482
+ MetadataSection,
483
+ {
484
+ name: `spot-${spot.id}`,
485
+ values: spot.metadata ?? {},
486
+ onChange: (metadata) => update({ metadata }),
487
+ label: field("metadata", "Metadata")
488
+ }
489
+ ),
311
490
  spot.conditions.map((item, index) => /* @__PURE__ */ jsx(
312
491
  ConditionEditor,
313
492
  {
@@ -338,6 +517,92 @@ function SpotItemForm({
338
517
  onRemove !== void 0 && /* @__PURE__ */ jsx("button", { type: "button", onClick: onRemove, children: field("removeSpot", "Remove spot") })
339
518
  ] });
340
519
  }
520
+ function newUnlockCondition(type) {
521
+ if (type === "stamp_count") return { type, count: 1 };
522
+ if (type === "stamps") return { type, stampIds: [] };
523
+ return { type, conditions: [] };
524
+ }
525
+ function RewardUnlockConditionEditor({
526
+ condition: unlock,
527
+ onChange,
528
+ onRemove
529
+ }) {
530
+ return /* @__PURE__ */ jsxs("fieldset", { children: [
531
+ /* @__PURE__ */ jsx("legend", { children: "Unlock condition" }),
532
+ /* @__PURE__ */ jsxs("label", { children: [
533
+ "Type",
534
+ /* @__PURE__ */ jsxs(
535
+ "select",
536
+ {
537
+ value: unlock.type,
538
+ onChange: (event) => onChange(newUnlockCondition(event.target.value)),
539
+ children: [
540
+ /* @__PURE__ */ jsx("option", { value: "stamp_count", children: "Stamp count" }),
541
+ /* @__PURE__ */ jsx("option", { value: "stamps", children: "Specific stamps" }),
542
+ /* @__PURE__ */ jsx("option", { value: "all", children: "All conditions" }),
543
+ /* @__PURE__ */ jsx("option", { value: "any", children: "Any condition" })
544
+ ]
545
+ }
546
+ )
547
+ ] }),
548
+ unlock.type === "stamp_count" && /* @__PURE__ */ jsxs("label", { children: [
549
+ "Count",
550
+ /* @__PURE__ */ jsx(
551
+ "input",
552
+ {
553
+ type: "number",
554
+ min: 0,
555
+ value: unlock.count,
556
+ onChange: (event) => onChange({ ...unlock, count: Number(event.target.value) })
557
+ }
558
+ )
559
+ ] }),
560
+ unlock.type === "stamps" && /* @__PURE__ */ jsxs("label", { children: [
561
+ "Stamp IDs",
562
+ /* @__PURE__ */ jsx(
563
+ "input",
564
+ {
565
+ value: unlock.stampIds.join(", "),
566
+ onChange: (event) => onChange({
567
+ ...unlock,
568
+ stampIds: event.target.value.split(",").map((value) => value.trim()).filter(Boolean)
569
+ })
570
+ }
571
+ )
572
+ ] }),
573
+ (unlock.type === "all" || unlock.type === "any") && /* @__PURE__ */ jsxs(Fragment, { children: [
574
+ unlock.conditions.map((child, index) => /* @__PURE__ */ jsx(
575
+ RewardUnlockConditionEditor,
576
+ {
577
+ condition: child,
578
+ onChange: (next) => onChange({
579
+ ...unlock,
580
+ conditions: unlock.conditions.map(
581
+ (current, childIndex) => childIndex === index ? next : current
582
+ )
583
+ }),
584
+ onRemove: () => onChange({
585
+ ...unlock,
586
+ conditions: unlock.conditions.filter((_, childIndex) => childIndex !== index)
587
+ })
588
+ },
589
+ `${unlock.type}-${JSON.stringify(child)}`
590
+ )),
591
+ /* @__PURE__ */ jsx(
592
+ "button",
593
+ {
594
+ type: "button",
595
+ onClick: () => onChange({
596
+ ...unlock,
597
+ conditions: [...unlock.conditions, newUnlockCondition("stamp_count")]
598
+ }),
599
+ children: "Add nested condition"
600
+ }
601
+ )
602
+ ] }),
603
+ onRemove !== void 0 && /* @__PURE__ */ jsx("button", { type: "button", onClick: onRemove, children: "Remove condition" })
604
+ ] });
605
+ }
341
606
  function RewardItemForm({
342
607
  reward,
343
608
  locale,
@@ -423,20 +688,36 @@ function RewardItemForm({
423
688
  }
424
689
  )
425
690
  ] }),
426
- /* @__PURE__ */ jsxs("label", { children: [
427
- field("unlockConditions", "Unlock conditions"),
691
+ /* @__PURE__ */ jsxs("fieldset", { children: [
692
+ /* @__PURE__ */ jsx("legend", { children: field("unlockConditions", "Unlock conditions") }),
693
+ (reward.conditions ?? []).map((unlock, index) => /* @__PURE__ */ jsx(
694
+ RewardUnlockConditionEditor,
695
+ {
696
+ condition: unlock,
697
+ onChange: (next) => onChange({
698
+ ...reward,
699
+ conditions: (reward.conditions ?? []).map(
700
+ (current, conditionIndex) => conditionIndex === index ? next : current
701
+ )
702
+ }),
703
+ onRemove: () => onChange({
704
+ ...reward,
705
+ conditions: (reward.conditions ?? []).filter(
706
+ (_, conditionIndex) => conditionIndex !== index
707
+ )
708
+ })
709
+ },
710
+ `${reward.id}-unlock-${JSON.stringify(unlock)}`
711
+ )),
428
712
  /* @__PURE__ */ jsx(
429
- "textarea",
713
+ "button",
430
714
  {
431
- value: JSON.stringify(reward.conditions ?? [], null, 2),
432
- onChange: (event) => {
433
- try {
434
- const parsed = JSON.parse(event.target.value);
435
- if (Array.isArray(parsed))
436
- onChange({ ...reward, conditions: parsed });
437
- } catch {
438
- }
439
- }
715
+ type: "button",
716
+ onClick: () => onChange({
717
+ ...reward,
718
+ conditions: [...reward.conditions ?? [], newUnlockCondition("stamp_count")]
719
+ }),
720
+ children: field("addUnlockCondition", "Add unlock condition")
440
721
  }
441
722
  )
442
723
  ] }),
@@ -496,6 +777,257 @@ function RewardItemForm({
496
777
  /* @__PURE__ */ jsx("button", { type: "button", onClick: onRemove, children: field("removeReward", "Remove reward") })
497
778
  ] });
498
779
  }
780
+ function ThemeEditor({
781
+ theme,
782
+ onChange,
783
+ locale,
784
+ dictionary
785
+ }) {
786
+ const activeLocale = locale ?? "en";
787
+ const field = (key, fallback) => text(dictionary, activeLocale, key, fallback);
788
+ const update = (key, value) => onChange({ ...theme, [key]: value });
789
+ return /* @__PURE__ */ jsxs("fieldset", { children: [
790
+ /* @__PURE__ */ jsx("legend", { children: field("theme", "Theme") }),
791
+ [
792
+ "primaryColor",
793
+ "backgroundColor",
794
+ "cardBackgroundColor",
795
+ "textColor",
796
+ "backgroundImageUrl",
797
+ "completedStampColor"
798
+ ].map((key) => /* @__PURE__ */ jsxs("label", { children: [
799
+ field(key, key),
800
+ /* @__PURE__ */ jsx(
801
+ "input",
802
+ {
803
+ type: key.toLowerCase().includes("color") ? "color" : "url",
804
+ value: theme[key] ?? "",
805
+ onChange: (event) => update(key, event.target.value)
806
+ }
807
+ )
808
+ ] }, key)),
809
+ /* @__PURE__ */ jsxs("label", { children: [
810
+ field("slotShape", "Slot shape"),
811
+ /* @__PURE__ */ jsx(
812
+ "select",
813
+ {
814
+ value: theme.slotShape,
815
+ onChange: (event) => update("slotShape", event.target.value),
816
+ children: ["circle", "square", "rounded"].map((shape) => /* @__PURE__ */ jsx("option", { value: shape, children: field(`slotShape.${shape}`, shape) }, shape))
817
+ }
818
+ )
819
+ ] }),
820
+ /* @__PURE__ */ jsxs("label", { children: [
821
+ field("gridColumns", "Grid columns"),
822
+ /* @__PURE__ */ jsx(
823
+ "input",
824
+ {
825
+ type: "number",
826
+ min: 1,
827
+ max: 12,
828
+ value: theme.gridColumns,
829
+ onChange: (event) => update("gridColumns", Number(event.target.value))
830
+ }
831
+ )
832
+ ] }),
833
+ /* @__PURE__ */ jsxs("label", { children: [
834
+ field("fontFamily", "Font family"),
835
+ /* @__PURE__ */ jsx(
836
+ "select",
837
+ {
838
+ value: theme.fontFamily ?? "system-ui",
839
+ onChange: (event) => update("fontFamily", event.target.value),
840
+ children: ["system-ui", "serif", "rounded-sans", "monospace", "handwritten"].map(
841
+ (font) => /* @__PURE__ */ jsx("option", { value: font, children: font }, font)
842
+ )
843
+ }
844
+ )
845
+ ] }),
846
+ /* @__PURE__ */ jsxs("label", { children: [
847
+ field("unclaimedOpacity", "Unclaimed opacity"),
848
+ /* @__PURE__ */ jsx(
849
+ "input",
850
+ {
851
+ type: "number",
852
+ min: 0,
853
+ max: 1,
854
+ step: 0.05,
855
+ value: theme.unclaimedOpacity ?? 1,
856
+ onChange: (event) => update("unclaimedOpacity", Number(event.target.value))
857
+ }
858
+ )
859
+ ] })
860
+ ] });
861
+ }
862
+ function metadataValue(value) {
863
+ return typeof value === "string" ? value : JSON.stringify(value);
864
+ }
865
+ function ExternalReferencesEditor({
866
+ references,
867
+ onChange,
868
+ label = "External references"
869
+ }) {
870
+ return /* @__PURE__ */ jsxs("fieldset", { children: [
871
+ /* @__PURE__ */ jsx("legend", { children: label }),
872
+ references.map((reference) => /* @__PURE__ */ jsxs("div", { children: [
873
+ /* @__PURE__ */ jsxs("label", { children: [
874
+ "Type",
875
+ /* @__PURE__ */ jsx(
876
+ "input",
877
+ {
878
+ value: reference.type,
879
+ onChange: (event) => onChange(
880
+ references.map(
881
+ (current) => current === reference ? { ...current, type: event.target.value } : current
882
+ )
883
+ )
884
+ }
885
+ )
886
+ ] }),
887
+ /* @__PURE__ */ jsxs("label", { children: [
888
+ "ID",
889
+ /* @__PURE__ */ jsx(
890
+ "input",
891
+ {
892
+ value: reference.id,
893
+ onChange: (event) => onChange(
894
+ references.map(
895
+ (current) => current === reference ? { ...current, id: event.target.value } : current
896
+ )
897
+ )
898
+ }
899
+ )
900
+ ] }),
901
+ /* @__PURE__ */ jsxs("label", { children: [
902
+ "URL",
903
+ /* @__PURE__ */ jsx(
904
+ "input",
905
+ {
906
+ type: "url",
907
+ value: reference.url ?? "",
908
+ onChange: (event) => onChange(
909
+ references.map(
910
+ (current) => current === reference ? { ...current, url: event.target.value } : current
911
+ )
912
+ )
913
+ }
914
+ )
915
+ ] }),
916
+ /* @__PURE__ */ jsx(
917
+ "button",
918
+ {
919
+ type: "button",
920
+ onClick: () => onChange(references.filter((current) => current !== reference)),
921
+ children: "Remove reference"
922
+ }
923
+ )
924
+ ] }, `${reference.type}-${reference.id}-${reference.url ?? ""}`)),
925
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => onChange([...references, { type: "", id: "" }]), children: "Add reference" })
926
+ ] });
927
+ }
928
+ function MetadataSection({
929
+ name,
930
+ values,
931
+ onChange,
932
+ label
933
+ }) {
934
+ const entries = Object.entries(values);
935
+ return /* @__PURE__ */ jsxs("fieldset", { children: [
936
+ /* @__PURE__ */ jsx("legend", { children: label }),
937
+ entries.map(([key, value]) => /* @__PURE__ */ jsxs("div", { children: [
938
+ /* @__PURE__ */ jsxs("label", { children: [
939
+ "Key",
940
+ /* @__PURE__ */ jsx(
941
+ "input",
942
+ {
943
+ value: key,
944
+ onChange: (event) => {
945
+ const next = { ...values };
946
+ delete next[key];
947
+ next[event.target.value] = value;
948
+ onChange(next);
949
+ }
950
+ }
951
+ )
952
+ ] }),
953
+ /* @__PURE__ */ jsxs("label", { children: [
954
+ "Value",
955
+ /* @__PURE__ */ jsx(
956
+ "input",
957
+ {
958
+ value: metadataValue(value),
959
+ onChange: (event) => {
960
+ let nextValue = event.target.value;
961
+ try {
962
+ nextValue = JSON.parse(event.target.value);
963
+ } catch {
964
+ }
965
+ onChange({ ...values, [key]: nextValue });
966
+ }
967
+ }
968
+ )
969
+ ] }),
970
+ /* @__PURE__ */ jsx(
971
+ "button",
972
+ {
973
+ type: "button",
974
+ onClick: () => {
975
+ const next = { ...values };
976
+ delete next[key];
977
+ onChange(next);
978
+ },
979
+ children: "Remove"
980
+ }
981
+ )
982
+ ] }, `${name}-${key}`)),
983
+ /* @__PURE__ */ jsx(
984
+ "button",
985
+ {
986
+ type: "button",
987
+ onClick: () => {
988
+ let index = entries.length + 1;
989
+ let key = `key${index}`;
990
+ while (key in values) {
991
+ index += 1;
992
+ key = `key${index}`;
993
+ }
994
+ onChange({ ...values, [key]: "" });
995
+ },
996
+ children: "Add field"
997
+ }
998
+ )
999
+ ] });
1000
+ }
1001
+ function MetadataEditor({
1002
+ publicMetadata = {},
1003
+ serverMetadata = {},
1004
+ onPublicMetadataChange,
1005
+ onServerMetadataChange,
1006
+ locale,
1007
+ dictionary
1008
+ }) {
1009
+ const activeLocale = locale ?? "en";
1010
+ return /* @__PURE__ */ jsxs("section", { "aria-label": text(dictionary, activeLocale, "metadata", "Metadata"), children: [
1011
+ /* @__PURE__ */ jsx(
1012
+ MetadataSection,
1013
+ {
1014
+ name: "public",
1015
+ values: publicMetadata,
1016
+ onChange: onPublicMetadataChange,
1017
+ label: "Public metadata"
1018
+ }
1019
+ ),
1020
+ /* @__PURE__ */ jsx(
1021
+ MetadataSection,
1022
+ {
1023
+ name: "server",
1024
+ values: serverMetadata,
1025
+ onChange: onServerMetadataChange,
1026
+ label: "Server metadata"
1027
+ }
1028
+ )
1029
+ ] });
1030
+ }
499
1031
  function AdminRallyEditor({ config, onChange, locale, dictionary }) {
500
1032
  const activeLocale = locale ?? "en";
501
1033
  const field = (key, fallback) => text(dictionary, activeLocale, key, fallback);
@@ -529,45 +1061,36 @@ function AdminRallyEditor({ config, onChange, locale, dictionary }) {
529
1061
  }
530
1062
  )
531
1063
  ] }),
1064
+ /* @__PURE__ */ jsx(
1065
+ ThemeEditor,
1066
+ {
1067
+ theme: config.theme ?? DEFAULT_SHEET_THEME,
1068
+ locale: activeLocale,
1069
+ ...dictionary === void 0 ? {} : { dictionary },
1070
+ onChange: (theme) => update({ theme })
1071
+ }
1072
+ ),
532
1073
  /* @__PURE__ */ jsxs("label", { children: [
533
- field("theme", "Theme (JSON)"),
1074
+ field("serverEndpoint", "Server endpoint"),
534
1075
  /* @__PURE__ */ jsx(
535
- "textarea",
1076
+ "input",
536
1077
  {
537
- "aria-label": field("theme", "Theme (JSON)"),
538
- value: JSON.stringify(config.theme ?? {}, null, 2),
539
- onChange: (event) => {
540
- try {
541
- const parsed = JSON.parse(event.target.value);
542
- if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed))
543
- update({ theme: parsed });
544
- } catch {
545
- }
546
- }
1078
+ value: config.serverEndpoint ?? "",
1079
+ onChange: (event) => update({ serverEndpoint: event.target.value })
547
1080
  }
548
1081
  )
549
1082
  ] }),
550
- ["serverEndpoint", "publicMetadata", "serverMetadata"].map((key) => /* @__PURE__ */ jsxs("label", { children: [
551
- field(key, key),
552
- /* @__PURE__ */ jsx(
553
- "textarea",
554
- {
555
- value: key === "serverEndpoint" ? config.serverEndpoint ?? "" : JSON.stringify(config[key] ?? {}, null, 2),
556
- onChange: (event) => {
557
- if (key === "serverEndpoint") {
558
- update({ serverEndpoint: event.target.value });
559
- return;
560
- }
561
- try {
562
- const parsed = JSON.parse(event.target.value);
563
- if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed))
564
- update({ [key]: parsed });
565
- } catch {
566
- }
567
- }
568
- }
569
- )
570
- ] }, key)),
1083
+ /* @__PURE__ */ jsx(
1084
+ MetadataEditor,
1085
+ {
1086
+ publicMetadata: config.publicMetadata ?? config.metadata ?? {},
1087
+ serverMetadata: config.serverMetadata ?? {},
1088
+ locale: activeLocale,
1089
+ ...dictionary === void 0 ? {} : { dictionary },
1090
+ onPublicMetadataChange: (metadata) => update({ publicMetadata: metadata }),
1091
+ onServerMetadataChange: (metadata) => update({ serverMetadata: metadata })
1092
+ }
1093
+ ),
571
1094
  /* @__PURE__ */ jsx(
572
1095
  "button",
573
1096
  {
@@ -616,21 +1139,40 @@ function AdminRallyEditor({ config, onChange, locale, dictionary }) {
616
1139
  children: field("addReward", "Add reward")
617
1140
  }
618
1141
  ),
619
- /* @__PURE__ */ jsx("div", { children: config.rewards.map((reward) => /* @__PURE__ */ jsx(
620
- RewardItemForm,
621
- {
622
- reward,
623
- locale: activeLocale,
624
- ...dictionary === void 0 ? {} : { dictionary },
625
- onChange: (next) => update({
626
- rewards: config.rewards.map(
627
- (current) => current.id === reward.id ? next : current
628
- )
629
- }),
630
- onRemove: () => update({ rewards: config.rewards.filter((current) => current.id !== reward.id) })
631
- },
632
- reward.id
633
- )) })
1142
+ /* @__PURE__ */ jsx("div", { children: config.rewards.map((reward, index) => /* @__PURE__ */ jsxs("div", { children: [
1143
+ /* @__PURE__ */ jsx(
1144
+ RewardItemForm,
1145
+ {
1146
+ reward,
1147
+ locale: activeLocale,
1148
+ ...dictionary === void 0 ? {} : { dictionary },
1149
+ onChange: (next) => update({
1150
+ rewards: config.rewards.map(
1151
+ (current) => current.id === reward.id ? next : current
1152
+ )
1153
+ }),
1154
+ onRemove: () => update({ rewards: config.rewards.filter((current) => current.id !== reward.id) })
1155
+ }
1156
+ ),
1157
+ /* @__PURE__ */ jsx(
1158
+ "button",
1159
+ {
1160
+ type: "button",
1161
+ disabled: index === 0,
1162
+ onClick: () => update({ rewards: move(config.rewards, index, -1) }),
1163
+ children: field("moveUp", "Move up")
1164
+ }
1165
+ ),
1166
+ /* @__PURE__ */ jsx(
1167
+ "button",
1168
+ {
1169
+ type: "button",
1170
+ disabled: index === config.rewards.length - 1,
1171
+ onClick: () => update({ rewards: move(config.rewards, index, 1) }),
1172
+ children: field("moveDown", "Move down")
1173
+ }
1174
+ )
1175
+ ] }, reward.id)) })
634
1176
  ] });
635
1177
  }
636
1178
  var RallyEditor = AdminRallyEditor;
@@ -700,6 +1242,6 @@ function JsonConfigIO({ config, onImport, locale, dictionary }) {
700
1242
  ] });
701
1243
  }
702
1244
 
703
- export { AdminRallyEditor, ConditionEditor, GeneralSettingsForm, JsonConfigIO, RallyEditor, SpotItemForm, useAdminRallyEditor, useRewardEditor, useSpotEditor };
1245
+ export { AdminRallyEditor, ConditionEditor, ExternalReferencesEditor, GeneralSettingsForm, JsonConfigIO, MetadataEditor, RallyEditor, RewardItemForm, RewardUnlockConditionEditor, SpotItemForm, ThemeEditor, useAdminRallyEditor, useRewardEditor, useSpotEditor };
704
1246
  //# sourceMappingURL=index.js.map
705
1247
  //# sourceMappingURL=index.js.map