react-achievements 1.2.0 → 1.3.1
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 +91 -94
- package/dist/components/AchievementModal.d.ts +3 -1
- package/dist/components/BadgesButton.d.ts +2 -0
- package/dist/components/BadgesModal.d.ts +3 -1
- package/dist/components/ConfettiWrapper.d.ts +0 -2
- package/dist/context/AchievementContext.d.ts +3 -1
- package/dist/defaultStyles.d.ts +28 -0
- package/dist/index.cjs.js +191 -109
- package/dist/index.esm.js +191 -109
- package/dist/types.d.ts +6 -7
- package/dist/utils/EventEmitter.d.ts +6 -0
- package/package.json +1 -1
- package/src/components/AchievementModal.tsx +13 -34
- package/src/components/BadgesButton.tsx +5 -11
- package/src/components/BadgesModal.tsx +15 -39
- package/src/components/ConfettiWrapper.tsx +2 -10
- package/src/context/AchievementContext.tsx +72 -35
- package/src/defaultStyles.ts +138 -0
- package/src/types.ts +8 -10
package/dist/index.cjs.js
CHANGED
|
@@ -2,91 +2,149 @@
|
|
|
2
2
|
|
|
3
3
|
var React = require('react');
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
const defaultStyles = {
|
|
6
|
+
achievementModal: {
|
|
7
|
+
overlay: {
|
|
8
|
+
position: 'fixed',
|
|
9
|
+
top: 0,
|
|
10
|
+
left: 0,
|
|
11
|
+
right: 0,
|
|
12
|
+
bottom: 0,
|
|
13
|
+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
14
|
+
display: 'flex',
|
|
15
|
+
alignItems: 'center',
|
|
16
|
+
justifyContent: 'center',
|
|
17
|
+
},
|
|
18
|
+
content: {
|
|
19
|
+
backgroundColor: '#ffffff',
|
|
20
|
+
borderRadius: '8px',
|
|
21
|
+
padding: '20px',
|
|
22
|
+
maxWidth: '400px',
|
|
23
|
+
width: '100%',
|
|
24
|
+
},
|
|
25
|
+
title: {
|
|
26
|
+
fontSize: '24px',
|
|
27
|
+
fontWeight: 'bold',
|
|
28
|
+
marginBottom: '10px',
|
|
29
|
+
},
|
|
30
|
+
icon: {
|
|
31
|
+
width: '50px',
|
|
32
|
+
height: '50px',
|
|
33
|
+
marginBottom: '10px',
|
|
34
|
+
},
|
|
35
|
+
description: {
|
|
36
|
+
fontSize: '16px',
|
|
37
|
+
marginBottom: '20px',
|
|
38
|
+
},
|
|
39
|
+
button: {
|
|
40
|
+
backgroundColor: '#007bff',
|
|
41
|
+
color: '#ffffff',
|
|
42
|
+
padding: '10px 20px',
|
|
43
|
+
borderRadius: '4px',
|
|
44
|
+
border: 'none',
|
|
45
|
+
cursor: 'pointer',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
badgesModal: {
|
|
49
|
+
overlay: {
|
|
50
|
+
position: 'fixed',
|
|
51
|
+
top: 0,
|
|
52
|
+
left: 0,
|
|
53
|
+
right: 0,
|
|
54
|
+
bottom: 0,
|
|
55
|
+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
56
|
+
display: 'flex',
|
|
57
|
+
alignItems: 'center',
|
|
58
|
+
justifyContent: 'center',
|
|
59
|
+
},
|
|
60
|
+
content: {
|
|
61
|
+
backgroundColor: '#ffffff',
|
|
62
|
+
borderRadius: '8px',
|
|
63
|
+
padding: '20px',
|
|
64
|
+
maxWidth: '600px',
|
|
65
|
+
width: '100%',
|
|
66
|
+
maxHeight: '80vh',
|
|
67
|
+
overflowY: 'auto',
|
|
68
|
+
},
|
|
69
|
+
title: {
|
|
70
|
+
fontSize: '24px',
|
|
71
|
+
fontWeight: 'bold',
|
|
72
|
+
marginBottom: '20px',
|
|
73
|
+
},
|
|
74
|
+
badgeContainer: {
|
|
75
|
+
display: 'flex',
|
|
76
|
+
flexWrap: 'wrap',
|
|
77
|
+
justifyContent: 'center',
|
|
78
|
+
},
|
|
79
|
+
badge: {
|
|
80
|
+
display: 'flex',
|
|
81
|
+
flexDirection: 'column',
|
|
82
|
+
alignItems: 'center',
|
|
83
|
+
margin: '10px',
|
|
84
|
+
},
|
|
85
|
+
badgeIcon: {
|
|
86
|
+
width: '50px',
|
|
87
|
+
height: '50px',
|
|
88
|
+
marginBottom: '5px',
|
|
89
|
+
},
|
|
90
|
+
badgeTitle: {
|
|
91
|
+
fontSize: '14px',
|
|
92
|
+
textAlign: 'center',
|
|
93
|
+
},
|
|
94
|
+
button: {
|
|
95
|
+
backgroundColor: '#007bff',
|
|
96
|
+
color: '#ffffff',
|
|
97
|
+
padding: '10px 20px',
|
|
98
|
+
borderRadius: '4px',
|
|
99
|
+
border: 'none',
|
|
100
|
+
cursor: 'pointer',
|
|
101
|
+
marginTop: '20px',
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
badgesButton: {
|
|
9
105
|
position: 'fixed',
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
boxShadow: '0 0 10px rgba(0,0,0,0.1)',
|
|
106
|
+
padding: '10px 20px',
|
|
107
|
+
backgroundColor: '#007bff',
|
|
108
|
+
color: '#ffffff',
|
|
109
|
+
border: 'none',
|
|
110
|
+
borderRadius: '4px',
|
|
111
|
+
cursor: 'pointer',
|
|
17
112
|
zIndex: 1000,
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
React.createElement("h2", { id: "achievement-title" }, "Achievement Unlocked!"),
|
|
32
|
-
React.createElement("img", { src: achievement.icon, alt: achievement.title, style: { width: '50px', height: '50px' } }),
|
|
33
|
-
React.createElement("h3", null, achievement.title),
|
|
34
|
-
React.createElement("p", null, achievement.description),
|
|
35
|
-
React.createElement("button", { onClick: onClose }, "Okay"))));
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const AchievementModal = ({ isOpen, achievement, onClose, styles }) => {
|
|
117
|
+
if (!isOpen || !achievement)
|
|
118
|
+
return null;
|
|
119
|
+
return (React.createElement("div", { style: styles.overlay },
|
|
120
|
+
React.createElement("div", { style: styles.content },
|
|
121
|
+
React.createElement("h2", { style: styles.title }, "Achievement Unlocked!"),
|
|
122
|
+
React.createElement("img", { src: achievement.icon, alt: achievement.title, style: styles.icon }),
|
|
123
|
+
React.createElement("h3", { style: styles.title }, achievement.title),
|
|
124
|
+
React.createElement("p", { style: styles.description }, achievement.description),
|
|
125
|
+
React.createElement("button", { onClick: onClose, style: styles.button }, "Okay"))));
|
|
36
126
|
};
|
|
37
127
|
var AchievementModal$1 = React.memo(AchievementModal);
|
|
38
128
|
|
|
39
|
-
const BadgesModal = ({
|
|
40
|
-
if (!
|
|
129
|
+
const BadgesModal = ({ isOpen, achievements, onClose, styles }) => {
|
|
130
|
+
if (!isOpen)
|
|
41
131
|
return null;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
borderRadius: '8px',
|
|
50
|
-
boxShadow: '0 0 10px rgba(0,0,0,0.1)',
|
|
51
|
-
zIndex: 1000,
|
|
52
|
-
maxWidth: '80%',
|
|
53
|
-
maxHeight: '80%',
|
|
54
|
-
overflow: 'auto',
|
|
55
|
-
};
|
|
56
|
-
const overlayStyle = {
|
|
57
|
-
position: 'fixed',
|
|
58
|
-
top: 0,
|
|
59
|
-
left: 0,
|
|
60
|
-
right: 0,
|
|
61
|
-
bottom: 0,
|
|
62
|
-
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
63
|
-
zIndex: 999,
|
|
64
|
-
};
|
|
65
|
-
return (React.createElement(React.Fragment, null,
|
|
66
|
-
React.createElement("div", { style: overlayStyle, onClick: onClose }),
|
|
67
|
-
React.createElement("div", { style: modalStyle, role: "dialog", "aria-modal": "true", "aria-labelledby": "badges-title" },
|
|
68
|
-
React.createElement("h2", { id: "badges-title" }, "Your Achievements"),
|
|
69
|
-
React.createElement("div", { style: { display: 'flex', flexWrap: 'wrap', justifyContent: 'center' } }, achievements.map(achievement => (React.createElement("div", { key: achievement.id, style: { margin: '10px', textAlign: 'center' } },
|
|
70
|
-
React.createElement("img", { src: achievement.icon, alt: achievement.title, style: { width: '50px', height: '50px' } }),
|
|
71
|
-
React.createElement("h4", null, achievement.title))))),
|
|
72
|
-
React.createElement("button", { onClick: onClose, style: { marginTop: '20px' } }, "Close"))));
|
|
132
|
+
return (React.createElement("div", { style: styles.overlay },
|
|
133
|
+
React.createElement("div", { style: styles.content },
|
|
134
|
+
React.createElement("h2", { style: styles.title }, "Your Achievements"),
|
|
135
|
+
React.createElement("div", { style: styles.badgeContainer }, achievements.map((achievement) => (React.createElement("div", { key: achievement.id, style: styles.badge },
|
|
136
|
+
React.createElement("img", { src: achievement.icon, alt: achievement.title, style: styles.badgeIcon }),
|
|
137
|
+
React.createElement("span", { style: styles.badgeTitle }, achievement.title))))),
|
|
138
|
+
React.createElement("button", { onClick: onClose, style: styles.button }, "Close"))));
|
|
73
139
|
};
|
|
74
140
|
var BadgesModal$1 = React.memo(BadgesModal);
|
|
75
141
|
|
|
76
|
-
const BadgesButton = ({ onClick, position }) => {
|
|
77
|
-
const
|
|
78
|
-
position: 'fixed',
|
|
142
|
+
const BadgesButton = ({ onClick, position, styles }) => {
|
|
143
|
+
const positionStyle = {
|
|
79
144
|
[position.split('-')[0]]: '20px',
|
|
80
145
|
[position.split('-')[1]]: '20px',
|
|
81
|
-
padding: '10px 20px',
|
|
82
|
-
backgroundColor: '#007bff',
|
|
83
|
-
color: '#fff',
|
|
84
|
-
border: 'none',
|
|
85
|
-
borderRadius: '5px',
|
|
86
|
-
cursor: 'pointer',
|
|
87
|
-
zIndex: 998,
|
|
88
146
|
};
|
|
89
|
-
return (React.createElement("button", {
|
|
147
|
+
return (React.createElement("button", { onClick: onClick, style: Object.assign(Object.assign({}, styles), positionStyle) }, "View Achievements"));
|
|
90
148
|
};
|
|
91
149
|
var BadgesButton$1 = React.memo(BadgesButton);
|
|
92
150
|
|
|
@@ -176,35 +234,26 @@ var useWindowSize = function (initialWidth, initialHeight) {
|
|
|
176
234
|
return state;
|
|
177
235
|
};
|
|
178
236
|
|
|
179
|
-
const ConfettiWrapper = ({ show
|
|
237
|
+
const ConfettiWrapper = ({ show }) => {
|
|
180
238
|
const { width, height } = useWindowSize();
|
|
181
239
|
if (!show)
|
|
182
240
|
return null;
|
|
183
|
-
return
|
|
241
|
+
return React.createElement(Confetti, { width: width, height: height, recycle: false });
|
|
184
242
|
};
|
|
185
243
|
|
|
186
244
|
const AchievementContext = React.createContext(undefined);
|
|
187
|
-
const AchievementProvider = ({ children, config, initialState = {}, storageKey = 'react-achievements', badgesButtonPosition = 'top-right' }) => {
|
|
188
|
-
const
|
|
189
|
-
return Object.keys(config).reduce((acc, key) => {
|
|
190
|
-
if (key in state) {
|
|
191
|
-
acc[key] = state[key];
|
|
192
|
-
}
|
|
193
|
-
else {
|
|
194
|
-
acc[key] = [];
|
|
195
|
-
}
|
|
196
|
-
return acc;
|
|
197
|
-
}, {});
|
|
198
|
-
};
|
|
245
|
+
const AchievementProvider = ({ children, config, initialState = {}, storageKey = 'react-achievements', badgesButtonPosition = 'top-right', styles = {}, }) => {
|
|
246
|
+
const mergedStyles = React.useMemo(() => mergeDeep(defaultStyles, styles), [styles]);
|
|
199
247
|
const [metrics, setMetrics] = React.useState(() => {
|
|
200
248
|
const savedMetrics = localStorage.getItem(`${storageKey}-metrics`);
|
|
201
|
-
|
|
202
|
-
return JSON.parse(savedMetrics);
|
|
203
|
-
}
|
|
204
|
-
return extractMetrics(initialState);
|
|
249
|
+
return savedMetrics ? JSON.parse(savedMetrics) : initialState;
|
|
205
250
|
});
|
|
206
|
-
const [
|
|
207
|
-
const saved = localStorage.getItem(`${storageKey}-achievements`);
|
|
251
|
+
const [unlockedAchievements, setUnlockedAchievements] = React.useState(() => {
|
|
252
|
+
const saved = localStorage.getItem(`${storageKey}-unlocked-achievements`);
|
|
253
|
+
return saved ? JSON.parse(saved) : [];
|
|
254
|
+
});
|
|
255
|
+
const [newlyUnlockedAchievements, setNewlyUnlockedAchievements] = React.useState(() => {
|
|
256
|
+
const saved = localStorage.getItem(`${storageKey}-newly-unlocked-achievements`);
|
|
208
257
|
return saved ? JSON.parse(saved) : [];
|
|
209
258
|
});
|
|
210
259
|
const [achievementQueue, setAchievementQueue] = React.useState([]);
|
|
@@ -216,31 +265,43 @@ const AchievementProvider = ({ children, config, initialState = {}, storageKey =
|
|
|
216
265
|
Object.entries(config).forEach(([metricKey, conditions]) => {
|
|
217
266
|
const metricValue = metrics[metricKey];
|
|
218
267
|
conditions.forEach(condition => {
|
|
219
|
-
if (condition.check(metricValue) && !
|
|
268
|
+
if (condition.check(metricValue) && !unlockedAchievements.includes(condition.data.id)) {
|
|
220
269
|
newAchievements.push(condition.data);
|
|
221
270
|
}
|
|
222
271
|
});
|
|
223
272
|
});
|
|
224
273
|
if (newAchievements.length > 0) {
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
274
|
+
const newlyUnlockedIds = newAchievements.map(a => a.id);
|
|
275
|
+
setUnlockedAchievements(prev => {
|
|
276
|
+
const updated = [...prev, ...newlyUnlockedIds];
|
|
277
|
+
localStorage.setItem(`${storageKey}-unlocked-achievements`, JSON.stringify(updated));
|
|
278
|
+
return updated;
|
|
279
|
+
});
|
|
280
|
+
setNewlyUnlockedAchievements(prev => {
|
|
281
|
+
const updated = [...prev, ...newlyUnlockedIds];
|
|
282
|
+
localStorage.setItem(`${storageKey}-newly-unlocked-achievements`, JSON.stringify(updated));
|
|
283
|
+
return updated;
|
|
284
|
+
});
|
|
228
285
|
setAchievementQueue(prevQueue => [...prevQueue, ...newAchievements]);
|
|
229
286
|
setShowConfetti(true);
|
|
230
287
|
}
|
|
231
|
-
}, [config, metrics,
|
|
288
|
+
}, [config, metrics, unlockedAchievements, storageKey]);
|
|
232
289
|
React.useEffect(() => {
|
|
233
290
|
checkAchievements();
|
|
234
291
|
}, [metrics, checkAchievements]);
|
|
235
292
|
React.useEffect(() => {
|
|
236
293
|
if (achievementQueue.length > 0 && !currentAchievement) {
|
|
237
|
-
|
|
294
|
+
const nextAchievement = achievementQueue[0];
|
|
295
|
+
setCurrentAchievement(nextAchievement);
|
|
238
296
|
setAchievementQueue(prevQueue => prevQueue.slice(1));
|
|
297
|
+
setNewlyUnlockedAchievements(prev => {
|
|
298
|
+
const updated = prev.filter(id => id !== nextAchievement.id);
|
|
299
|
+
localStorage.setItem(`${storageKey}-newly-unlocked-achievements`, JSON.stringify(updated));
|
|
300
|
+
return updated;
|
|
301
|
+
});
|
|
239
302
|
}
|
|
240
|
-
}, [achievementQueue, currentAchievement]);
|
|
241
|
-
const showBadgesModal = () =>
|
|
242
|
-
setShowBadges(true);
|
|
243
|
-
};
|
|
303
|
+
}, [achievementQueue, currentAchievement, storageKey]);
|
|
304
|
+
const showBadgesModal = () => setShowBadges(true);
|
|
244
305
|
const getAchievements = (achievedIds) => {
|
|
245
306
|
return Object.values(config).flatMap(conditions => conditions.filter(c => achievedIds.includes(c.data.id)).map(c => c.data));
|
|
246
307
|
};
|
|
@@ -253,20 +314,20 @@ const AchievementProvider = ({ children, config, initialState = {}, storageKey =
|
|
|
253
314
|
return updatedMetrics;
|
|
254
315
|
});
|
|
255
316
|
},
|
|
256
|
-
|
|
317
|
+
unlockedAchievements,
|
|
257
318
|
checkAchievements,
|
|
258
319
|
showBadgesModal
|
|
259
320
|
};
|
|
260
321
|
return (React.createElement(AchievementContext.Provider, { value: contextValue },
|
|
261
322
|
children,
|
|
262
|
-
React.createElement(AchievementModal$1, {
|
|
323
|
+
React.createElement(AchievementModal$1, { isOpen: !!currentAchievement, achievement: currentAchievement, onClose: () => {
|
|
263
324
|
setCurrentAchievement(null);
|
|
264
|
-
if (achievementQueue.length === 0) {
|
|
325
|
+
if (achievementQueue.length === 0 && newlyUnlockedAchievements.length === 0) {
|
|
265
326
|
setShowConfetti(false);
|
|
266
327
|
}
|
|
267
|
-
} }),
|
|
268
|
-
React.createElement(BadgesModal$1, {
|
|
269
|
-
React.createElement(BadgesButton$1, { onClick: showBadgesModal, position: badgesButtonPosition }),
|
|
328
|
+
}, styles: mergedStyles.achievementModal }),
|
|
329
|
+
React.createElement(BadgesModal$1, { isOpen: showBadges, achievements: getAchievements(unlockedAchievements), onClose: () => setShowBadges(false), styles: mergedStyles.badgesModal }),
|
|
330
|
+
React.createElement(BadgesButton$1, { onClick: showBadgesModal, position: badgesButtonPosition, styles: mergedStyles.badgesButton }),
|
|
270
331
|
React.createElement(ConfettiWrapper, { show: showConfetti || achievementQueue.length > 0 })));
|
|
271
332
|
};
|
|
272
333
|
const useAchievement = () => {
|
|
@@ -276,6 +337,27 @@ const useAchievement = () => {
|
|
|
276
337
|
}
|
|
277
338
|
return context;
|
|
278
339
|
};
|
|
340
|
+
// Helper function to deep merge objects
|
|
341
|
+
function mergeDeep(target, source) {
|
|
342
|
+
const output = Object.assign({}, target);
|
|
343
|
+
if (isObject(target) && isObject(source)) {
|
|
344
|
+
Object.keys(source).forEach(key => {
|
|
345
|
+
if (isObject(source[key])) {
|
|
346
|
+
if (!(key in target))
|
|
347
|
+
Object.assign(output, { [key]: source[key] });
|
|
348
|
+
else
|
|
349
|
+
output[key] = mergeDeep(target[key], source[key]);
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
Object.assign(output, { [key]: source[key] });
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
return output;
|
|
357
|
+
}
|
|
358
|
+
function isObject(item) {
|
|
359
|
+
return (item && typeof item === 'object' && !Array.isArray(item));
|
|
360
|
+
}
|
|
279
361
|
|
|
280
362
|
exports.AchievementProvider = AchievementProvider;
|
|
281
363
|
exports.ConfettiWrapper = ConfettiWrapper;
|