@unicitylabs/sphere-ui 0.1.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.
@@ -0,0 +1,813 @@
1
+ // src/hooks/useCanvasState.ts
2
+ import { useState, useCallback, useMemo, useEffect, useRef } from "react";
3
+ import { arrayMove } from "@dnd-kit/sortable";
4
+ function chainColor(group) {
5
+ let hash = 0;
6
+ for (const c of group) hash = hash * 31 + c.charCodeAt(0) & 4294967295;
7
+ const hue = Math.abs(hash) % 360;
8
+ return `hsl(${hue}, 70%, 55%)`;
9
+ }
10
+ function parseDndId(id) {
11
+ if (id === "unassigned") return { type: "unassigned", rawId: "unassigned" };
12
+ const [type, rawId] = id.split(":");
13
+ return { type, rawId };
14
+ }
15
+ function normalizeLaneId(id) {
16
+ if (id.startsWith("lane:")) return id.replace("lane:", "track:");
17
+ return id;
18
+ }
19
+ function getLaneId(quest) {
20
+ if (quest.trackId) return `track:${quest.trackId}`;
21
+ return "unassigned";
22
+ }
23
+ function useCanvasState(tracks, quests) {
24
+ const [trackOrder, setTrackOrder] = useState([]);
25
+ const [questOrderByLane, setQuestOrderByLane] = useState(/* @__PURE__ */ new Map());
26
+ const [selectedItem, setSelectedItem] = useState(null);
27
+ const [activePanelTab, setActivePanelTab] = useState("quests");
28
+ const [pendingChanges, setPendingChanges] = useState([]);
29
+ const [localQuestFields, setLocalQuestFields] = useState(/* @__PURE__ */ new Map());
30
+ const [localTrackFields, setLocalTrackFields] = useState(/* @__PURE__ */ new Map());
31
+ const undoStack = useRef([]);
32
+ const [activeDragId, setActiveDragId] = useState(null);
33
+ const dragSourceLane = useRef(null);
34
+ useEffect(() => {
35
+ const sortedTracks = [...tracks].sort((a, b) => a.sortOrder - b.sortOrder);
36
+ setTrackOrder(sortedTracks.map((t) => `track:${t._id}`));
37
+ const byLane = /* @__PURE__ */ new Map();
38
+ for (const t of sortedTracks) {
39
+ const laneId = `track:${t._id}`;
40
+ const laneQuests = quests.filter((q) => getLaneId(q) === laneId).sort((a, b) => a.sortOrder - b.sortOrder);
41
+ byLane.set(laneId, laneQuests.map((q) => `quest:${q._id}`));
42
+ }
43
+ const unassigned = quests.filter((q) => getLaneId(q) === "unassigned").sort((a, b) => a.sortOrder - b.sortOrder);
44
+ byLane.set("unassigned", unassigned.map((q) => `quest:${q._id}`));
45
+ setQuestOrderByLane(byLane);
46
+ setPendingChanges([]);
47
+ setLocalQuestFields(/* @__PURE__ */ new Map());
48
+ setLocalTrackFields(/* @__PURE__ */ new Map());
49
+ }, [tracks, quests]);
50
+ const pushUndo = useCallback(() => {
51
+ undoStack.current = [
52
+ ...undoStack.current.slice(-19),
53
+ {
54
+ trackOrder,
55
+ questOrderByLane: new Map(questOrderByLane),
56
+ localQuestFields: new Map(localQuestFields),
57
+ localTrackFields: new Map(localTrackFields),
58
+ pendingChanges: [...pendingChanges]
59
+ }
60
+ ];
61
+ }, [trackOrder, questOrderByLane, localQuestFields, localTrackFields, pendingChanges]);
62
+ const undo = useCallback(() => {
63
+ const entry = undoStack.current.pop();
64
+ if (!entry) return;
65
+ setTrackOrder(entry.trackOrder);
66
+ setQuestOrderByLane(entry.questOrderByLane);
67
+ setLocalQuestFields(entry.localQuestFields);
68
+ setLocalTrackFields(entry.localTrackFields);
69
+ setPendingChanges(entry.pendingChanges);
70
+ }, []);
71
+ const getQuest = useCallback((id) => {
72
+ const base = quests.find((q) => q._id === id);
73
+ if (!base) return void 0;
74
+ const overrides = localQuestFields.get(id);
75
+ return overrides ? { ...base, ...overrides } : base;
76
+ }, [quests, localQuestFields]);
77
+ const getTrack = useCallback((id) => {
78
+ const base = tracks.find((t) => t._id === id);
79
+ if (!base) return void 0;
80
+ const overrides = localTrackFields.get(id);
81
+ return overrides ? { ...base, ...overrides } : base;
82
+ }, [tracks, localTrackFields]);
83
+ const getQuestsForLane = useCallback((laneId) => {
84
+ const ids = questOrderByLane.get(laneId) ?? [];
85
+ return ids.map((dndId) => {
86
+ const { rawId } = parseDndId(dndId);
87
+ return getQuest(rawId);
88
+ }).filter((q) => Boolean(q));
89
+ }, [questOrderByLane, getQuest]);
90
+ const findLaneForQuest = useCallback((questDndId) => {
91
+ for (const [laneId, ids] of questOrderByLane) {
92
+ if (ids.includes(questDndId)) return laneId;
93
+ }
94
+ return void 0;
95
+ }, [questOrderByLane]);
96
+ const updateQuestField = useCallback((id, fields) => {
97
+ pushUndo();
98
+ setLocalQuestFields((m) => {
99
+ const next = new Map(m);
100
+ next.set(id, { ...next.get(id) ?? {}, ...fields });
101
+ return next;
102
+ });
103
+ setPendingChanges((p) => [...p, { kind: "quest-field", id, fields }]);
104
+ const laneChanged = "trackId" in fields;
105
+ if (laneChanged) {
106
+ const questDndId = `quest:${id}`;
107
+ const newLane = fields.trackId ? `track:${fields.trackId}` : "unassigned";
108
+ setQuestOrderByLane((prev) => {
109
+ const next = new Map(prev);
110
+ for (const [laneId, ids] of next) {
111
+ if (ids.includes(questDndId)) {
112
+ next.set(laneId, ids.filter((qid) => qid !== questDndId));
113
+ break;
114
+ }
115
+ }
116
+ const target = next.get(newLane) ?? [];
117
+ if (!target.includes(questDndId)) {
118
+ next.set(newLane, [...target, questDndId]);
119
+ }
120
+ return next;
121
+ });
122
+ }
123
+ }, [pushUndo]);
124
+ const updateTrackField = useCallback((id, fields) => {
125
+ pushUndo();
126
+ setLocalTrackFields((m) => {
127
+ const next = new Map(m);
128
+ next.set(id, { ...next.get(id) ?? {}, ...fields });
129
+ return next;
130
+ });
131
+ setPendingChanges((p) => [...p, { kind: "track-field", id, fields }]);
132
+ }, [pushUndo]);
133
+ const handleDragStart = useCallback((id) => {
134
+ setActiveDragId(id);
135
+ dragSourceLane.current = findLaneForQuest(id) ?? null;
136
+ }, [findLaneForQuest]);
137
+ const handleDragOver = useCallback((event) => {
138
+ const { active, over } = event;
139
+ if (!over) return;
140
+ const activeId = String(active.id);
141
+ const overId = String(over.id);
142
+ const { type: activeType } = parseDndId(activeId);
143
+ if (activeType !== "quest") return;
144
+ const sourceLane = findLaneForQuest(activeId);
145
+ if (!sourceLane) return;
146
+ let targetLane;
147
+ const normalizedOverId = normalizeLaneId(overId);
148
+ const { type: overType } = parseDndId(normalizedOverId);
149
+ if (overType === "track" || normalizedOverId === "unassigned") {
150
+ targetLane = normalizedOverId;
151
+ } else if (overType === "quest") {
152
+ targetLane = findLaneForQuest(normalizedOverId) ?? sourceLane;
153
+ } else {
154
+ return;
155
+ }
156
+ if (sourceLane === targetLane) return;
157
+ setQuestOrderByLane((prev) => {
158
+ const next = new Map(prev);
159
+ const srcList = (next.get(sourceLane) ?? []).filter((id) => id !== activeId);
160
+ const tgtList = (next.get(targetLane) ?? []).filter((id) => id !== activeId);
161
+ const overIdx = tgtList.indexOf(overId);
162
+ if (overIdx !== -1) {
163
+ tgtList.splice(overIdx, 0, activeId);
164
+ } else {
165
+ tgtList.push(activeId);
166
+ }
167
+ next.set(sourceLane, srcList);
168
+ next.set(targetLane, tgtList);
169
+ return next;
170
+ });
171
+ }, [findLaneForQuest]);
172
+ const handleDragEnd = useCallback((event) => {
173
+ const { active, over } = event;
174
+ setActiveDragId(null);
175
+ if (!over) return;
176
+ const activeId = String(active.id);
177
+ const overId = String(over.id);
178
+ const { type: activeType, rawId: activeRawId } = parseDndId(activeId);
179
+ if (activeType === "track") {
180
+ if (activeId === overId) return;
181
+ pushUndo();
182
+ const newOrder = arrayMove(
183
+ trackOrder,
184
+ trackOrder.indexOf(activeId),
185
+ trackOrder.indexOf(overId)
186
+ );
187
+ setTrackOrder(newOrder);
188
+ setPendingChanges((p) => [...p, { kind: "track-reorder" }]);
189
+ return;
190
+ }
191
+ if (activeType === "quest") {
192
+ const currentLane = findLaneForQuest(activeId);
193
+ if (!currentLane) return;
194
+ const sourceLane = dragSourceLane.current;
195
+ dragSourceLane.current = null;
196
+ const isCrossLane = sourceLane !== null && sourceLane !== currentLane;
197
+ if (isCrossLane) {
198
+ pushUndo();
199
+ setPendingChanges((p) => [
200
+ ...p,
201
+ { kind: "quest-reorder", laneId: currentLane },
202
+ { kind: "quest-reorder", laneId: sourceLane }
203
+ ]);
204
+ const track = currentLane === "unassigned" ? null : tracks.find((t) => `track:${t._id}` === currentLane);
205
+ const newTrackId = track?._id ?? null;
206
+ setPendingChanges((p) => [...p, { kind: "quest-field", id: activeRawId, fields: { trackId: newTrackId } }]);
207
+ setLocalQuestFields((m) => {
208
+ const next = new Map(m);
209
+ next.set(activeRawId, { ...next.get(activeRawId) ?? {}, trackId: newTrackId });
210
+ return next;
211
+ });
212
+ return;
213
+ }
214
+ const laneIds = questOrderByLane.get(currentLane) ?? [];
215
+ if (activeId === overId) return;
216
+ pushUndo();
217
+ const newLaneIds = arrayMove(laneIds, laneIds.indexOf(activeId), laneIds.indexOf(overId));
218
+ setQuestOrderByLane((prev) => {
219
+ const next = new Map(prev);
220
+ next.set(currentLane, newLaneIds);
221
+ return next;
222
+ });
223
+ setPendingChanges((p) => [...p, { kind: "quest-reorder", laneId: currentLane }]);
224
+ }
225
+ }, [trackOrder, questOrderByLane, tracks, findLaneForQuest, pushUndo]);
226
+ const reset = useCallback(() => {
227
+ const sortedTracks = [...tracks].sort((a, b) => a.sortOrder - b.sortOrder);
228
+ setTrackOrder(sortedTracks.map((t) => `track:${t._id}`));
229
+ const byLane = /* @__PURE__ */ new Map();
230
+ for (const t of sortedTracks) {
231
+ const laneId = `track:${t._id}`;
232
+ const laneQuests = quests.filter((q) => getLaneId(q) === laneId).sort((a, b) => a.sortOrder - b.sortOrder);
233
+ byLane.set(laneId, laneQuests.map((q) => `quest:${q._id}`));
234
+ }
235
+ const unassigned = quests.filter((q) => getLaneId(q) === "unassigned").sort((a, b) => a.sortOrder - b.sortOrder);
236
+ byLane.set("unassigned", unassigned.map((q) => `quest:${q._id}`));
237
+ setQuestOrderByLane(byLane);
238
+ setPendingChanges([]);
239
+ setLocalQuestFields(/* @__PURE__ */ new Map());
240
+ setLocalTrackFields(/* @__PURE__ */ new Map());
241
+ undoStack.current = [];
242
+ }, [tracks, quests]);
243
+ const selectItem = useCallback((item) => {
244
+ setSelectedItem(item);
245
+ if (item) setActivePanelTab("settings");
246
+ }, []);
247
+ const orderedTracks = useMemo(() => {
248
+ return trackOrder.map((dndId) => {
249
+ const { rawId } = parseDndId(dndId);
250
+ return getTrack(rawId);
251
+ }).filter((t) => Boolean(t));
252
+ }, [trackOrder, getTrack]);
253
+ return {
254
+ // Data
255
+ orderedTracks,
256
+ getQuestsForLane,
257
+ getQuest,
258
+ getTrack,
259
+ trackOrder,
260
+ questOrderByLane,
261
+ // DnD
262
+ activeDragId,
263
+ handleDragStart,
264
+ handleDragOver,
265
+ handleDragEnd,
266
+ // Selection
267
+ selectedItem,
268
+ selectItem,
269
+ // Panel
270
+ activePanelTab,
271
+ setActivePanelTab,
272
+ // Edits
273
+ updateQuestField,
274
+ updateTrackField,
275
+ // Undo
276
+ undo,
277
+ // Pending changes
278
+ pendingChanges,
279
+ hasPendingChanges: pendingChanges.length > 0,
280
+ clearPendingChanges: () => setPendingChanges([]),
281
+ reset
282
+ };
283
+ }
284
+
285
+ // src/hooks/useAchievementCanvasState.ts
286
+ import { useState as useState2, useCallback as useCallback2, useMemo as useMemo2, useEffect as useEffect2, useRef as useRef2 } from "react";
287
+ import { arrayMove as arrayMove2 } from "@dnd-kit/sortable";
288
+ function parseDndId2(id) {
289
+ if (id === "unassigned") return { type: "unassigned", rawId: "unassigned" };
290
+ const [type, rawId] = id.split(":");
291
+ return { type, rawId };
292
+ }
293
+ function normalizeLaneId2(id) {
294
+ if (id.startsWith("lane:")) return id.replace("lane:", "track:");
295
+ return id;
296
+ }
297
+ function getAchLaneId(ach) {
298
+ if (ach.trackId) return `track:${ach.trackId}`;
299
+ return "unassigned";
300
+ }
301
+ function useAchievementCanvasState(tracks, achievements) {
302
+ const [trackOrder, setTrackOrder] = useState2([]);
303
+ const [achOrderByLane, setAchOrderByLane] = useState2(/* @__PURE__ */ new Map());
304
+ const [selectedItem, setSelectedItem] = useState2(null);
305
+ const [activePanelTab, setActivePanelTab] = useState2("achievements");
306
+ const [groupMode, setGroupMode] = useState2("track");
307
+ const [pendingChanges, setPendingChanges] = useState2([]);
308
+ const [localAchFields, setLocalAchFields] = useState2(/* @__PURE__ */ new Map());
309
+ const [localTrackFields, setLocalTrackFields] = useState2(/* @__PURE__ */ new Map());
310
+ const undoStack = useRef2([]);
311
+ const [activeDragId, setActiveDragId] = useState2(null);
312
+ useEffect2(() => {
313
+ const sortedTracks = [...tracks].sort((a, b) => a.sortOrder - b.sortOrder);
314
+ setTrackOrder(sortedTracks.map((t) => `track:${t._id}`));
315
+ const byLane = /* @__PURE__ */ new Map();
316
+ for (const t of sortedTracks) {
317
+ const laneId = `track:${t._id}`;
318
+ const laneAchs = achievements.filter((a) => getAchLaneId(a) === laneId).sort((a, b) => a.sortOrder - b.sortOrder);
319
+ byLane.set(laneId, laneAchs.map((a) => `ach:${a._id}`));
320
+ }
321
+ const unassigned = achievements.filter((a) => getAchLaneId(a) === "unassigned").sort((a, b) => a.sortOrder - b.sortOrder);
322
+ byLane.set("unassigned", unassigned.map((a) => `ach:${a._id}`));
323
+ setAchOrderByLane(byLane);
324
+ setPendingChanges([]);
325
+ setLocalAchFields(/* @__PURE__ */ new Map());
326
+ setLocalTrackFields(/* @__PURE__ */ new Map());
327
+ }, [tracks, achievements]);
328
+ const pushUndo = useCallback2(() => {
329
+ undoStack.current = [...undoStack.current.slice(-19), {
330
+ trackOrder,
331
+ achOrderByLane: new Map(achOrderByLane),
332
+ localAchFields: new Map(localAchFields),
333
+ localTrackFields: new Map(localTrackFields),
334
+ pendingChanges: [...pendingChanges]
335
+ }];
336
+ }, [trackOrder, achOrderByLane, localAchFields, localTrackFields, pendingChanges]);
337
+ const undo = useCallback2(() => {
338
+ const entry = undoStack.current.pop();
339
+ if (!entry) return;
340
+ setTrackOrder(entry.trackOrder);
341
+ setAchOrderByLane(entry.achOrderByLane);
342
+ setLocalAchFields(entry.localAchFields);
343
+ setLocalTrackFields(entry.localTrackFields);
344
+ setPendingChanges(entry.pendingChanges);
345
+ }, []);
346
+ const getAchievement = useCallback2((id) => {
347
+ const base = achievements.find((a) => a._id === id);
348
+ if (!base) return void 0;
349
+ const overrides = localAchFields.get(id);
350
+ return overrides ? { ...base, ...overrides } : base;
351
+ }, [achievements, localAchFields]);
352
+ const getTrack = useCallback2((id) => {
353
+ const base = tracks.find((t) => t._id === id);
354
+ if (!base) return void 0;
355
+ const overrides = localTrackFields.get(id);
356
+ return overrides ? { ...base, ...overrides } : base;
357
+ }, [tracks, localTrackFields]);
358
+ const getAchievementsForLane = useCallback2((laneId) => {
359
+ const ids = achOrderByLane.get(laneId) ?? [];
360
+ return ids.map((dndId) => {
361
+ const { rawId } = parseDndId2(dndId);
362
+ return getAchievement(rawId);
363
+ }).filter((a) => Boolean(a));
364
+ }, [achOrderByLane, getAchievement]);
365
+ const findLaneForAch = useCallback2((achDndId) => {
366
+ for (const [laneId, ids] of achOrderByLane) {
367
+ if (ids.includes(achDndId)) return laneId;
368
+ }
369
+ return void 0;
370
+ }, [achOrderByLane]);
371
+ const updateAchField = useCallback2((id, fields) => {
372
+ pushUndo();
373
+ setLocalAchFields((m) => {
374
+ const next = new Map(m);
375
+ next.set(id, { ...next.get(id) ?? {}, ...fields });
376
+ return next;
377
+ });
378
+ setPendingChanges((p) => [...p, { kind: "ach-field", id, fields }]);
379
+ if ("trackId" in fields) {
380
+ const achDndId = `ach:${id}`;
381
+ const newLane = fields.trackId ? `track:${fields.trackId}` : "unassigned";
382
+ setAchOrderByLane((prev) => {
383
+ const next = new Map(prev);
384
+ for (const [laneId, ids] of next) {
385
+ if (ids.includes(achDndId)) {
386
+ next.set(laneId, ids.filter((aid) => aid !== achDndId));
387
+ break;
388
+ }
389
+ }
390
+ const target = next.get(newLane) ?? [];
391
+ if (!target.includes(achDndId)) next.set(newLane, [...target, achDndId]);
392
+ return next;
393
+ });
394
+ }
395
+ }, [pushUndo]);
396
+ const updateTrackField = useCallback2((id, fields) => {
397
+ pushUndo();
398
+ setLocalTrackFields((m) => {
399
+ const next = new Map(m);
400
+ next.set(id, { ...next.get(id) ?? {}, ...fields });
401
+ return next;
402
+ });
403
+ setPendingChanges((p) => [...p, { kind: "track-field", id, fields }]);
404
+ }, [pushUndo]);
405
+ const handleDragStart = useCallback2((id) => {
406
+ setActiveDragId(id);
407
+ }, []);
408
+ const handleDragOver = useCallback2((event) => {
409
+ const { active, over } = event;
410
+ if (!over) return;
411
+ const activeId = String(active.id);
412
+ const overId = String(over.id);
413
+ const { type: activeType } = parseDndId2(activeId);
414
+ if (activeType !== "ach") return;
415
+ const sourceLane = findLaneForAch(activeId);
416
+ if (!sourceLane) return;
417
+ let targetLane;
418
+ const normalizedOverId = normalizeLaneId2(overId);
419
+ const { type: overType } = parseDndId2(normalizedOverId);
420
+ if (overType === "track" || normalizedOverId === "unassigned") targetLane = normalizedOverId;
421
+ else if (overType === "ach") targetLane = findLaneForAch(normalizedOverId) ?? sourceLane;
422
+ else return;
423
+ if (sourceLane === targetLane) return;
424
+ if (groupMode === "source") return;
425
+ setAchOrderByLane((prev) => {
426
+ const next = new Map(prev);
427
+ const srcList = (next.get(sourceLane) ?? []).filter((id) => id !== activeId);
428
+ const tgtList = [...next.get(targetLane) ?? [], activeId];
429
+ next.set(sourceLane, srcList);
430
+ next.set(targetLane, tgtList);
431
+ return next;
432
+ });
433
+ }, [findLaneForAch, groupMode]);
434
+ const handleDragEnd = useCallback2((event) => {
435
+ const { active, over } = event;
436
+ setActiveDragId(null);
437
+ if (!over) return;
438
+ const activeId = String(active.id);
439
+ const overId = String(over.id);
440
+ const { type: activeType, rawId: activeRawId } = parseDndId2(activeId);
441
+ if (activeType === "track") {
442
+ if (activeId === overId) return;
443
+ pushUndo();
444
+ setTrackOrder(arrayMove2(trackOrder, trackOrder.indexOf(activeId), trackOrder.indexOf(overId)));
445
+ setPendingChanges((p) => [...p, { kind: "track-reorder" }]);
446
+ return;
447
+ }
448
+ if (activeType === "ach") {
449
+ const lane = findLaneForAch(activeId);
450
+ if (!lane) return;
451
+ const laneIds = achOrderByLane.get(lane) ?? [];
452
+ const overIsInSameLane = laneIds.includes(overId);
453
+ if (!overIsInSameLane) {
454
+ const finalLane = findLaneForAch(activeId);
455
+ if (finalLane) {
456
+ pushUndo();
457
+ setPendingChanges((p) => [...p, { kind: "ach-reorder", laneId: finalLane }]);
458
+ if (finalLane !== "unassigned") {
459
+ const track = tracks.find((t) => `track:${t._id}` === finalLane);
460
+ if (track) updateAchField(activeRawId, { trackId: track._id });
461
+ } else {
462
+ updateAchField(activeRawId, { trackId: null });
463
+ }
464
+ }
465
+ return;
466
+ }
467
+ if (activeId === overId) return;
468
+ pushUndo();
469
+ const newLaneIds = arrayMove2(laneIds, laneIds.indexOf(activeId), laneIds.indexOf(overId));
470
+ setAchOrderByLane((prev) => {
471
+ const next = new Map(prev);
472
+ next.set(lane, newLaneIds);
473
+ return next;
474
+ });
475
+ setPendingChanges((p) => [...p, { kind: "ach-reorder", laneId: lane }]);
476
+ }
477
+ }, [trackOrder, achOrderByLane, tracks, findLaneForAch, pushUndo, updateAchField]);
478
+ const reset = useCallback2(() => {
479
+ const sortedTracks = [...tracks].sort((a, b) => a.sortOrder - b.sortOrder);
480
+ setTrackOrder(sortedTracks.map((t) => `track:${t._id}`));
481
+ const byLane = /* @__PURE__ */ new Map();
482
+ for (const t of sortedTracks) {
483
+ const laneId = `track:${t._id}`;
484
+ byLane.set(laneId, achievements.filter((a) => getAchLaneId(a) === laneId).sort((a, b) => a.sortOrder - b.sortOrder).map((a) => `ach:${a._id}`));
485
+ }
486
+ byLane.set("unassigned", achievements.filter((a) => getAchLaneId(a) === "unassigned").sort((a, b) => a.sortOrder - b.sortOrder).map((a) => `ach:${a._id}`));
487
+ setAchOrderByLane(byLane);
488
+ setPendingChanges([]);
489
+ setLocalAchFields(/* @__PURE__ */ new Map());
490
+ setLocalTrackFields(/* @__PURE__ */ new Map());
491
+ undoStack.current = [];
492
+ }, [tracks, achievements]);
493
+ const selectItem = useCallback2((item) => {
494
+ setSelectedItem(item);
495
+ if (item) setActivePanelTab("settings");
496
+ }, []);
497
+ const orderedTracks = useMemo2(() => {
498
+ return trackOrder.map((dndId) => {
499
+ const { rawId } = parseDndId2(dndId);
500
+ return getTrack(rawId);
501
+ }).filter((t) => Boolean(t));
502
+ }, [trackOrder, getTrack]);
503
+ return {
504
+ orderedTracks,
505
+ getAchievementsForLane,
506
+ getAchievement,
507
+ getTrack,
508
+ trackOrder,
509
+ achOrderByLane,
510
+ groupMode,
511
+ setGroupMode,
512
+ activeDragId,
513
+ handleDragStart,
514
+ handleDragOver,
515
+ handleDragEnd,
516
+ selectedItem,
517
+ selectItem,
518
+ activePanelTab,
519
+ setActivePanelTab,
520
+ updateAchField,
521
+ updateTrackField,
522
+ undo,
523
+ pendingChanges,
524
+ hasPendingChanges: pendingChanges.length > 0,
525
+ clearPendingChanges: () => setPendingChanges([]),
526
+ reset
527
+ };
528
+ }
529
+
530
+ // src/hooks/useTestMode.ts
531
+ import { useState as useState3, useCallback as useCallback3 } from "react";
532
+ function computeQuestTestState(quest, state, allQuests) {
533
+ if (state.completedIds.has(quest._id)) return "passed";
534
+ if (state.verifyingId === quest._id) return "verifying";
535
+ if (state.failedIds.has(quest._id)) return "failed";
536
+ if (quest.status !== "ACTIVE") return "locked";
537
+ if (state.ignoreChain) return "ready";
538
+ const prereqs = quest.prerequisites ?? [];
539
+ for (const prereqId of prereqs) {
540
+ if (!state.completedIds.has(prereqId)) return "locked";
541
+ }
542
+ const chains = quest.chains ?? {};
543
+ for (const [group, order] of Object.entries(chains)) {
544
+ if (order <= 0) continue;
545
+ const prevOrder = order - 1;
546
+ const prevQuests = allQuests.filter(
547
+ (q) => ((q.chains ?? {})[group] ?? -1) === prevOrder
548
+ );
549
+ for (const pq of prevQuests) {
550
+ if (!state.completedIds.has(pq._id)) return "locked";
551
+ }
552
+ }
553
+ return "ready";
554
+ }
555
+ function useTestMode(verifyFn) {
556
+ const [state, setState] = useState3({
557
+ active: false,
558
+ withVerification: false,
559
+ ignoreChain: false,
560
+ completedIds: /* @__PURE__ */ new Set(),
561
+ verifyingId: null,
562
+ failedIds: /* @__PURE__ */ new Map()
563
+ });
564
+ const enterTestMode = useCallback3(() => {
565
+ setState((s) => ({ ...s, active: true, completedIds: /* @__PURE__ */ new Set(), verifyingId: null, failedIds: /* @__PURE__ */ new Map() }));
566
+ }, []);
567
+ const exitTestMode = useCallback3(() => {
568
+ setState((s) => ({ ...s, active: false, completedIds: /* @__PURE__ */ new Set(), verifyingId: null, failedIds: /* @__PURE__ */ new Map() }));
569
+ }, []);
570
+ const toggleVerification = useCallback3(() => {
571
+ setState((s) => ({ ...s, withVerification: !s.withVerification }));
572
+ }, []);
573
+ const toggleIgnoreChain = useCallback3(() => {
574
+ setState((s) => ({ ...s, ignoreChain: !s.ignoreChain }));
575
+ }, []);
576
+ const claimQuest = useCallback3(async (questId) => {
577
+ if (!state.active) return;
578
+ if (!state.withVerification || !verifyFn) {
579
+ setState((s) => {
580
+ const next = new Set(s.completedIds);
581
+ next.add(questId);
582
+ return { ...s, completedIds: next };
583
+ });
584
+ return;
585
+ }
586
+ setState((s) => ({ ...s, verifyingId: questId }));
587
+ try {
588
+ const result = await verifyFn(questId);
589
+ setState((s) => {
590
+ if (result.passed) {
591
+ const next = new Set(s.completedIds);
592
+ next.add(questId);
593
+ return { ...s, verifyingId: null, completedIds: next };
594
+ } else {
595
+ const nextFailed = new Map(s.failedIds);
596
+ nextFailed.set(questId, result.reason ?? "Verification failed");
597
+ return { ...s, verifyingId: null, failedIds: nextFailed };
598
+ }
599
+ });
600
+ } catch {
601
+ setState((s) => {
602
+ const nextFailed = new Map(s.failedIds);
603
+ nextFailed.set(questId, "Network error");
604
+ return { ...s, verifyingId: null, failedIds: nextFailed };
605
+ });
606
+ }
607
+ }, [state.active, state.withVerification, verifyFn]);
608
+ const resetTest = useCallback3(() => {
609
+ setState((s) => ({ ...s, completedIds: /* @__PURE__ */ new Set(), verifyingId: null, failedIds: /* @__PURE__ */ new Map() }));
610
+ }, []);
611
+ return {
612
+ testMode: state,
613
+ enterTestMode,
614
+ exitTestMode,
615
+ toggleVerification,
616
+ toggleIgnoreChain,
617
+ claimQuest,
618
+ resetTest
619
+ };
620
+ }
621
+
622
+ // src/hooks/useChainMode.ts
623
+ import { useState as useState4, useMemo as useMemo3, useCallback as useCallback4 } from "react";
624
+ function useChainMode(items) {
625
+ const [active, setActive] = useState4(false);
626
+ const [selectedChain, setSelectedChain] = useState4(null);
627
+ const chainMap = useMemo3(() => {
628
+ const map = /* @__PURE__ */ new Map();
629
+ for (const q of items) {
630
+ const chains = q.chains ?? {};
631
+ for (const group of Object.keys(chains)) {
632
+ const list = map.get(group) ?? [];
633
+ list.push(q);
634
+ map.set(group, list);
635
+ }
636
+ }
637
+ for (const [group, list] of map) {
638
+ list.sort((a, b) => ((a.chains ?? {})[group] ?? 0) - ((b.chains ?? {})[group] ?? 0));
639
+ }
640
+ return map;
641
+ }, [items]);
642
+ const chainGroups = useMemo3(
643
+ () => [...chainMap.keys()].sort(),
644
+ [chainMap]
645
+ );
646
+ const toggle = useCallback4(() => {
647
+ setActive((prev) => {
648
+ if (prev) setSelectedChain(null);
649
+ return !prev;
650
+ });
651
+ }, []);
652
+ const selectChain = useCallback4((group) => {
653
+ setSelectedChain(group);
654
+ }, []);
655
+ const deselectChain = useCallback4(() => {
656
+ setSelectedChain(null);
657
+ }, []);
658
+ const exit = useCallback4(() => {
659
+ setActive(false);
660
+ setSelectedChain(null);
661
+ }, []);
662
+ return {
663
+ active,
664
+ selectedChain,
665
+ toggle,
666
+ selectChain,
667
+ deselectChain,
668
+ exit,
669
+ chainMap,
670
+ chainGroups
671
+ };
672
+ }
673
+
674
+ // src/hooks/useAchievementTestMode.ts
675
+ import { useState as useState5, useCallback as useCallback5 } from "react";
676
+ function computeAchTestState(ach, state, allAchievements) {
677
+ const progress = state.progressMap.get(ach._id);
678
+ if (progress?.claimed) return "claimed";
679
+ if (progress?.completed) return "completed";
680
+ if (ach.status !== "ACTIVE") return "locked";
681
+ if (state.ignoreChain) return "ready";
682
+ const chains = ach.chains ?? {};
683
+ for (const [group, order] of Object.entries(chains)) {
684
+ if (order <= 0) continue;
685
+ const prevOrder = order - 1;
686
+ const prevAchs = allAchievements.filter(
687
+ (a) => ((a.chains ?? {})[group] ?? -1) === prevOrder
688
+ );
689
+ for (const pa of prevAchs) {
690
+ const pp = state.progressMap.get(pa._id);
691
+ if (!pp?.completed) return "locked";
692
+ }
693
+ }
694
+ return "ready";
695
+ }
696
+ function useAchievementTestMode(fetchProgressFn) {
697
+ const [state, setState] = useState5({
698
+ active: false,
699
+ walletAddress: "",
700
+ ignoreChain: false,
701
+ progressMap: /* @__PURE__ */ new Map(),
702
+ loading: false,
703
+ error: null
704
+ });
705
+ const enterTestMode = useCallback5(() => {
706
+ setState((s) => ({ ...s, active: true, progressMap: /* @__PURE__ */ new Map(), error: null }));
707
+ }, []);
708
+ const exitTestMode = useCallback5(() => {
709
+ setState((s) => ({ ...s, active: false, walletAddress: "", progressMap: /* @__PURE__ */ new Map(), loading: false, error: null }));
710
+ }, []);
711
+ const toggleIgnoreChain = useCallback5(() => {
712
+ setState((s) => ({ ...s, ignoreChain: !s.ignoreChain }));
713
+ }, []);
714
+ const setWalletAddress = useCallback5((address) => {
715
+ setState((s) => ({ ...s, walletAddress: address }));
716
+ }, []);
717
+ const fetchProgress = useCallback5(async (address) => {
718
+ const wallet = address ?? state.walletAddress;
719
+ if (!wallet.trim() || !fetchProgressFn) return;
720
+ setState((s) => ({ ...s, loading: true, error: null, walletAddress: wallet }));
721
+ try {
722
+ const progress = await fetchProgressFn(wallet.trim());
723
+ const map = /* @__PURE__ */ new Map();
724
+ for (const p of progress) {
725
+ map.set(p.achievementId, p);
726
+ }
727
+ setState((s) => ({ ...s, progressMap: map, loading: false }));
728
+ } catch (err) {
729
+ setState((s) => ({
730
+ ...s,
731
+ loading: false,
732
+ error: err instanceof Error ? err.message : String(err)
733
+ }));
734
+ }
735
+ }, [state.walletAddress, fetchProgressFn]);
736
+ const clearProgress = useCallback5(() => {
737
+ setState((s) => ({ ...s, progressMap: /* @__PURE__ */ new Map(), walletAddress: "", error: null }));
738
+ }, []);
739
+ return {
740
+ testMode: state,
741
+ enterTestMode,
742
+ exitTestMode,
743
+ toggleIgnoreChain,
744
+ setWalletAddress,
745
+ fetchProgress,
746
+ clearProgress
747
+ };
748
+ }
749
+
750
+ // src/hooks/useAchievementChainMode.ts
751
+ import { useState as useState6, useMemo as useMemo4, useCallback as useCallback6 } from "react";
752
+ function useAchievementChainMode(achievements) {
753
+ const [active, setActive] = useState6(false);
754
+ const [selectedChain, setSelectedChain] = useState6(null);
755
+ const chainMap = useMemo4(() => {
756
+ const map = /* @__PURE__ */ new Map();
757
+ for (const a of achievements) {
758
+ const chains = a.chains ?? {};
759
+ for (const group of Object.keys(chains)) {
760
+ const list = map.get(group) ?? [];
761
+ list.push(a);
762
+ map.set(group, list);
763
+ }
764
+ }
765
+ for (const [group, list] of map) {
766
+ list.sort((a, b) => ((a.chains ?? {})[group] ?? 0) - ((b.chains ?? {})[group] ?? 0));
767
+ }
768
+ return map;
769
+ }, [achievements]);
770
+ const chainGroups = useMemo4(
771
+ () => [...chainMap.keys()].sort(),
772
+ [chainMap]
773
+ );
774
+ const toggle = useCallback6(() => {
775
+ setActive((prev) => {
776
+ if (prev) setSelectedChain(null);
777
+ return !prev;
778
+ });
779
+ }, []);
780
+ const selectChain = useCallback6((group) => {
781
+ setSelectedChain(group);
782
+ }, []);
783
+ const deselectChain = useCallback6(() => {
784
+ setSelectedChain(null);
785
+ }, []);
786
+ const exit = useCallback6(() => {
787
+ setActive(false);
788
+ setSelectedChain(null);
789
+ }, []);
790
+ return {
791
+ active,
792
+ selectedChain,
793
+ toggle,
794
+ selectChain,
795
+ deselectChain,
796
+ exit,
797
+ chainMap,
798
+ chainGroups
799
+ };
800
+ }
801
+ export {
802
+ chainColor,
803
+ computeAchTestState,
804
+ computeQuestTestState,
805
+ getAchLaneId,
806
+ getLaneId,
807
+ useAchievementCanvasState,
808
+ useAchievementChainMode,
809
+ useAchievementTestMode,
810
+ useCanvasState,
811
+ useChainMode,
812
+ useTestMode
813
+ };