react-achievements 3.9.1 → 4.0.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/README.md +148 -101
- package/dist/headless.cjs +317 -0
- package/dist/headless.cjs.map +1 -0
- package/dist/headless.d.ts +176 -0
- package/dist/headless.esm.js +222 -0
- package/dist/headless.esm.js.map +1 -0
- package/dist/index.cjs +839 -881
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +163 -153
- package/dist/index.esm.js +835 -883
- package/dist/index.esm.js.map +1 -1
- package/dist/web.cjs +1416 -0
- package/dist/web.cjs.map +1 -0
- package/dist/web.d.ts +534 -0
- package/dist/web.esm.js +1306 -0
- package/dist/web.esm.js.map +1 -0
- package/package.json +13 -28
- package/dist/types/__mocks__/confetti-wrapper.d.ts +0 -5
- package/dist/types/__mocks__/react-confetti.d.ts +0 -3
- package/dist/types/__mocks__/react-toastify.d.ts +0 -13
- package/dist/types/core/components/BadgesButton.d.ts +0 -25
- package/dist/types/core/components/BadgesButtonWithModal.d.ts +0 -53
- package/dist/types/core/components/BadgesModal.d.ts +0 -14
- package/dist/types/core/components/ConfettiWrapper.d.ts +0 -6
- package/dist/types/core/errors/AchievementErrors.d.ts +0 -55
- package/dist/types/core/hooks/useWindowSize.d.ts +0 -16
- package/dist/types/core/icons/defaultIcons.d.ts +0 -8
- package/dist/types/core/storage/AsyncStorageAdapter.d.ts +0 -48
- package/dist/types/core/storage/IndexedDBStorage.d.ts +0 -29
- package/dist/types/core/storage/LocalStorage.d.ts +0 -16
- package/dist/types/core/storage/MemoryStorage.d.ts +0 -11
- package/dist/types/core/storage/OfflineQueueStorage.d.ts +0 -42
- package/dist/types/core/storage/RestApiStorage.d.ts +0 -20
- package/dist/types/core/styles/defaultStyles.d.ts +0 -2
- package/dist/types/core/types.d.ts +0 -115
- package/dist/types/core/ui/BuiltInConfetti.d.ts +0 -7
- package/dist/types/core/ui/BuiltInModal.d.ts +0 -7
- package/dist/types/core/ui/BuiltInNotification.d.ts +0 -7
- package/dist/types/core/ui/LegacyWrappers.d.ts +0 -21
- package/dist/types/core/ui/interfaces.d.ts +0 -127
- package/dist/types/core/ui/legacyDetector.d.ts +0 -40
- package/dist/types/core/ui/themes.d.ts +0 -14
- package/dist/types/core/utils/configNormalizer.d.ts +0 -3
- package/dist/types/core/utils/dataExport.d.ts +0 -34
- package/dist/types/core/utils/dataImport.d.ts +0 -50
- package/dist/types/hooks/useAchievementEngine.d.ts +0 -36
- package/dist/types/hooks/useAchievements.d.ts +0 -1
- package/dist/types/hooks/useSimpleAchievements.d.ts +0 -63
- package/dist/types/index.d.ts +0 -36
- package/dist/types/providers/AchievementProvider.d.ts +0 -47
- package/dist/types/setupTests.d.ts +0 -1
- package/dist/types/utils/achievementHelpers.d.ts +0 -135
package/dist/web.cjs
ADDED
|
@@ -0,0 +1,1416 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var achievementsEngine = require('achievements-engine');
|
|
4
|
+
var React = require('react');
|
|
5
|
+
|
|
6
|
+
// Type guard to detect async storage
|
|
7
|
+
function isAsyncStorage(storage) {
|
|
8
|
+
// Check if methods return Promises
|
|
9
|
+
const testResult = storage.getMetrics();
|
|
10
|
+
return testResult && typeof testResult.then === 'function';
|
|
11
|
+
}
|
|
12
|
+
var StorageType;
|
|
13
|
+
(function (StorageType) {
|
|
14
|
+
StorageType["Local"] = "local";
|
|
15
|
+
StorageType["Memory"] = "memory";
|
|
16
|
+
StorageType["IndexedDB"] = "indexeddb";
|
|
17
|
+
StorageType["RestAPI"] = "restapi"; // Asynchronous REST API storage
|
|
18
|
+
})(StorageType || (StorageType = {}));
|
|
19
|
+
|
|
20
|
+
/******************************************************************************
|
|
21
|
+
Copyright (c) Microsoft Corporation.
|
|
22
|
+
|
|
23
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
24
|
+
purpose with or without fee is hereby granted.
|
|
25
|
+
|
|
26
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
27
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
28
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
29
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
30
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
31
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
32
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
33
|
+
***************************************************************************** */
|
|
34
|
+
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
function __rest(s, e) {
|
|
38
|
+
var t = {};
|
|
39
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
40
|
+
t[p] = s[p];
|
|
41
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
42
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
43
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
44
|
+
t[p[i]] = s[p[i]];
|
|
45
|
+
}
|
|
46
|
+
return t;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
50
|
+
var e = new Error(message);
|
|
51
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const warnedMessages = new Set();
|
|
55
|
+
function warnDeprecation(message) {
|
|
56
|
+
var _a, _b;
|
|
57
|
+
const isProduction = typeof globalThis !== 'undefined' &&
|
|
58
|
+
((_b = (_a = globalThis.process) === null || _a === void 0 ? void 0 : _a.env) === null || _b === void 0 ? void 0 : _b.NODE_ENV) === 'production';
|
|
59
|
+
if (isProduction || warnedMessages.has(message)) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
warnedMessages.add(message);
|
|
63
|
+
console.warn(`[react-achievements] ${message}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const AchievementContext = React.createContext(undefined);
|
|
67
|
+
const getAllAchievementRecord = (engine) => {
|
|
68
|
+
return Object.fromEntries(engine.getAllAchievements().map((achievement) => [
|
|
69
|
+
achievement.achievementId,
|
|
70
|
+
achievement,
|
|
71
|
+
]));
|
|
72
|
+
};
|
|
73
|
+
const AchievementProvider$1 = ({ achievements: achievementsConfig, storage = 'local', children, onError, useBuiltInUI, restApiConfig, engine: externalEngine, eventMapping, icons = {}, }) => {
|
|
74
|
+
if (useBuiltInUI !== undefined) {
|
|
75
|
+
warnDeprecation('`useBuiltInUI` is deprecated and is now a no-op because built-in UI is the default. It will be removed in 4.2.');
|
|
76
|
+
}
|
|
77
|
+
if (achievementsConfig && externalEngine) {
|
|
78
|
+
throw new Error('Cannot provide both "achievements" and "engine" props to AchievementProvider.\n\n' +
|
|
79
|
+
'Choose one pattern:\n' +
|
|
80
|
+
'1. Direct metric tracking: <AchievementProvider achievements={config}>\n' +
|
|
81
|
+
'2. Event-based tracking: <AchievementProvider engine={myEngine}>');
|
|
82
|
+
}
|
|
83
|
+
const isProviderCreatedEngine = Boolean(achievementsConfig);
|
|
84
|
+
const [engine] = React.useState(() => {
|
|
85
|
+
if (externalEngine) {
|
|
86
|
+
return externalEngine;
|
|
87
|
+
}
|
|
88
|
+
if (!achievementsConfig) {
|
|
89
|
+
throw new Error('AchievementProvider requires either "achievements" or "engine" prop.\n\n' +
|
|
90
|
+
'1. Direct metric tracking: <AchievementProvider achievements={config}>\n' +
|
|
91
|
+
'2. Event-based tracking: <AchievementProvider engine={myEngine}>');
|
|
92
|
+
}
|
|
93
|
+
return new achievementsEngine.AchievementEngine({
|
|
94
|
+
achievements: achievementsConfig,
|
|
95
|
+
storage: storage,
|
|
96
|
+
restApiConfig,
|
|
97
|
+
onError: onError,
|
|
98
|
+
eventMapping,
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
const [achievementState, setAchievementState] = React.useState(() => ({
|
|
102
|
+
unlocked: [...engine.getUnlocked()],
|
|
103
|
+
all: getAllAchievementRecord(engine),
|
|
104
|
+
}));
|
|
105
|
+
const syncAchievementState = React.useCallback(() => {
|
|
106
|
+
setAchievementState({
|
|
107
|
+
unlocked: [...engine.getUnlocked()],
|
|
108
|
+
all: getAllAchievementRecord(engine),
|
|
109
|
+
});
|
|
110
|
+
}, [engine]);
|
|
111
|
+
React.useEffect(() => {
|
|
112
|
+
return () => {
|
|
113
|
+
if (!externalEngine) {
|
|
114
|
+
engine.destroy();
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}, [engine, externalEngine]);
|
|
118
|
+
React.useEffect(() => {
|
|
119
|
+
const unsubscribeUnlocked = engine.on('achievement:unlocked', syncAchievementState);
|
|
120
|
+
const unsubscribeStateChanged = engine.on('state:changed', syncAchievementState);
|
|
121
|
+
return () => {
|
|
122
|
+
unsubscribeUnlocked();
|
|
123
|
+
unsubscribeStateChanged();
|
|
124
|
+
};
|
|
125
|
+
}, [engine, syncAchievementState]);
|
|
126
|
+
const update = (newMetrics) => {
|
|
127
|
+
engine.update(newMetrics);
|
|
128
|
+
};
|
|
129
|
+
const reset = () => {
|
|
130
|
+
engine.reset();
|
|
131
|
+
syncAchievementState();
|
|
132
|
+
};
|
|
133
|
+
const getState = () => {
|
|
134
|
+
const metrics = engine.getMetrics();
|
|
135
|
+
const unlocked = engine.getUnlocked();
|
|
136
|
+
const metricsInArrayFormat = {};
|
|
137
|
+
Object.entries(metrics).forEach(([key, value]) => {
|
|
138
|
+
metricsInArrayFormat[key] = Array.isArray(value) ? value : [value];
|
|
139
|
+
});
|
|
140
|
+
return {
|
|
141
|
+
metrics: metricsInArrayFormat,
|
|
142
|
+
unlocked: [...unlocked],
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
const exportData = () => {
|
|
146
|
+
return engine.export();
|
|
147
|
+
};
|
|
148
|
+
const importData = (jsonString, options) => {
|
|
149
|
+
const result = engine.import(jsonString, options);
|
|
150
|
+
syncAchievementState();
|
|
151
|
+
return result;
|
|
152
|
+
};
|
|
153
|
+
const getAllAchievements = () => {
|
|
154
|
+
return engine.getAllAchievements();
|
|
155
|
+
};
|
|
156
|
+
return (React.createElement(AchievementContext.Provider, { value: {
|
|
157
|
+
update,
|
|
158
|
+
achievements: achievementState,
|
|
159
|
+
reset,
|
|
160
|
+
getState,
|
|
161
|
+
exportData,
|
|
162
|
+
importData,
|
|
163
|
+
getAllAchievements,
|
|
164
|
+
engine,
|
|
165
|
+
icons,
|
|
166
|
+
_isLegacyPattern: isProviderCreatedEngine,
|
|
167
|
+
} }, children));
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Access the active AchievementEngine instance.
|
|
172
|
+
*
|
|
173
|
+
* In v4 this works with both provider-created engines (`achievements` prop) and
|
|
174
|
+
* injected engines (`engine` prop).
|
|
175
|
+
*/
|
|
176
|
+
const useAchievementEngine = () => {
|
|
177
|
+
const context = React.useContext(AchievementContext);
|
|
178
|
+
if (!context) {
|
|
179
|
+
throw new Error('useAchievementEngine must be used within an AchievementProvider.\n\n' +
|
|
180
|
+
'Wrap your component tree:\n' +
|
|
181
|
+
'<AchievementProvider achievements={achievements}>\n' +
|
|
182
|
+
' <YourComponent />\n' +
|
|
183
|
+
'</AchievementProvider>');
|
|
184
|
+
}
|
|
185
|
+
return context.engine;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Built-in theme presets
|
|
190
|
+
*/
|
|
191
|
+
const builtInThemes = {
|
|
192
|
+
/**
|
|
193
|
+
* Modern theme - Dark gradients with vibrant accents
|
|
194
|
+
* Inspired by contemporary achievement systems (Discord, Steam, Xbox)
|
|
195
|
+
*/
|
|
196
|
+
modern: {
|
|
197
|
+
name: 'modern',
|
|
198
|
+
notification: {
|
|
199
|
+
background: 'linear-gradient(135deg, rgba(30, 30, 50, 0.98) 0%, rgba(50, 50, 70, 0.98) 100%)',
|
|
200
|
+
textColor: '#ffffff',
|
|
201
|
+
accentColor: '#4CAF50',
|
|
202
|
+
borderRadius: '12px',
|
|
203
|
+
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.4)',
|
|
204
|
+
fontSize: {
|
|
205
|
+
header: '12px',
|
|
206
|
+
title: '18px',
|
|
207
|
+
description: '14px',
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
modal: {
|
|
211
|
+
overlayColor: 'rgba(0, 0, 0, 0.85)',
|
|
212
|
+
background: 'linear-gradient(135deg, #1e1e32 0%, #323246 100%)',
|
|
213
|
+
textColor: '#ffffff',
|
|
214
|
+
accentColor: '#4CAF50',
|
|
215
|
+
borderRadius: '16px',
|
|
216
|
+
headerFontSize: '28px',
|
|
217
|
+
},
|
|
218
|
+
confetti: {
|
|
219
|
+
colors: ['#FFD700', '#4CAF50', '#2196F3', '#FF6B6B'],
|
|
220
|
+
particleCount: 50,
|
|
221
|
+
shapes: ['circle', 'square'],
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
/**
|
|
225
|
+
* Minimal theme - Clean, light design with subtle accents
|
|
226
|
+
* Perfect for professional or minimalist applications
|
|
227
|
+
*/
|
|
228
|
+
minimal: {
|
|
229
|
+
name: 'minimal',
|
|
230
|
+
notification: {
|
|
231
|
+
background: 'rgba(255, 255, 255, 0.98)',
|
|
232
|
+
textColor: '#333333',
|
|
233
|
+
accentColor: '#4CAF50',
|
|
234
|
+
borderRadius: '8px',
|
|
235
|
+
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
|
236
|
+
fontSize: {
|
|
237
|
+
header: '11px',
|
|
238
|
+
title: '16px',
|
|
239
|
+
description: '13px',
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
modal: {
|
|
243
|
+
overlayColor: 'rgba(0, 0, 0, 0.5)',
|
|
244
|
+
background: '#ffffff',
|
|
245
|
+
textColor: '#333333',
|
|
246
|
+
accentColor: '#4CAF50',
|
|
247
|
+
borderRadius: '12px',
|
|
248
|
+
headerFontSize: '24px',
|
|
249
|
+
},
|
|
250
|
+
confetti: {
|
|
251
|
+
colors: ['#4CAF50', '#2196F3'],
|
|
252
|
+
particleCount: 30,
|
|
253
|
+
shapes: ['circle'],
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
/**
|
|
257
|
+
* Gamified theme - Modern gaming aesthetic with sci-fi colors
|
|
258
|
+
* Dark navy backgrounds with cyan and orange accents (2024 gaming trend)
|
|
259
|
+
* Features square/badge-shaped achievement cards
|
|
260
|
+
*/
|
|
261
|
+
gamified: {
|
|
262
|
+
name: 'gamified',
|
|
263
|
+
notification: {
|
|
264
|
+
background: 'linear-gradient(135deg, rgba(5, 8, 22, 0.98) 0%, rgba(15, 23, 42, 0.98) 100%)',
|
|
265
|
+
textColor: '#22d3ee', // Bright cyan
|
|
266
|
+
accentColor: '#f97316', // Bright orange
|
|
267
|
+
borderRadius: '6px',
|
|
268
|
+
boxShadow: '0 8px 32px rgba(34, 211, 238, 0.4), 0 0 20px rgba(249, 115, 22, 0.3)',
|
|
269
|
+
fontSize: {
|
|
270
|
+
header: '13px',
|
|
271
|
+
title: '20px',
|
|
272
|
+
description: '15px',
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
modal: {
|
|
276
|
+
overlayColor: 'rgba(5, 8, 22, 0.85)',
|
|
277
|
+
background: 'linear-gradient(135deg, #0f172a 0%, #050816 100%)',
|
|
278
|
+
textColor: '#22d3ee', // Bright cyan
|
|
279
|
+
accentColor: '#f97316', // Bright orange
|
|
280
|
+
borderRadius: '8px',
|
|
281
|
+
headerFontSize: '32px',
|
|
282
|
+
achievementCardBorderRadius: '8px', // Square badge-like cards
|
|
283
|
+
achievementLayout: 'badge', // Use badge/grid layout instead of horizontal list
|
|
284
|
+
},
|
|
285
|
+
confetti: {
|
|
286
|
+
colors: ['#22d3ee', '#f97316', '#a855f7', '#eab308'], // Cyan, orange, purple, yellow
|
|
287
|
+
particleCount: 100,
|
|
288
|
+
shapes: ['circle', 'square'],
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
/**
|
|
293
|
+
* Retrieve a theme by name (internal use only)
|
|
294
|
+
* Only checks built-in themes
|
|
295
|
+
*
|
|
296
|
+
* @param name - Theme name (built-in only)
|
|
297
|
+
* @returns Theme configuration or undefined if not found
|
|
298
|
+
* @internal
|
|
299
|
+
*/
|
|
300
|
+
function getTheme(name) {
|
|
301
|
+
return builtInThemes[name];
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const defaultAchievementIcons = {
|
|
305
|
+
// Essential fallback icons for system use
|
|
306
|
+
default: '⭐', // Fallback when no icon is provided
|
|
307
|
+
loading: '⏳', // For loading states
|
|
308
|
+
error: '⚠️', // For error states
|
|
309
|
+
success: '✅', // For success states
|
|
310
|
+
// A few common icons for backward compatibility
|
|
311
|
+
trophy: '🏆',
|
|
312
|
+
star: '⭐',
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Built-in notification component
|
|
317
|
+
* Modern, theme-aware achievement notification with smooth animations
|
|
318
|
+
*/
|
|
319
|
+
const BuiltInNotification = ({ achievement, onClose, duration = 5000, position = 'top-center', theme = 'modern', icons = {}, stackIndex = 0, }) => {
|
|
320
|
+
var _a, _b, _c;
|
|
321
|
+
const [isVisible, setIsVisible] = React.useState(false);
|
|
322
|
+
const [isExiting, setIsExiting] = React.useState(false);
|
|
323
|
+
// Merge custom icons with defaults
|
|
324
|
+
const mergedIcons = Object.assign(Object.assign({}, defaultAchievementIcons), icons);
|
|
325
|
+
// Get theme configuration
|
|
326
|
+
const themeConfig = getTheme(theme) || builtInThemes.modern;
|
|
327
|
+
const { notification: themeStyles } = themeConfig;
|
|
328
|
+
React.useEffect(() => {
|
|
329
|
+
// Slide in animation
|
|
330
|
+
const showTimer = setTimeout(() => setIsVisible(true), 10);
|
|
331
|
+
// Auto-dismiss
|
|
332
|
+
const dismissTimer = setTimeout(() => {
|
|
333
|
+
setIsExiting(true);
|
|
334
|
+
setTimeout(() => onClose === null || onClose === void 0 ? void 0 : onClose(), 300);
|
|
335
|
+
}, duration);
|
|
336
|
+
return () => {
|
|
337
|
+
clearTimeout(showTimer);
|
|
338
|
+
clearTimeout(dismissTimer);
|
|
339
|
+
};
|
|
340
|
+
}, [duration, onClose]);
|
|
341
|
+
const getPositionStyles = () => {
|
|
342
|
+
const stackedOffset = 20 + stackIndex * 104;
|
|
343
|
+
const base = {
|
|
344
|
+
position: 'fixed',
|
|
345
|
+
zIndex: 9999,
|
|
346
|
+
};
|
|
347
|
+
switch (position) {
|
|
348
|
+
case 'top-center':
|
|
349
|
+
return Object.assign(Object.assign({}, base), { top: stackedOffset, left: '50%', transform: 'translateX(-50%)' });
|
|
350
|
+
case 'top-left':
|
|
351
|
+
return Object.assign(Object.assign({}, base), { top: stackedOffset, left: 20 });
|
|
352
|
+
case 'top-right':
|
|
353
|
+
return Object.assign(Object.assign({}, base), { top: stackedOffset, right: 20 });
|
|
354
|
+
case 'bottom-center':
|
|
355
|
+
return Object.assign(Object.assign({}, base), { bottom: stackedOffset, left: '50%', transform: 'translateX(-50%)' });
|
|
356
|
+
case 'bottom-left':
|
|
357
|
+
return Object.assign(Object.assign({}, base), { bottom: stackedOffset, left: 20 });
|
|
358
|
+
case 'bottom-right':
|
|
359
|
+
return Object.assign(Object.assign({}, base), { bottom: stackedOffset, right: 20 });
|
|
360
|
+
default:
|
|
361
|
+
return Object.assign(Object.assign({}, base), { top: stackedOffset, left: '50%', transform: 'translateX(-50%)' });
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
const containerStyles = Object.assign(Object.assign({}, getPositionStyles()), { background: themeStyles.background, borderRadius: themeStyles.borderRadius, boxShadow: themeStyles.boxShadow, padding: '16px 24px', minWidth: '320px', maxWidth: '500px', display: 'flex', alignItems: 'center', gap: '16px', opacity: isVisible && !isExiting ? 1 : 0, transform: position.startsWith('top')
|
|
365
|
+
? `translateY(${isVisible && !isExiting ? '0' : '-20px'}) ${position.includes('center') ? 'translateX(-50%)' : ''}`
|
|
366
|
+
: `translateY(${isVisible && !isExiting ? '0' : '20px'}) ${position.includes('center') ? 'translateX(-50%)' : ''}`, transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', pointerEvents: isVisible ? 'auto' : 'none' });
|
|
367
|
+
const iconStyles = {
|
|
368
|
+
fontSize: '48px',
|
|
369
|
+
lineHeight: 1,
|
|
370
|
+
flexShrink: 0,
|
|
371
|
+
};
|
|
372
|
+
const contentStyles = {
|
|
373
|
+
flex: 1,
|
|
374
|
+
color: themeStyles.textColor,
|
|
375
|
+
minWidth: 0,
|
|
376
|
+
};
|
|
377
|
+
const headerStyles = {
|
|
378
|
+
fontSize: ((_a = themeStyles.fontSize) === null || _a === void 0 ? void 0 : _a.header) || '12px',
|
|
379
|
+
textTransform: 'uppercase',
|
|
380
|
+
letterSpacing: '1px',
|
|
381
|
+
opacity: 0.8,
|
|
382
|
+
marginBottom: '4px',
|
|
383
|
+
color: themeStyles.accentColor,
|
|
384
|
+
fontWeight: 600,
|
|
385
|
+
};
|
|
386
|
+
const titleStyles = {
|
|
387
|
+
fontSize: ((_b = themeStyles.fontSize) === null || _b === void 0 ? void 0 : _b.title) || '18px',
|
|
388
|
+
fontWeight: 'bold',
|
|
389
|
+
marginBottom: '4px',
|
|
390
|
+
overflow: 'hidden',
|
|
391
|
+
textOverflow: 'ellipsis',
|
|
392
|
+
whiteSpace: 'nowrap',
|
|
393
|
+
};
|
|
394
|
+
const descriptionStyles = {
|
|
395
|
+
fontSize: ((_c = themeStyles.fontSize) === null || _c === void 0 ? void 0 : _c.description) || '14px',
|
|
396
|
+
opacity: 0.9,
|
|
397
|
+
overflow: 'hidden',
|
|
398
|
+
textOverflow: 'ellipsis',
|
|
399
|
+
display: '-webkit-box',
|
|
400
|
+
WebkitLineClamp: 2,
|
|
401
|
+
WebkitBoxOrient: 'vertical',
|
|
402
|
+
};
|
|
403
|
+
const closeButtonStyles = {
|
|
404
|
+
background: 'none',
|
|
405
|
+
border: 'none',
|
|
406
|
+
color: themeStyles.textColor,
|
|
407
|
+
fontSize: '24px',
|
|
408
|
+
cursor: 'pointer',
|
|
409
|
+
opacity: 0.6,
|
|
410
|
+
transition: 'opacity 0.2s',
|
|
411
|
+
padding: '4px',
|
|
412
|
+
flexShrink: 0,
|
|
413
|
+
lineHeight: 1,
|
|
414
|
+
};
|
|
415
|
+
// If achievementIconKey exists but not in mergedIcons, use it directly (might be an emoji)
|
|
416
|
+
// Otherwise, look up in mergedIcons or fall back to default
|
|
417
|
+
const icon = (achievement.achievementIconKey &&
|
|
418
|
+
mergedIcons[achievement.achievementIconKey]) ||
|
|
419
|
+
achievement.achievementIconKey ||
|
|
420
|
+
mergedIcons.default ||
|
|
421
|
+
'⭐';
|
|
422
|
+
return (React.createElement("div", { style: containerStyles, "data-testid": "built-in-notification" },
|
|
423
|
+
React.createElement("div", { style: iconStyles }, icon),
|
|
424
|
+
React.createElement("div", { style: contentStyles },
|
|
425
|
+
React.createElement("div", { style: headerStyles }, "Achievement Unlocked!"),
|
|
426
|
+
React.createElement("div", { style: titleStyles }, achievement.achievementTitle),
|
|
427
|
+
achievement.achievementDescription && (React.createElement("div", { style: descriptionStyles }, achievement.achievementDescription))),
|
|
428
|
+
React.createElement("button", { onClick: () => {
|
|
429
|
+
setIsExiting(true);
|
|
430
|
+
setTimeout(() => onClose === null || onClose === void 0 ? void 0 : onClose(), 300);
|
|
431
|
+
}, style: closeButtonStyles, onMouseEnter: (e) => (e.currentTarget.style.opacity = '1'), onMouseLeave: (e) => (e.currentTarget.style.opacity = '0.6'), "aria-label": "Close notification" }, "\u00D7")));
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Hook to track window dimensions
|
|
436
|
+
* Replacement for react-use's useWindowSize
|
|
437
|
+
*
|
|
438
|
+
* @returns Object with width and height properties
|
|
439
|
+
*
|
|
440
|
+
* @example
|
|
441
|
+
* ```tsx
|
|
442
|
+
* const { width, height } = useWindowSize();
|
|
443
|
+
* console.log(`Window size: ${width}x${height}`);
|
|
444
|
+
* ```
|
|
445
|
+
*/
|
|
446
|
+
function useWindowSize() {
|
|
447
|
+
const [size, setSize] = React.useState({
|
|
448
|
+
width: typeof window !== 'undefined' ? window.innerWidth : 0,
|
|
449
|
+
height: typeof window !== 'undefined' ? window.innerHeight : 0,
|
|
450
|
+
});
|
|
451
|
+
React.useEffect(() => {
|
|
452
|
+
// Handle SSR - window may not be defined
|
|
453
|
+
if (typeof window === 'undefined') {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const handleResize = () => {
|
|
457
|
+
setSize({
|
|
458
|
+
width: window.innerWidth,
|
|
459
|
+
height: window.innerHeight,
|
|
460
|
+
});
|
|
461
|
+
};
|
|
462
|
+
// Set initial size
|
|
463
|
+
handleResize();
|
|
464
|
+
// Add event listener
|
|
465
|
+
window.addEventListener('resize', handleResize);
|
|
466
|
+
// Cleanup
|
|
467
|
+
return () => {
|
|
468
|
+
window.removeEventListener('resize', handleResize);
|
|
469
|
+
};
|
|
470
|
+
}, []);
|
|
471
|
+
return size;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Built-in confetti component
|
|
476
|
+
* Lightweight CSS-based confetti animation
|
|
477
|
+
*/
|
|
478
|
+
const BuiltInConfetti = ({ show, duration = 5000, particleCount = 50, colors = ['#FFD700', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7'], }) => {
|
|
479
|
+
const [isVisible, setIsVisible] = React.useState(false);
|
|
480
|
+
const { width, height } = useWindowSize();
|
|
481
|
+
React.useEffect(() => {
|
|
482
|
+
if (show) {
|
|
483
|
+
setIsVisible(true);
|
|
484
|
+
const timer = setTimeout(() => setIsVisible(false), duration);
|
|
485
|
+
return () => clearTimeout(timer);
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
setIsVisible(false);
|
|
489
|
+
}
|
|
490
|
+
}, [show, duration]);
|
|
491
|
+
if (!isVisible)
|
|
492
|
+
return null;
|
|
493
|
+
const containerStyles = {
|
|
494
|
+
position: 'fixed',
|
|
495
|
+
top: 0,
|
|
496
|
+
left: 0,
|
|
497
|
+
width: '100%',
|
|
498
|
+
height: '100%',
|
|
499
|
+
pointerEvents: 'none',
|
|
500
|
+
zIndex: 10001,
|
|
501
|
+
overflow: 'hidden',
|
|
502
|
+
};
|
|
503
|
+
// Generate particles
|
|
504
|
+
const particles = Array.from({ length: particleCount }, (_, i) => {
|
|
505
|
+
const color = colors[Math.floor(Math.random() * colors.length)];
|
|
506
|
+
const startX = Math.random() * width;
|
|
507
|
+
const rotation = Math.random() * 360;
|
|
508
|
+
const fallDuration = 3 + Math.random() * 2; // 3-5 seconds
|
|
509
|
+
const delay = Math.random() * 0.5; // 0-0.5s delay
|
|
510
|
+
const shape = Math.random() > 0.5 ? 'circle' : 'square';
|
|
511
|
+
const particleStyles = {
|
|
512
|
+
position: 'absolute',
|
|
513
|
+
top: '-20px',
|
|
514
|
+
left: `${startX}px`,
|
|
515
|
+
width: '10px',
|
|
516
|
+
height: '10px',
|
|
517
|
+
backgroundColor: color,
|
|
518
|
+
borderRadius: shape === 'circle' ? '50%' : '0',
|
|
519
|
+
transform: `rotate(${rotation}deg)`,
|
|
520
|
+
animation: `confettiFall ${fallDuration}s linear ${delay}s forwards`,
|
|
521
|
+
opacity: 0.9,
|
|
522
|
+
};
|
|
523
|
+
return React.createElement("div", { key: i, style: particleStyles, "data-testid": "confetti-particle" });
|
|
524
|
+
});
|
|
525
|
+
return (React.createElement(React.Fragment, null,
|
|
526
|
+
React.createElement("style", null, `
|
|
527
|
+
@keyframes confettiFall {
|
|
528
|
+
0% {
|
|
529
|
+
transform: translateY(0) rotate(0deg);
|
|
530
|
+
opacity: 1;
|
|
531
|
+
}
|
|
532
|
+
100% {
|
|
533
|
+
transform: translateY(${height + 50}px) rotate(720deg);
|
|
534
|
+
opacity: 0;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
`),
|
|
538
|
+
React.createElement("div", { style: containerStyles, "data-testid": "built-in-confetti" }, particles)));
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
const NOTIFICATION_DURATION_MS = 5000;
|
|
542
|
+
const AchievementUIContext = React.createContext({
|
|
543
|
+
icons: {},
|
|
544
|
+
ui: {},
|
|
545
|
+
});
|
|
546
|
+
const AchievementEffects = ({ icons, ui }) => {
|
|
547
|
+
const engine = useAchievementEngine();
|
|
548
|
+
const seenAchievementsRef = React.useRef(new Set(engine.getUnlocked()));
|
|
549
|
+
const confettiTimerRef = React.useRef(null);
|
|
550
|
+
const [showConfetti, setShowConfetti] = React.useState(false);
|
|
551
|
+
const [notifications, setNotifications] = React.useState([]);
|
|
552
|
+
React.useEffect(() => {
|
|
553
|
+
const unsubscribeUnlocked = engine.on('achievement:unlocked', (event) => {
|
|
554
|
+
if (seenAchievementsRef.current.has(event.achievementId)) {
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
seenAchievementsRef.current.add(event.achievementId);
|
|
558
|
+
const unlockedAchievement = {
|
|
559
|
+
achievementId: event.achievementId,
|
|
560
|
+
achievementTitle: event.achievementTitle,
|
|
561
|
+
achievementDescription: event.achievementDescription,
|
|
562
|
+
achievementIconKey: event.achievementIconKey,
|
|
563
|
+
isUnlocked: true,
|
|
564
|
+
};
|
|
565
|
+
if (ui.enableNotifications !== false) {
|
|
566
|
+
setNotifications((currentNotifications) => {
|
|
567
|
+
if (currentNotifications.some((notification) => notification.achievementId === unlockedAchievement.achievementId)) {
|
|
568
|
+
return currentNotifications;
|
|
569
|
+
}
|
|
570
|
+
return [...currentNotifications, unlockedAchievement];
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
if (ui.enableConfetti !== false) {
|
|
574
|
+
if (confettiTimerRef.current) {
|
|
575
|
+
clearTimeout(confettiTimerRef.current);
|
|
576
|
+
}
|
|
577
|
+
setShowConfetti(true);
|
|
578
|
+
confettiTimerRef.current = setTimeout(() => {
|
|
579
|
+
setShowConfetti(false);
|
|
580
|
+
confettiTimerRef.current = null;
|
|
581
|
+
}, NOTIFICATION_DURATION_MS);
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
const unsubscribeStateChanged = engine.on('state:changed', () => {
|
|
585
|
+
const unlocked = new Set(engine.getUnlocked());
|
|
586
|
+
seenAchievementsRef.current.forEach((achievementId) => {
|
|
587
|
+
if (!unlocked.has(achievementId)) {
|
|
588
|
+
seenAchievementsRef.current.delete(achievementId);
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
return () => {
|
|
593
|
+
unsubscribeUnlocked();
|
|
594
|
+
unsubscribeStateChanged();
|
|
595
|
+
if (confettiTimerRef.current) {
|
|
596
|
+
clearTimeout(confettiTimerRef.current);
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
}, [engine, ui.enableConfetti, ui.enableNotifications]);
|
|
600
|
+
const NotificationComponent = ui.NotificationComponent || BuiltInNotification;
|
|
601
|
+
const ConfettiComponentResolved = ui.ConfettiComponent || BuiltInConfetti;
|
|
602
|
+
return (React.createElement(React.Fragment, null,
|
|
603
|
+
ui.enableNotifications !== false &&
|
|
604
|
+
notifications.map((notification, index) => (React.createElement(NotificationComponent, { key: notification.achievementId, achievement: notification, onClose: () => setNotifications((currentNotifications) => currentNotifications.filter((currentNotification) => currentNotification.achievementId !== notification.achievementId)), duration: NOTIFICATION_DURATION_MS, position: ui.notificationPosition || 'top-center', theme: ui.theme || 'modern', icons: icons, stackIndex: index }))),
|
|
605
|
+
ui.enableConfetti !== false && (React.createElement(ConfettiComponentResolved, { show: showConfetti, duration: NOTIFICATION_DURATION_MS }))));
|
|
606
|
+
};
|
|
607
|
+
const AchievementProvider = (_a) => {
|
|
608
|
+
var { children, icons = {}, ui = {}, useBuiltInUI } = _a, providerProps = __rest(_a, ["children", "icons", "ui", "useBuiltInUI"]);
|
|
609
|
+
if (useBuiltInUI !== undefined) {
|
|
610
|
+
warnDeprecation('`useBuiltInUI` is deprecated and is now a no-op because built-in UI is the default. It will be removed in 4.2.');
|
|
611
|
+
}
|
|
612
|
+
const uiContextValue = React.useMemo(() => ({ icons, ui }), [icons, ui]);
|
|
613
|
+
return (React.createElement(AchievementUIContext.Provider, { value: uiContextValue },
|
|
614
|
+
React.createElement(AchievementProvider$1, Object.assign({}, providerProps, { icons: icons }),
|
|
615
|
+
children,
|
|
616
|
+
React.createElement(AchievementEffects, { icons: icons, ui: ui }))));
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
const useAchievements = () => {
|
|
620
|
+
const context = React.useContext(AchievementContext);
|
|
621
|
+
if (!context) {
|
|
622
|
+
throw new Error('useAchievements must be used within an AchievementProvider');
|
|
623
|
+
}
|
|
624
|
+
return context;
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
const useAchievementState = () => {
|
|
628
|
+
const { achievements, getAllAchievements, getState } = useAchievements();
|
|
629
|
+
const allAchievements = getAllAchievements();
|
|
630
|
+
const unlockedIds = achievements.unlocked;
|
|
631
|
+
const unlockedAchievementSet = new Set(unlockedIds);
|
|
632
|
+
const unlockedAchievements = allAchievements.filter((achievement) => unlockedAchievementSet.has(achievement.achievementId));
|
|
633
|
+
return {
|
|
634
|
+
unlockedIds,
|
|
635
|
+
unlockedAchievements,
|
|
636
|
+
allAchievements,
|
|
637
|
+
unlockedCount: unlockedIds.length,
|
|
638
|
+
totalCount: allAchievements.length,
|
|
639
|
+
metrics: getState().metrics,
|
|
640
|
+
};
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
const defaultStyles = {
|
|
644
|
+
badgesButton: {
|
|
645
|
+
backgroundColor: '#4CAF50',
|
|
646
|
+
color: 'white',
|
|
647
|
+
padding: '10px 20px',
|
|
648
|
+
border: 'none',
|
|
649
|
+
borderRadius: '20px',
|
|
650
|
+
cursor: 'pointer',
|
|
651
|
+
display: 'flex',
|
|
652
|
+
alignItems: 'center',
|
|
653
|
+
gap: '8px',
|
|
654
|
+
fontSize: '16px',
|
|
655
|
+
boxShadow: '0 2px 5px rgba(0,0,0,0.2)',
|
|
656
|
+
transition: 'transform 0.2s ease-in-out',
|
|
657
|
+
},
|
|
658
|
+
badgesModal: {
|
|
659
|
+
overlay: {
|
|
660
|
+
position: 'fixed',
|
|
661
|
+
top: 0,
|
|
662
|
+
right: 0,
|
|
663
|
+
bottom: 0,
|
|
664
|
+
left: 0,
|
|
665
|
+
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
|
666
|
+
display: 'flex',
|
|
667
|
+
alignItems: 'center',
|
|
668
|
+
justifyContent: 'center',
|
|
669
|
+
zIndex: 10000,
|
|
670
|
+
},
|
|
671
|
+
content: {
|
|
672
|
+
position: 'relative',
|
|
673
|
+
background: '#fff',
|
|
674
|
+
borderRadius: '8px',
|
|
675
|
+
padding: '20px',
|
|
676
|
+
maxWidth: '500px',
|
|
677
|
+
width: '90%',
|
|
678
|
+
maxHeight: '80vh',
|
|
679
|
+
overflow: 'auto',
|
|
680
|
+
},
|
|
681
|
+
header: {
|
|
682
|
+
display: 'flex',
|
|
683
|
+
justifyContent: 'space-between',
|
|
684
|
+
alignItems: 'center',
|
|
685
|
+
marginBottom: '20px',
|
|
686
|
+
},
|
|
687
|
+
closeButton: {
|
|
688
|
+
background: 'none',
|
|
689
|
+
border: 'none',
|
|
690
|
+
fontSize: '24px',
|
|
691
|
+
cursor: 'pointer',
|
|
692
|
+
padding: '0',
|
|
693
|
+
},
|
|
694
|
+
achievementList: {
|
|
695
|
+
display: 'flex',
|
|
696
|
+
flexDirection: 'column',
|
|
697
|
+
gap: '16px',
|
|
698
|
+
},
|
|
699
|
+
achievementItem: {
|
|
700
|
+
display: 'flex',
|
|
701
|
+
gap: '16px',
|
|
702
|
+
padding: '16px',
|
|
703
|
+
borderRadius: '8px',
|
|
704
|
+
backgroundColor: '#f5f5f5',
|
|
705
|
+
alignItems: 'center',
|
|
706
|
+
},
|
|
707
|
+
achievementTitle: {
|
|
708
|
+
margin: '0',
|
|
709
|
+
fontSize: '18px',
|
|
710
|
+
fontWeight: 'bold',
|
|
711
|
+
},
|
|
712
|
+
achievementDescription: {
|
|
713
|
+
margin: '4px 0 0 0',
|
|
714
|
+
color: '#666',
|
|
715
|
+
},
|
|
716
|
+
achievementIcon: {
|
|
717
|
+
fontSize: '32px',
|
|
718
|
+
display: 'flex',
|
|
719
|
+
alignItems: 'center',
|
|
720
|
+
justifyContent: 'center',
|
|
721
|
+
},
|
|
722
|
+
lockIcon: {
|
|
723
|
+
fontSize: '24px',
|
|
724
|
+
position: 'absolute',
|
|
725
|
+
top: '50%',
|
|
726
|
+
right: '16px',
|
|
727
|
+
transform: 'translateY(-50%)',
|
|
728
|
+
},
|
|
729
|
+
lockedAchievementItem: {
|
|
730
|
+
display: 'flex',
|
|
731
|
+
gap: '16px',
|
|
732
|
+
padding: '16px',
|
|
733
|
+
borderRadius: '8px',
|
|
734
|
+
backgroundColor: '#e0e0e0',
|
|
735
|
+
alignItems: 'center',
|
|
736
|
+
opacity: 0.5,
|
|
737
|
+
},
|
|
738
|
+
},
|
|
739
|
+
levelProgress: {
|
|
740
|
+
container: {
|
|
741
|
+
display: 'flex',
|
|
742
|
+
flexDirection: 'column',
|
|
743
|
+
gap: '8px',
|
|
744
|
+
padding: '12px 16px',
|
|
745
|
+
},
|
|
746
|
+
header: {
|
|
747
|
+
display: 'flex',
|
|
748
|
+
alignItems: 'center',
|
|
749
|
+
justifyContent: 'space-between',
|
|
750
|
+
gap: '12px',
|
|
751
|
+
fontWeight: 600,
|
|
752
|
+
},
|
|
753
|
+
levelLabel: {
|
|
754
|
+
fontSize: '14px',
|
|
755
|
+
},
|
|
756
|
+
valueText: {
|
|
757
|
+
fontSize: '12px',
|
|
758
|
+
opacity: 0.8,
|
|
759
|
+
},
|
|
760
|
+
progressTrack: {
|
|
761
|
+
position: 'relative',
|
|
762
|
+
height: '10px',
|
|
763
|
+
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
|
764
|
+
borderRadius: '999px',
|
|
765
|
+
overflow: 'hidden',
|
|
766
|
+
},
|
|
767
|
+
progressBar: {
|
|
768
|
+
height: '100%',
|
|
769
|
+
borderRadius: '999px',
|
|
770
|
+
transition: 'width 0.2s ease-in-out',
|
|
771
|
+
},
|
|
772
|
+
progressText: {
|
|
773
|
+
fontSize: '12px',
|
|
774
|
+
opacity: 0.8,
|
|
775
|
+
textAlign: 'right',
|
|
776
|
+
},
|
|
777
|
+
},
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
const resolveIcon = (achievement, icons) => {
|
|
781
|
+
return ((achievement.achievementIconKey && icons[achievement.achievementIconKey]) ||
|
|
782
|
+
achievement.achievementIconKey ||
|
|
783
|
+
icons.default ||
|
|
784
|
+
'⭐');
|
|
785
|
+
};
|
|
786
|
+
const AchievementsList = ({ achievements, showLocked = true, showUnlockConditions = false, icons = {}, styles = {}, emptyState, className, renderAchievement, }) => {
|
|
787
|
+
const context = React.useContext(AchievementContext);
|
|
788
|
+
const uiContext = React.useContext(AchievementUIContext);
|
|
789
|
+
const mergedIcons = Object.assign(Object.assign(Object.assign(Object.assign({}, defaultAchievementIcons), context === null || context === void 0 ? void 0 : context.icons), uiContext.icons), icons);
|
|
790
|
+
const sourceAchievements = achievements || (context === null || context === void 0 ? void 0 : context.getAllAchievements());
|
|
791
|
+
if (!sourceAchievements) {
|
|
792
|
+
throw new Error('AchievementsList requires either an achievements prop or an AchievementProvider parent.');
|
|
793
|
+
}
|
|
794
|
+
const achievementsToDisplay = showLocked
|
|
795
|
+
? sourceAchievements
|
|
796
|
+
: sourceAchievements.filter((achievement) => achievement.isUnlocked);
|
|
797
|
+
if (achievementsToDisplay.length === 0) {
|
|
798
|
+
return (React.createElement("p", { style: { textAlign: 'center', color: '#666' } }, emptyState || 'No achievements configured.'));
|
|
799
|
+
}
|
|
800
|
+
return (React.createElement("div", { className: className, style: Object.assign(Object.assign({}, defaultStyles.badgesModal.achievementList), styles === null || styles === void 0 ? void 0 : styles.achievementList), "data-testid": "achievements-list" }, achievementsToDisplay.map((achievement, index) => {
|
|
801
|
+
const isLocked = !achievement.isUnlocked;
|
|
802
|
+
const icon = resolveIcon(achievement, mergedIcons);
|
|
803
|
+
if (renderAchievement) {
|
|
804
|
+
return (React.createElement(React.Fragment, { key: achievement.achievementId }, renderAchievement({ achievement, isLocked, icon, index })));
|
|
805
|
+
}
|
|
806
|
+
return (React.createElement("div", { key: achievement.achievementId, style: Object.assign(Object.assign(Object.assign({}, (isLocked
|
|
807
|
+
? Object.assign(Object.assign({}, defaultStyles.badgesModal.lockedAchievementItem), styles === null || styles === void 0 ? void 0 : styles.lockedAchievementItem) : defaultStyles.badgesModal.achievementItem)), styles === null || styles === void 0 ? void 0 : styles.achievementItem), { position: 'relative' }), "data-testid": "achievement-list-item", "data-unlocked": achievement.isUnlocked ? 'true' : 'false' },
|
|
808
|
+
React.createElement("div", { style: Object.assign(Object.assign(Object.assign({}, defaultStyles.badgesModal.achievementIcon), styles === null || styles === void 0 ? void 0 : styles.achievementIcon), { opacity: isLocked ? 0.4 : 1 }) }, icon),
|
|
809
|
+
React.createElement("div", { style: { flex: 1 } },
|
|
810
|
+
React.createElement("h3", { style: Object.assign(Object.assign(Object.assign({}, defaultStyles.badgesModal.achievementTitle), styles === null || styles === void 0 ? void 0 : styles.achievementTitle), { color: isLocked ? '#999' : undefined }) }, achievement.achievementTitle),
|
|
811
|
+
achievement.achievementDescription && (React.createElement("p", { style: Object.assign(Object.assign(Object.assign({}, defaultStyles.badgesModal.achievementDescription), styles === null || styles === void 0 ? void 0 : styles.achievementDescription), { color: isLocked ? '#aaa' : '#666' }) },
|
|
812
|
+
achievement.achievementDescription,
|
|
813
|
+
showUnlockConditions && isLocked && (React.createElement("span", { style: {
|
|
814
|
+
display: 'block',
|
|
815
|
+
fontSize: '12px',
|
|
816
|
+
marginTop: '4px',
|
|
817
|
+
fontStyle: 'italic',
|
|
818
|
+
color: '#888',
|
|
819
|
+
} },
|
|
820
|
+
"\uD83D\uDD13 ",
|
|
821
|
+
achievement.achievementDescription))))),
|
|
822
|
+
isLocked && (React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.badgesModal.lockIcon), styles === null || styles === void 0 ? void 0 : styles.lockIcon) }, "\uD83D\uDD12"))));
|
|
823
|
+
})));
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
const AchievementsModal = ({ isOpen, onClose, achievements, title = '🏆 Achievements', styles = {}, icons = {}, showLocked = true, showUnlockConditions = false, emptyState, renderAchievement, theme, }) => {
|
|
827
|
+
const context = React.useContext(AchievementContext);
|
|
828
|
+
const uiContext = React.useContext(AchievementUIContext);
|
|
829
|
+
React.useEffect(() => {
|
|
830
|
+
if (!isOpen || typeof document === 'undefined') {
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
const previousOverflow = document.body.style.overflow;
|
|
834
|
+
document.body.style.overflow = 'hidden';
|
|
835
|
+
const handleKeyDown = (event) => {
|
|
836
|
+
if (event.key === 'Escape') {
|
|
837
|
+
onClose();
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
841
|
+
return () => {
|
|
842
|
+
document.body.style.overflow = previousOverflow;
|
|
843
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
844
|
+
};
|
|
845
|
+
}, [isOpen, onClose]);
|
|
846
|
+
if (!isOpen)
|
|
847
|
+
return null;
|
|
848
|
+
const CustomModal = uiContext.ui.ModalComponent;
|
|
849
|
+
const sourceAchievements = achievements || (context === null || context === void 0 ? void 0 : context.getAllAchievements());
|
|
850
|
+
const modalAchievements = showLocked
|
|
851
|
+
? sourceAchievements
|
|
852
|
+
: sourceAchievements === null || sourceAchievements === void 0 ? void 0 : sourceAchievements.filter((achievement) => achievement.isUnlocked);
|
|
853
|
+
const resolvedTheme = theme || uiContext.ui.theme || 'modern';
|
|
854
|
+
const mergedIcons = Object.assign(Object.assign(Object.assign(Object.assign({}, defaultAchievementIcons), context === null || context === void 0 ? void 0 : context.icons), uiContext.icons), icons);
|
|
855
|
+
if (CustomModal) {
|
|
856
|
+
if (!modalAchievements) {
|
|
857
|
+
throw new Error('AchievementsModal requires either an achievements prop or an AchievementProvider parent.');
|
|
858
|
+
}
|
|
859
|
+
return (React.createElement(CustomModal, { isOpen: isOpen, onClose: onClose, achievements: modalAchievements, icons: mergedIcons, theme: resolvedTheme }));
|
|
860
|
+
}
|
|
861
|
+
return (React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.badgesModal.overlay), styles === null || styles === void 0 ? void 0 : styles.overlay), role: "presentation", onClick: onClose, "data-testid": "achievements-modal-overlay" },
|
|
862
|
+
React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.badgesModal.content), styles === null || styles === void 0 ? void 0 : styles.content), role: "dialog", "aria-modal": "true", "aria-labelledby": "achievements-modal-title", onClick: (event) => event.stopPropagation(), "data-testid": "achievements-modal" },
|
|
863
|
+
React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.badgesModal.header), styles === null || styles === void 0 ? void 0 : styles.header) },
|
|
864
|
+
React.createElement("h2", { id: "achievements-modal-title", style: { margin: 0 } }, title),
|
|
865
|
+
React.createElement("button", { onClick: onClose, style: Object.assign(Object.assign({}, defaultStyles.badgesModal.closeButton), styles === null || styles === void 0 ? void 0 : styles.closeButton), "aria-label": "Close" }, "\u00D7")),
|
|
866
|
+
React.createElement(AchievementsList, { achievements: modalAchievements, showLocked: showLocked, showUnlockConditions: showUnlockConditions, icons: icons, styles: styles, emptyState: emptyState, renderAchievement: renderAchievement }))));
|
|
867
|
+
};
|
|
868
|
+
|
|
869
|
+
const getPositionStyles$1 = (position) => {
|
|
870
|
+
const base = {
|
|
871
|
+
position: 'fixed',
|
|
872
|
+
margin: '20px',
|
|
873
|
+
zIndex: 1000,
|
|
874
|
+
};
|
|
875
|
+
switch (position) {
|
|
876
|
+
case 'top-left':
|
|
877
|
+
return Object.assign(Object.assign({}, base), { top: 0, left: 0 });
|
|
878
|
+
case 'top-right':
|
|
879
|
+
return Object.assign(Object.assign({}, base), { top: 0, right: 0 });
|
|
880
|
+
case 'bottom-left':
|
|
881
|
+
return Object.assign(Object.assign({}, base), { bottom: 0, left: 0 });
|
|
882
|
+
case 'bottom-right':
|
|
883
|
+
return Object.assign(Object.assign({}, base), { bottom: 0, right: 0 });
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
const AchievementsWidget = ({ position = 'bottom-right', placement = 'fixed', showAllAchievements = true, showUnlockConditions = false, showCount = true, icons, theme, label = 'Achievements', icon = '🏆', triggerClassName, renderTrigger, buttonStyles, modalStyles, modalTitle, emptyState, renderAchievement, }) => {
|
|
887
|
+
const uiContext = React.useContext(AchievementUIContext);
|
|
888
|
+
const [isModalOpen, setIsModalOpen] = React.useState(false);
|
|
889
|
+
const { unlockedAchievements, allAchievements, unlockedCount, totalCount } = useAchievementState();
|
|
890
|
+
const resolvedTheme = theme || uiContext.ui.theme || 'modern';
|
|
891
|
+
const themeConfig = getTheme(resolvedTheme) || builtInThemes.modern;
|
|
892
|
+
const modalAchievements = showAllAchievements ? allAchievements : unlockedAchievements;
|
|
893
|
+
const openModal = () => setIsModalOpen(true);
|
|
894
|
+
const buttonBaseStyles = placement === 'inline'
|
|
895
|
+
? Object.assign({ backgroundColor: 'transparent', color: 'inherit', padding: '10px 12px', border: 'none', borderRadius: '6px', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px', font: 'inherit', width: '100%', textAlign: 'left' }, buttonStyles) : Object.assign(Object.assign({ backgroundColor: themeConfig.notification.accentColor, color: 'white', padding: '10px 20px', border: 'none', borderRadius: '20px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px', fontSize: '16px', boxShadow: '0 2px 5px rgba(0,0,0,0.2)', transition: 'transform 0.2s ease-in-out' }, getPositionStyles$1(position)), buttonStyles);
|
|
896
|
+
const buttonProps = {
|
|
897
|
+
type: 'button',
|
|
898
|
+
onClick: openModal,
|
|
899
|
+
style: buttonBaseStyles,
|
|
900
|
+
className: triggerClassName,
|
|
901
|
+
'data-placement': placement,
|
|
902
|
+
'data-testid': 'achievements-widget-button',
|
|
903
|
+
'aria-label': `${label}: ${unlockedCount} of ${totalCount} achievements unlocked`,
|
|
904
|
+
};
|
|
905
|
+
return (React.createElement(React.Fragment, null,
|
|
906
|
+
renderTrigger ? (renderTrigger({
|
|
907
|
+
open: openModal,
|
|
908
|
+
label,
|
|
909
|
+
unlockedCount,
|
|
910
|
+
totalCount,
|
|
911
|
+
unlockedAchievements,
|
|
912
|
+
allAchievements,
|
|
913
|
+
buttonProps,
|
|
914
|
+
})) : (React.createElement("button", Object.assign({}, buttonProps, { onMouseEnter: (event) => {
|
|
915
|
+
if (placement !== 'inline') {
|
|
916
|
+
event.currentTarget.style.transform = 'scale(1.05)';
|
|
917
|
+
}
|
|
918
|
+
}, onMouseLeave: (event) => {
|
|
919
|
+
if (placement !== 'inline') {
|
|
920
|
+
event.currentTarget.style.transform = 'scale(1)';
|
|
921
|
+
}
|
|
922
|
+
} }),
|
|
923
|
+
React.createElement("span", null, icon),
|
|
924
|
+
React.createElement("span", { style: { flex: 1 } }, label),
|
|
925
|
+
showCount && React.createElement("span", null, unlockedCount))),
|
|
926
|
+
React.createElement(AchievementsModal, { isOpen: isModalOpen, onClose: () => setIsModalOpen(false), achievements: modalAchievements, showUnlockConditions: showUnlockConditions, icons: icons, styles: modalStyles, title: modalTitle, emptyState: emptyState, renderAchievement: renderAchievement, theme: resolvedTheme })));
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
const getPositionStyles = (position) => {
|
|
930
|
+
const base = {
|
|
931
|
+
position: 'fixed',
|
|
932
|
+
margin: '20px',
|
|
933
|
+
zIndex: 1000,
|
|
934
|
+
};
|
|
935
|
+
switch (position) {
|
|
936
|
+
case 'top-left':
|
|
937
|
+
return Object.assign(Object.assign({}, base), { top: 0, left: 0 });
|
|
938
|
+
case 'top-right':
|
|
939
|
+
return Object.assign(Object.assign({}, base), { top: 0, right: 0 });
|
|
940
|
+
case 'bottom-left':
|
|
941
|
+
return Object.assign(Object.assign({}, base), { bottom: 0, left: 0 });
|
|
942
|
+
case 'bottom-right':
|
|
943
|
+
return Object.assign(Object.assign({}, base), { bottom: 0, right: 0 });
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
/**
|
|
947
|
+
* @deprecated Use `AchievementsWidget` for new integrations. This v3
|
|
948
|
+
* compatibility wrapper will be removed in 4.2.
|
|
949
|
+
*/
|
|
950
|
+
const BadgesButton = ({ onClick, position = 'bottom-right', placement = 'fixed', styles = {}, unlockedAchievements, theme = 'modern', }) => {
|
|
951
|
+
React.useEffect(() => {
|
|
952
|
+
warnDeprecation('`BadgesButton` is deprecated. Use `AchievementsWidget` instead. `BadgesButton` will be removed in 4.2.');
|
|
953
|
+
}, []);
|
|
954
|
+
// Get theme configuration for consistent styling
|
|
955
|
+
const themeConfig = getTheme(theme) || builtInThemes.modern;
|
|
956
|
+
const accentColor = themeConfig.notification.accentColor;
|
|
957
|
+
// Different styling for fixed vs inline placement
|
|
958
|
+
const baseStyles = placement === 'inline'
|
|
959
|
+
? Object.assign({
|
|
960
|
+
// Inline mode: looks like a navigation item
|
|
961
|
+
backgroundColor: 'transparent', color: themeConfig.notification.textColor, padding: '12px 16px', border: 'none', borderRadius: '6px', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px', fontSize: '15px', width: '100%', textAlign: 'left', transition: 'background-color 0.2s ease-in-out' }, styles) : Object.assign(Object.assign({
|
|
962
|
+
// Fixed mode: floating button
|
|
963
|
+
backgroundColor: accentColor, color: 'white', padding: '10px 20px', border: 'none', borderRadius: '20px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px', fontSize: '16px', boxShadow: '0 2px 5px rgba(0,0,0,0.2)', transition: 'transform 0.2s ease-in-out' }, getPositionStyles(position)), styles);
|
|
964
|
+
return (React.createElement("button", { onClick: onClick, style: baseStyles, onMouseEnter: (e) => {
|
|
965
|
+
if (placement === 'inline') {
|
|
966
|
+
// Inline mode: subtle background color change
|
|
967
|
+
e.target.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
|
|
968
|
+
}
|
|
969
|
+
else {
|
|
970
|
+
// Fixed mode: scale transformation
|
|
971
|
+
e.target.style.transform = 'scale(1.05)';
|
|
972
|
+
}
|
|
973
|
+
}, onMouseLeave: (e) => {
|
|
974
|
+
if (placement === 'inline') {
|
|
975
|
+
e.target.style.backgroundColor = 'transparent';
|
|
976
|
+
}
|
|
977
|
+
else {
|
|
978
|
+
e.target.style.transform = 'scale(1)';
|
|
979
|
+
}
|
|
980
|
+
}, "data-placement": placement, "data-testid": "badges-button" },
|
|
981
|
+
"\uD83C\uDFC6 Achievements (",
|
|
982
|
+
unlockedAchievements.length,
|
|
983
|
+
")"));
|
|
984
|
+
};
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* @deprecated Use `AchievementsModal`, `AchievementsWidget`, or
|
|
988
|
+
* `AchievementsList` for new integrations. This v3 compatibility wrapper will
|
|
989
|
+
* be removed in 4.2.
|
|
990
|
+
*/
|
|
991
|
+
const BadgesModal = ({ isOpen, onClose, achievements, styles = {}, icons = {}, showAllAchievements = false, showUnlockConditions = false, allAchievements, }) => {
|
|
992
|
+
React.useEffect(() => {
|
|
993
|
+
warnDeprecation('`BadgesModal` is deprecated. Use `AchievementsWidget` or `AchievementsList` instead. `BadgesModal` will be removed in 4.2.');
|
|
994
|
+
}, []);
|
|
995
|
+
const achievementsToDisplay = showAllAchievements && allAchievements
|
|
996
|
+
? allAchievements
|
|
997
|
+
: achievements.map((achievement) => (Object.assign(Object.assign({}, achievement), { isUnlocked: true })));
|
|
998
|
+
return (React.createElement(AchievementsModal, { isOpen: isOpen, onClose: onClose, achievements: achievementsToDisplay, styles: styles, icons: icons, showUnlockConditions: showUnlockConditions }));
|
|
999
|
+
};
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* @deprecated Use `AchievementsWidget` for new integrations. This v3
|
|
1003
|
+
* compatibility wrapper will be removed in 4.2.
|
|
1004
|
+
*/
|
|
1005
|
+
const BadgesButtonWithModal = ({ unlockedAchievements, position = 'bottom-right', placement = 'fixed', showAllAchievements = false, allAchievements, showUnlockConditions = false, icons, theme = 'modern', buttonStyles, modalStyles, }) => {
|
|
1006
|
+
const [isModalOpen, setIsModalOpen] = React.useState(false);
|
|
1007
|
+
return (React.createElement(React.Fragment, null,
|
|
1008
|
+
React.createElement(BadgesButton, { onClick: () => setIsModalOpen(true), unlockedAchievements: unlockedAchievements, position: position, placement: placement, theme: theme, styles: buttonStyles }),
|
|
1009
|
+
React.createElement(BadgesModal, { isOpen: isModalOpen, onClose: () => setIsModalOpen(false), achievements: unlockedAchievements, showAllAchievements: showAllAchievements, allAchievements: allAchievements, showUnlockConditions: showUnlockConditions, icons: icons, styles: modalStyles })));
|
|
1010
|
+
};
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* @deprecated Use the provider `ui.ConfettiComponent` option or the built-in
|
|
1014
|
+
* confetti default. This v3 compatibility wrapper will be removed in 4.2.
|
|
1015
|
+
*/
|
|
1016
|
+
const ConfettiWrapper = ({ show }) => {
|
|
1017
|
+
React.useEffect(() => {
|
|
1018
|
+
warnDeprecation('`ConfettiWrapper` is deprecated. Use the provider `ui.ConfettiComponent` option or built-in confetti defaults instead. `ConfettiWrapper` will be removed in 4.2.');
|
|
1019
|
+
}, []);
|
|
1020
|
+
return React.createElement(BuiltInConfetti, { show: show, particleCount: 200 });
|
|
1021
|
+
};
|
|
1022
|
+
|
|
1023
|
+
const clamp = (value, min, max) => {
|
|
1024
|
+
return Math.min(Math.max(value, min), max);
|
|
1025
|
+
};
|
|
1026
|
+
const LevelProgress = ({ level, currentXP, nextLevelXP, label = 'Level', valueLabel, showValues = true, showPercent = true, theme = 'modern', styles = {}, className, }) => {
|
|
1027
|
+
const themeConfig = getTheme(theme) || builtInThemes.modern;
|
|
1028
|
+
const safeMax = nextLevelXP > 0 ? nextLevelXP : 1;
|
|
1029
|
+
const clampedCurrent = clamp(currentXP, 0, safeMax);
|
|
1030
|
+
const progress = Math.round((clampedCurrent / safeMax) * 100);
|
|
1031
|
+
const computedValueLabel = valueLabel !== null && valueLabel !== void 0 ? valueLabel : `${clampedCurrent} / ${safeMax} XP`;
|
|
1032
|
+
const containerStyles = Object.assign(Object.assign(Object.assign({}, defaultStyles.levelProgress.container), { background: themeConfig.notification.background, color: themeConfig.notification.textColor, borderRadius: themeConfig.notification.borderRadius, boxShadow: themeConfig.notification.boxShadow }), styles === null || styles === void 0 ? void 0 : styles.container);
|
|
1033
|
+
const progressBarStyles = Object.assign(Object.assign(Object.assign({}, defaultStyles.levelProgress.progressBar), { backgroundColor: themeConfig.notification.accentColor, width: `${progress}%` }), styles === null || styles === void 0 ? void 0 : styles.progressBar);
|
|
1034
|
+
return (React.createElement("div", { className: className, style: containerStyles, "data-testid": "level-progress" },
|
|
1035
|
+
React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.levelProgress.header), styles === null || styles === void 0 ? void 0 : styles.header) },
|
|
1036
|
+
React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.levelProgress.levelLabel), styles === null || styles === void 0 ? void 0 : styles.levelLabel) },
|
|
1037
|
+
label,
|
|
1038
|
+
" ",
|
|
1039
|
+
level),
|
|
1040
|
+
showValues && (React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.levelProgress.valueText), styles === null || styles === void 0 ? void 0 : styles.valueText) }, computedValueLabel))),
|
|
1041
|
+
React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.levelProgress.progressTrack), styles === null || styles === void 0 ? void 0 : styles.progressTrack), role: "progressbar", "aria-valuemin": 0, "aria-valuemax": safeMax, "aria-valuenow": clampedCurrent, "aria-label": "Level progress" },
|
|
1042
|
+
React.createElement("div", { style: progressBarStyles })),
|
|
1043
|
+
showPercent && (React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.levelProgress.progressText), styles === null || styles === void 0 ? void 0 : styles.progressText) },
|
|
1044
|
+
progress,
|
|
1045
|
+
"%"))));
|
|
1046
|
+
};
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* A simplified hook for achievement tracking.
|
|
1050
|
+
* Provides the v4 happy path for direct metric updates plus explicit state names.
|
|
1051
|
+
*/
|
|
1052
|
+
const useSimpleAchievements = () => {
|
|
1053
|
+
const { update, reset, getState, exportData, importData } = useAchievements();
|
|
1054
|
+
const achievementState = useAchievementState();
|
|
1055
|
+
const track = (metric, value) => update({ [metric]: value });
|
|
1056
|
+
const increment = (metric, amount = 1) => {
|
|
1057
|
+
const currentState = getState();
|
|
1058
|
+
const currentMetricArray = currentState.metrics[metric] || [0];
|
|
1059
|
+
const currentValue = Array.isArray(currentMetricArray)
|
|
1060
|
+
? currentMetricArray[0]
|
|
1061
|
+
: currentMetricArray;
|
|
1062
|
+
const newValue = (typeof currentValue === 'number' ? currentValue : 0) + amount;
|
|
1063
|
+
update({ [metric]: newValue });
|
|
1064
|
+
};
|
|
1065
|
+
const trackMultiple = (metrics) => update(metrics);
|
|
1066
|
+
return {
|
|
1067
|
+
track,
|
|
1068
|
+
increment,
|
|
1069
|
+
trackMultiple,
|
|
1070
|
+
unlockedIds: achievementState.unlockedIds,
|
|
1071
|
+
unlockedAchievements: achievementState.unlockedAchievements,
|
|
1072
|
+
allAchievements: achievementState.allAchievements,
|
|
1073
|
+
unlockedCount: achievementState.unlockedCount,
|
|
1074
|
+
totalCount: achievementState.totalCount,
|
|
1075
|
+
metrics: achievementState.metrics,
|
|
1076
|
+
reset,
|
|
1077
|
+
getState,
|
|
1078
|
+
exportData,
|
|
1079
|
+
importData,
|
|
1080
|
+
getAllAchievements: () => achievementState.allAchievements,
|
|
1081
|
+
/**
|
|
1082
|
+
* @deprecated Use `unlockedIds` instead. This alias will be removed in 4.2.
|
|
1083
|
+
*/
|
|
1084
|
+
unlocked: achievementState.unlockedIds,
|
|
1085
|
+
/**
|
|
1086
|
+
* @deprecated Use `allAchievements` instead. This alias will be removed in 4.2.
|
|
1087
|
+
*/
|
|
1088
|
+
all: achievementState.allAchievements,
|
|
1089
|
+
};
|
|
1090
|
+
};
|
|
1091
|
+
|
|
1092
|
+
/**
|
|
1093
|
+
* Built-in modal component
|
|
1094
|
+
* Modern, theme-aware achievement modal with smooth animations
|
|
1095
|
+
*/
|
|
1096
|
+
const BuiltInModal = ({ isOpen, onClose, achievements, icons = {}, theme = 'modern', }) => {
|
|
1097
|
+
// Merge custom icons with defaults
|
|
1098
|
+
const mergedIcons = Object.assign(Object.assign({}, defaultAchievementIcons), icons);
|
|
1099
|
+
// Get theme configuration
|
|
1100
|
+
const themeConfig = getTheme(theme) || builtInThemes.modern;
|
|
1101
|
+
const { modal: themeStyles } = themeConfig;
|
|
1102
|
+
React.useEffect(() => {
|
|
1103
|
+
if (isOpen) {
|
|
1104
|
+
// Lock body scroll when modal is open
|
|
1105
|
+
document.body.style.overflow = 'hidden';
|
|
1106
|
+
}
|
|
1107
|
+
else {
|
|
1108
|
+
// Restore body scroll
|
|
1109
|
+
document.body.style.overflow = '';
|
|
1110
|
+
}
|
|
1111
|
+
return () => {
|
|
1112
|
+
document.body.style.overflow = '';
|
|
1113
|
+
};
|
|
1114
|
+
}, [isOpen]);
|
|
1115
|
+
if (!isOpen)
|
|
1116
|
+
return null;
|
|
1117
|
+
const overlayStyles = {
|
|
1118
|
+
position: 'fixed',
|
|
1119
|
+
top: 0,
|
|
1120
|
+
left: 0,
|
|
1121
|
+
right: 0,
|
|
1122
|
+
bottom: 0,
|
|
1123
|
+
backgroundColor: themeStyles.overlayColor,
|
|
1124
|
+
display: 'flex',
|
|
1125
|
+
alignItems: 'center',
|
|
1126
|
+
justifyContent: 'center',
|
|
1127
|
+
zIndex: 10000,
|
|
1128
|
+
animation: 'fadeIn 0.3s ease-in-out',
|
|
1129
|
+
};
|
|
1130
|
+
const modalStyles = {
|
|
1131
|
+
background: themeStyles.background,
|
|
1132
|
+
borderRadius: themeStyles.borderRadius,
|
|
1133
|
+
padding: '32px',
|
|
1134
|
+
maxWidth: '600px',
|
|
1135
|
+
width: '90%',
|
|
1136
|
+
maxHeight: '80vh',
|
|
1137
|
+
overflow: 'auto',
|
|
1138
|
+
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.5)',
|
|
1139
|
+
animation: 'scaleIn 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
|
1140
|
+
position: 'relative',
|
|
1141
|
+
};
|
|
1142
|
+
const headerStyles = {
|
|
1143
|
+
display: 'flex',
|
|
1144
|
+
justifyContent: 'space-between',
|
|
1145
|
+
alignItems: 'center',
|
|
1146
|
+
marginBottom: '24px',
|
|
1147
|
+
};
|
|
1148
|
+
const titleStyles = {
|
|
1149
|
+
margin: 0,
|
|
1150
|
+
color: themeStyles.textColor,
|
|
1151
|
+
fontSize: themeStyles.headerFontSize || '28px',
|
|
1152
|
+
fontWeight: 'bold',
|
|
1153
|
+
};
|
|
1154
|
+
const closeButtonStyles = {
|
|
1155
|
+
background: 'none',
|
|
1156
|
+
border: 'none',
|
|
1157
|
+
fontSize: '32px',
|
|
1158
|
+
cursor: 'pointer',
|
|
1159
|
+
color: themeStyles.textColor,
|
|
1160
|
+
opacity: 0.6,
|
|
1161
|
+
transition: 'opacity 0.2s',
|
|
1162
|
+
padding: 0,
|
|
1163
|
+
lineHeight: 1,
|
|
1164
|
+
};
|
|
1165
|
+
const isBadgeLayout = themeStyles.achievementLayout === 'badge';
|
|
1166
|
+
const listStyles = isBadgeLayout
|
|
1167
|
+
? {
|
|
1168
|
+
display: 'grid',
|
|
1169
|
+
gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))',
|
|
1170
|
+
gap: '16px',
|
|
1171
|
+
}
|
|
1172
|
+
: {
|
|
1173
|
+
display: 'flex',
|
|
1174
|
+
flexDirection: 'column',
|
|
1175
|
+
gap: '12px',
|
|
1176
|
+
};
|
|
1177
|
+
const getAchievementItemStyles = (isUnlocked) => {
|
|
1178
|
+
const baseStyles = {
|
|
1179
|
+
borderRadius: themeStyles.achievementCardBorderRadius || '12px',
|
|
1180
|
+
backgroundColor: isUnlocked
|
|
1181
|
+
? `${themeStyles.accentColor}1A` // 10% opacity
|
|
1182
|
+
: 'rgba(255, 255, 255, 0.05)',
|
|
1183
|
+
border: `2px solid ${isUnlocked ? themeStyles.accentColor : 'rgba(255, 255, 255, 0.1)'}`,
|
|
1184
|
+
opacity: isUnlocked ? 1 : 0.5,
|
|
1185
|
+
transition: 'all 0.2s',
|
|
1186
|
+
};
|
|
1187
|
+
if (isBadgeLayout) {
|
|
1188
|
+
// Badge layout: vertical, centered, square-ish
|
|
1189
|
+
return Object.assign(Object.assign({}, baseStyles), { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', textAlign: 'center', padding: '20px 12px', aspectRatio: '1 / 1.1', minHeight: '160px' });
|
|
1190
|
+
}
|
|
1191
|
+
else {
|
|
1192
|
+
// Horizontal layout (default)
|
|
1193
|
+
return Object.assign(Object.assign({}, baseStyles), { display: 'flex', gap: '16px', padding: '16px' });
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
const getIconContainerStyles = (isUnlocked) => {
|
|
1197
|
+
if (isBadgeLayout) {
|
|
1198
|
+
return {
|
|
1199
|
+
fontSize: '48px',
|
|
1200
|
+
lineHeight: 1,
|
|
1201
|
+
marginBottom: '8px',
|
|
1202
|
+
opacity: isUnlocked ? 1 : 0.3,
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
return {
|
|
1206
|
+
fontSize: '40px',
|
|
1207
|
+
flexShrink: 0,
|
|
1208
|
+
lineHeight: 1,
|
|
1209
|
+
opacity: isUnlocked ? 1 : 0.3,
|
|
1210
|
+
};
|
|
1211
|
+
};
|
|
1212
|
+
const contentStyles = isBadgeLayout
|
|
1213
|
+
? {
|
|
1214
|
+
width: '100%',
|
|
1215
|
+
}
|
|
1216
|
+
: {
|
|
1217
|
+
flex: 1,
|
|
1218
|
+
minWidth: 0,
|
|
1219
|
+
};
|
|
1220
|
+
const achievementTitleStyles = isBadgeLayout
|
|
1221
|
+
? {
|
|
1222
|
+
margin: '0 0 4px 0',
|
|
1223
|
+
color: themeStyles.textColor,
|
|
1224
|
+
fontSize: '14px',
|
|
1225
|
+
fontWeight: 'bold',
|
|
1226
|
+
lineHeight: '1.3',
|
|
1227
|
+
}
|
|
1228
|
+
: {
|
|
1229
|
+
margin: '0 0 8px 0',
|
|
1230
|
+
color: themeStyles.textColor,
|
|
1231
|
+
fontSize: '18px',
|
|
1232
|
+
fontWeight: 'bold',
|
|
1233
|
+
overflow: 'hidden',
|
|
1234
|
+
textOverflow: 'ellipsis',
|
|
1235
|
+
whiteSpace: 'nowrap',
|
|
1236
|
+
};
|
|
1237
|
+
const achievementDescriptionStyles = isBadgeLayout
|
|
1238
|
+
? {
|
|
1239
|
+
margin: 0,
|
|
1240
|
+
color: themeStyles.textColor,
|
|
1241
|
+
opacity: 0.7,
|
|
1242
|
+
fontSize: '11px',
|
|
1243
|
+
lineHeight: '1.3',
|
|
1244
|
+
}
|
|
1245
|
+
: {
|
|
1246
|
+
margin: 0,
|
|
1247
|
+
color: themeStyles.textColor,
|
|
1248
|
+
opacity: 0.8,
|
|
1249
|
+
fontSize: '14px',
|
|
1250
|
+
};
|
|
1251
|
+
const getLockIconStyles = () => {
|
|
1252
|
+
if (isBadgeLayout) {
|
|
1253
|
+
return {
|
|
1254
|
+
position: 'absolute',
|
|
1255
|
+
top: '8px',
|
|
1256
|
+
right: '8px',
|
|
1257
|
+
fontSize: '18px',
|
|
1258
|
+
opacity: 0.6,
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
return {
|
|
1262
|
+
fontSize: '24px',
|
|
1263
|
+
flexShrink: 0,
|
|
1264
|
+
opacity: 0.5,
|
|
1265
|
+
};
|
|
1266
|
+
};
|
|
1267
|
+
return (React.createElement(React.Fragment, null,
|
|
1268
|
+
React.createElement("style", null, `
|
|
1269
|
+
@keyframes fadeIn {
|
|
1270
|
+
from { opacity: 0; }
|
|
1271
|
+
to { opacity: 1; }
|
|
1272
|
+
}
|
|
1273
|
+
@keyframes scaleIn {
|
|
1274
|
+
from {
|
|
1275
|
+
transform: scale(0.9);
|
|
1276
|
+
opacity: 0;
|
|
1277
|
+
}
|
|
1278
|
+
to {
|
|
1279
|
+
transform: scale(1);
|
|
1280
|
+
opacity: 1;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
`),
|
|
1284
|
+
React.createElement("div", { style: overlayStyles, onClick: onClose, "data-testid": "built-in-modal-overlay" },
|
|
1285
|
+
React.createElement("div", { style: modalStyles, onClick: (e) => e.stopPropagation(), "data-testid": "built-in-modal" },
|
|
1286
|
+
React.createElement("div", { style: headerStyles },
|
|
1287
|
+
React.createElement("h2", { style: titleStyles }, "\uD83C\uDFC6 Achievements"),
|
|
1288
|
+
React.createElement("button", { onClick: onClose, style: closeButtonStyles, onMouseEnter: (e) => (e.currentTarget.style.opacity = '1'), onMouseLeave: (e) => (e.currentTarget.style.opacity = '0.6'), "aria-label": "Close modal" }, "\u00D7")),
|
|
1289
|
+
React.createElement("div", { style: listStyles }, achievements.length === 0 ? (React.createElement("div", { style: { textAlign: 'center', padding: '40px 20px', color: themeStyles.textColor, opacity: 0.6 } }, "No achievements yet. Start exploring to unlock them!")) : (achievements.map((achievement) => {
|
|
1290
|
+
// If achievementIconKey exists but not in mergedIcons, use it directly (might be an emoji)
|
|
1291
|
+
// Otherwise, look up in mergedIcons or fall back to default
|
|
1292
|
+
const icon = (achievement.achievementIconKey &&
|
|
1293
|
+
mergedIcons[achievement.achievementIconKey]) ||
|
|
1294
|
+
achievement.achievementIconKey ||
|
|
1295
|
+
mergedIcons.default ||
|
|
1296
|
+
'⭐';
|
|
1297
|
+
return (React.createElement("div", { key: achievement.achievementId, style: Object.assign(Object.assign({}, getAchievementItemStyles(achievement.isUnlocked)), { position: isBadgeLayout ? 'relative' : 'static' }) },
|
|
1298
|
+
React.createElement("div", { style: getIconContainerStyles(achievement.isUnlocked) }, icon),
|
|
1299
|
+
React.createElement("div", { style: contentStyles },
|
|
1300
|
+
React.createElement("h3", { style: achievementTitleStyles }, achievement.achievementTitle),
|
|
1301
|
+
React.createElement("p", { style: achievementDescriptionStyles }, achievement.achievementDescription)),
|
|
1302
|
+
!achievement.isUnlocked && (React.createElement("div", { style: getLockIconStyles() }, "\uD83D\uDD12"))));
|
|
1303
|
+
})))))));
|
|
1304
|
+
};
|
|
1305
|
+
|
|
1306
|
+
Object.defineProperty(exports, 'AchievementBuilder', {
|
|
1307
|
+
enumerable: true,
|
|
1308
|
+
get: function () { return achievementsEngine.AchievementBuilder; }
|
|
1309
|
+
});
|
|
1310
|
+
Object.defineProperty(exports, 'AchievementEngine', {
|
|
1311
|
+
enumerable: true,
|
|
1312
|
+
get: function () { return achievementsEngine.AchievementEngine; }
|
|
1313
|
+
});
|
|
1314
|
+
Object.defineProperty(exports, 'AchievementError', {
|
|
1315
|
+
enumerable: true,
|
|
1316
|
+
get: function () { return achievementsEngine.AchievementError; }
|
|
1317
|
+
});
|
|
1318
|
+
Object.defineProperty(exports, 'AsyncStorageAdapter', {
|
|
1319
|
+
enumerable: true,
|
|
1320
|
+
get: function () { return achievementsEngine.AsyncStorageAdapter; }
|
|
1321
|
+
});
|
|
1322
|
+
Object.defineProperty(exports, 'ConfigurationError', {
|
|
1323
|
+
enumerable: true,
|
|
1324
|
+
get: function () { return achievementsEngine.ConfigurationError; }
|
|
1325
|
+
});
|
|
1326
|
+
Object.defineProperty(exports, 'ImportValidationError', {
|
|
1327
|
+
enumerable: true,
|
|
1328
|
+
get: function () { return achievementsEngine.ImportValidationError; }
|
|
1329
|
+
});
|
|
1330
|
+
Object.defineProperty(exports, 'IndexedDBStorage', {
|
|
1331
|
+
enumerable: true,
|
|
1332
|
+
get: function () { return achievementsEngine.IndexedDBStorage; }
|
|
1333
|
+
});
|
|
1334
|
+
Object.defineProperty(exports, 'LocalStorage', {
|
|
1335
|
+
enumerable: true,
|
|
1336
|
+
get: function () { return achievementsEngine.LocalStorage; }
|
|
1337
|
+
});
|
|
1338
|
+
Object.defineProperty(exports, 'MemoryStorage', {
|
|
1339
|
+
enumerable: true,
|
|
1340
|
+
get: function () { return achievementsEngine.MemoryStorage; }
|
|
1341
|
+
});
|
|
1342
|
+
Object.defineProperty(exports, 'OfflineQueueStorage', {
|
|
1343
|
+
enumerable: true,
|
|
1344
|
+
get: function () { return achievementsEngine.OfflineQueueStorage; }
|
|
1345
|
+
});
|
|
1346
|
+
Object.defineProperty(exports, 'RestApiStorage', {
|
|
1347
|
+
enumerable: true,
|
|
1348
|
+
get: function () { return achievementsEngine.RestApiStorage; }
|
|
1349
|
+
});
|
|
1350
|
+
Object.defineProperty(exports, 'StorageError', {
|
|
1351
|
+
enumerable: true,
|
|
1352
|
+
get: function () { return achievementsEngine.StorageError; }
|
|
1353
|
+
});
|
|
1354
|
+
Object.defineProperty(exports, 'StorageQuotaError', {
|
|
1355
|
+
enumerable: true,
|
|
1356
|
+
get: function () { return achievementsEngine.StorageQuotaError; }
|
|
1357
|
+
});
|
|
1358
|
+
Object.defineProperty(exports, 'StorageType', {
|
|
1359
|
+
enumerable: true,
|
|
1360
|
+
get: function () { return achievementsEngine.StorageType; }
|
|
1361
|
+
});
|
|
1362
|
+
Object.defineProperty(exports, 'SyncError', {
|
|
1363
|
+
enumerable: true,
|
|
1364
|
+
get: function () { return achievementsEngine.SyncError; }
|
|
1365
|
+
});
|
|
1366
|
+
Object.defineProperty(exports, 'createConfigHash', {
|
|
1367
|
+
enumerable: true,
|
|
1368
|
+
get: function () { return achievementsEngine.createConfigHash; }
|
|
1369
|
+
});
|
|
1370
|
+
Object.defineProperty(exports, 'exportAchievementData', {
|
|
1371
|
+
enumerable: true,
|
|
1372
|
+
get: function () { return achievementsEngine.exportAchievementData; }
|
|
1373
|
+
});
|
|
1374
|
+
Object.defineProperty(exports, 'importAchievementData', {
|
|
1375
|
+
enumerable: true,
|
|
1376
|
+
get: function () { return achievementsEngine.importAchievementData; }
|
|
1377
|
+
});
|
|
1378
|
+
Object.defineProperty(exports, 'isAchievementError', {
|
|
1379
|
+
enumerable: true,
|
|
1380
|
+
get: function () { return achievementsEngine.isAchievementError; }
|
|
1381
|
+
});
|
|
1382
|
+
Object.defineProperty(exports, 'isRecoverableError', {
|
|
1383
|
+
enumerable: true,
|
|
1384
|
+
get: function () { return achievementsEngine.isRecoverableError; }
|
|
1385
|
+
});
|
|
1386
|
+
Object.defineProperty(exports, 'isSimpleConfig', {
|
|
1387
|
+
enumerable: true,
|
|
1388
|
+
get: function () { return achievementsEngine.isSimpleConfig; }
|
|
1389
|
+
});
|
|
1390
|
+
Object.defineProperty(exports, 'normalizeAchievements', {
|
|
1391
|
+
enumerable: true,
|
|
1392
|
+
get: function () { return achievementsEngine.normalizeAchievements; }
|
|
1393
|
+
});
|
|
1394
|
+
exports.AchievementContext = AchievementContext;
|
|
1395
|
+
exports.AchievementProvider = AchievementProvider;
|
|
1396
|
+
exports.AchievementsList = AchievementsList;
|
|
1397
|
+
exports.AchievementsModal = AchievementsModal;
|
|
1398
|
+
exports.AchievementsWidget = AchievementsWidget;
|
|
1399
|
+
exports.BadgesButton = BadgesButton;
|
|
1400
|
+
exports.BadgesButtonWithModal = BadgesButtonWithModal;
|
|
1401
|
+
exports.BadgesModal = BadgesModal;
|
|
1402
|
+
exports.BuiltInConfetti = BuiltInConfetti;
|
|
1403
|
+
exports.BuiltInModal = BuiltInModal;
|
|
1404
|
+
exports.BuiltInNotification = BuiltInNotification;
|
|
1405
|
+
exports.ConfettiWrapper = ConfettiWrapper;
|
|
1406
|
+
exports.HeadlessAchievementProvider = AchievementProvider$1;
|
|
1407
|
+
exports.LevelProgress = LevelProgress;
|
|
1408
|
+
exports.defaultAchievementIcons = defaultAchievementIcons;
|
|
1409
|
+
exports.defaultStyles = defaultStyles;
|
|
1410
|
+
exports.isAsyncStorage = isAsyncStorage;
|
|
1411
|
+
exports.useAchievementEngine = useAchievementEngine;
|
|
1412
|
+
exports.useAchievementState = useAchievementState;
|
|
1413
|
+
exports.useAchievements = useAchievements;
|
|
1414
|
+
exports.useSimpleAchievements = useSimpleAchievements;
|
|
1415
|
+
exports.useWindowSize = useWindowSize;
|
|
1416
|
+
//# sourceMappingURL=web.cjs.map
|