@stamprally/admin-ui 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
+ "use client";
1
2
  import { resolveLocalizedText, updateLocalizedField, DEFAULT_SHEET_THEME, safeParseAdminConfig } from '@stamprally/core';
2
- import { useRef, useState, useCallback } from 'react';
3
+ import { useRef, useState, useEffect, useCallback } from 'react';
3
4
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
4
5
 
5
- // src/index.tsx
6
6
  function nextEntityId(prefix, ids) {
7
7
  let index = ids.size + 1;
8
8
  let candidate = `${prefix}-${index}`;
@@ -24,12 +24,22 @@ function localizedValue(value, locale, nextValue) {
24
24
  }
25
25
  function useAdminRallyEditor(initialConfig, options = {}) {
26
26
  const initialRef = useRef(initialConfig);
27
+ const previousInitialRef = useRef(initialConfig);
27
28
  const [history, setHistory] = useState({
28
29
  past: [],
29
30
  present: initialConfig,
30
31
  future: []
31
32
  });
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]);
33
43
  const commit = useCallback(
34
44
  (value) => {
35
45
  setHistory((current) => {
@@ -66,6 +76,7 @@ function useAdminRallyEditor(initialConfig, options = {}) {
66
76
  spots: current.spots.filter((spot) => spot.id !== spotId).map((spot, index) => ({ ...spot, orderIndex: index }))
67
77
  }));
68
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) }));
69
80
  const duplicateSpot = (spotId) => commit((current) => {
70
81
  const source = current.spots.find((spot) => spot.id === spotId);
71
82
  if (source === void 0) return current;
@@ -164,6 +175,7 @@ function useAdminRallyEditor(initialConfig, options = {}) {
164
175
  });
165
176
  const resetConfig = (newConfig) => {
166
177
  initialRef.current = newConfig;
178
+ previousInitialRef.current = newConfig;
167
179
  setHistory({ past: [], present: newConfig, future: [] });
168
180
  options.onChange?.(newConfig);
169
181
  };
@@ -176,6 +188,7 @@ function useAdminRallyEditor(initialConfig, options = {}) {
176
188
  addSpot,
177
189
  removeSpot,
178
190
  reorderSpots: (fromIndex, toIndex) => reorderSpots(fromIndex, toIndex),
191
+ reorderRewards,
179
192
  duplicateSpot,
180
193
  addReward,
181
194
  removeReward,
@@ -196,9 +209,13 @@ function useSpotEditor(spotId, options = {}) {
196
209
  const [config, setConfig] = useState(
197
210
  options.config ?? options.initialConfig
198
211
  );
199
- const commit = (next) => {
200
- setConfig(next);
201
- 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
+ });
202
219
  };
203
220
  const spot = config?.spots.find((item) => item.id === spotId);
204
221
  return {
@@ -207,14 +224,17 @@ function useSpotEditor(spotId, options = {}) {
207
224
  setConfig,
208
225
  update: (patch) => {
209
226
  if (config === void 0 || spot === void 0) return;
210
- commit({
211
- ...config,
212
- spots: config.spots.map((item) => item.id === spotId ? { ...item, ...patch } : item)
213
- });
227
+ commit((current) => ({
228
+ ...current,
229
+ spots: current.spots.map((item) => item.id === spotId ? { ...item, ...patch } : item)
230
+ }));
214
231
  },
215
232
  remove: () => {
216
233
  if (config === void 0 || spot === void 0) return;
217
- 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
+ }));
218
238
  }
219
239
  };
220
240
  }
@@ -223,9 +243,13 @@ function useRewardEditor(rewardId, options = {}) {
223
243
  options.config ?? options.initialConfig
224
244
  );
225
245
  const reward = config?.rewards.find((item) => item.id === rewardId);
226
- const commit = (next) => {
227
- setConfig(next);
228
- 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
+ });
229
253
  };
230
254
  return {
231
255
  config,
@@ -233,16 +257,19 @@ function useRewardEditor(rewardId, options = {}) {
233
257
  setConfig,
234
258
  update: (patch) => {
235
259
  if (config === void 0 || reward === void 0) return;
236
- commit({
237
- ...config,
238
- rewards: config.rewards.map(
260
+ commit((current) => ({
261
+ ...current,
262
+ rewards: current.rewards.map(
239
263
  (item) => item.id === rewardId ? { ...item, ...patch } : item
240
264
  )
241
- });
265
+ }));
242
266
  },
243
267
  remove: () => {
244
268
  if (config === void 0 || reward === void 0) return;
245
- 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
+ }));
246
273
  }
247
274
  };
248
275
  }
@@ -1112,21 +1139,40 @@ function AdminRallyEditor({ config, onChange, locale, dictionary }) {
1112
1139
  children: field("addReward", "Add reward")
1113
1140
  }
1114
1141
  ),
1115
- /* @__PURE__ */ jsx("div", { children: config.rewards.map((reward) => /* @__PURE__ */ jsx(
1116
- RewardItemForm,
1117
- {
1118
- reward,
1119
- locale: activeLocale,
1120
- ...dictionary === void 0 ? {} : { dictionary },
1121
- onChange: (next) => update({
1122
- rewards: config.rewards.map(
1123
- (current) => current.id === reward.id ? next : current
1124
- )
1125
- }),
1126
- onRemove: () => update({ rewards: config.rewards.filter((current) => current.id !== reward.id) })
1127
- },
1128
- reward.id
1129
- )) })
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)) })
1130
1176
  ] });
1131
1177
  }
1132
1178
  var RallyEditor = AdminRallyEditor;
@@ -1137,18 +1183,97 @@ function GeneralSettingsForm({
1137
1183
  dictionary
1138
1184
  }) {
1139
1185
  const activeLocale = locale ?? "en";
1140
- return /* @__PURE__ */ jsxs("label", { children: [
1141
- text(dictionary, activeLocale, "title", "Rally title"),
1142
- /* @__PURE__ */ jsx(
1143
- "input",
1144
- {
1145
- value: resolveLocalizedText(config.title, activeLocale),
1146
- onChange: (event) => onChange({
1147
- ...config,
1148
- title: updateLocalizedField(config.title, activeLocale, event.target.value)
1149
- })
1150
- }
1151
- )
1186
+ return /* @__PURE__ */ jsxs("section", { "aria-label": text(dictionary, activeLocale, "generalSettings", "General settings"), children: [
1187
+ /* @__PURE__ */ jsxs("label", { children: [
1188
+ text(dictionary, activeLocale, "title", "Rally title"),
1189
+ /* @__PURE__ */ jsx(
1190
+ "input",
1191
+ {
1192
+ value: resolveLocalizedText(config.title, activeLocale),
1193
+ onChange: (event) => onChange({
1194
+ ...config,
1195
+ title: updateLocalizedField(config.title, activeLocale, event.target.value)
1196
+ })
1197
+ }
1198
+ )
1199
+ ] }),
1200
+ /* @__PURE__ */ jsxs("label", { children: [
1201
+ text(dictionary, activeLocale, "inventoryLimit", "Global inventory limit"),
1202
+ /* @__PURE__ */ jsx(
1203
+ "input",
1204
+ {
1205
+ type: "number",
1206
+ min: 0,
1207
+ placeholder: text(dictionary, activeLocale, "inventoryLimitPlaceholder", "Unlimited"),
1208
+ title: text(
1209
+ dictionary,
1210
+ activeLocale,
1211
+ "inventoryOverrideHelp",
1212
+ "Individual reward limits override this global limit."
1213
+ ),
1214
+ value: config.inventory?.global ?? "",
1215
+ onChange: (event) => {
1216
+ const value = event.target.value;
1217
+ const inventory = { ...config.inventory ?? {} };
1218
+ if (value === "") delete inventory.global;
1219
+ else inventory.global = Number(value);
1220
+ onChange({ ...config, inventory });
1221
+ }
1222
+ }
1223
+ )
1224
+ ] }),
1225
+ /* @__PURE__ */ jsxs("label", { children: [
1226
+ text(dictionary, activeLocale, "inventoryMode", "Inventory aggregation"),
1227
+ /* @__PURE__ */ jsxs(
1228
+ "select",
1229
+ {
1230
+ value: config.inventoryMode ?? "shared",
1231
+ title: text(
1232
+ dictionary,
1233
+ activeLocale,
1234
+ "inventoryModeHelp",
1235
+ "Individual reward limits override this setting."
1236
+ ),
1237
+ onChange: (event) => onChange({
1238
+ ...config,
1239
+ inventoryMode: event.target.value
1240
+ }),
1241
+ children: [
1242
+ /* @__PURE__ */ jsx("option", { value: "shared", children: text(dictionary, activeLocale, "inventoryShared", "Shared") }),
1243
+ /* @__PURE__ */ jsx("option", { value: "per_reward", children: text(dictionary, activeLocale, "inventoryPerReward", "Per reward") })
1244
+ ]
1245
+ }
1246
+ )
1247
+ ] }),
1248
+ /* @__PURE__ */ jsxs("label", { children: [
1249
+ text(dictionary, activeLocale, "staffPasscode", "Global staff passcode"),
1250
+ /* @__PURE__ */ jsx(
1251
+ "input",
1252
+ {
1253
+ type: "password",
1254
+ placeholder: text(
1255
+ dictionary,
1256
+ activeLocale,
1257
+ "staffPasscodePlaceholder",
1258
+ "Used unless a reward has its own passcode"
1259
+ ),
1260
+ title: text(
1261
+ dictionary,
1262
+ activeLocale,
1263
+ "staffPasscodeHelp",
1264
+ "Individual spot or reward settings override this global passcode."
1265
+ ),
1266
+ value: config.staffPasscode ?? "",
1267
+ onChange: (event) => {
1268
+ const value = event.target.value;
1269
+ if (value === "") {
1270
+ const { staffPasscode: _staffPasscode, ...withoutPasscode } = config;
1271
+ onChange(withoutPasscode);
1272
+ } else onChange({ ...config, staffPasscode: value });
1273
+ }
1274
+ }
1275
+ )
1276
+ ] })
1152
1277
  ] });
1153
1278
  }
1154
1279
  function JsonConfigIO({ config, onImport, locale, dictionary }) {