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/dist/index.cjs.js CHANGED
@@ -2,91 +2,149 @@
2
2
 
3
3
  var React = require('react');
4
4
 
5
- const AchievementModal = ({ show, achievement, onClose }) => {
6
- if (!show || !achievement)
7
- return null;
8
- const modalStyle = {
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
- top: '50%',
11
- left: '50%',
12
- transform: 'translate(-50%, -50%)',
13
- backgroundColor: '#fff',
14
- padding: '20px',
15
- borderRadius: '8px',
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
- const overlayStyle = {
20
- position: 'fixed',
21
- top: 0,
22
- left: 0,
23
- right: 0,
24
- bottom: 0,
25
- backgroundColor: 'rgba(0,0,0,0.5)',
26
- zIndex: 999,
27
- };
28
- return (React.createElement(React.Fragment, null,
29
- React.createElement("div", { style: overlayStyle, onClick: onClose }),
30
- React.createElement("div", { style: modalStyle, role: "dialog", "aria-modal": "true", "aria-labelledby": "achievement-title" },
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 = ({ show, achievements, onClose }) => {
40
- if (!show)
129
+ const BadgesModal = ({ isOpen, achievements, onClose, styles }) => {
130
+ if (!isOpen)
41
131
  return null;
42
- const modalStyle = {
43
- position: 'fixed',
44
- top: '50%',
45
- left: '50%',
46
- transform: 'translate(-50%, -50%)',
47
- backgroundColor: '#fff',
48
- padding: '20px',
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 buttonStyle = {
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", { style: buttonStyle, onClick: onClick }, "View Achievements"));
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, confettiProps }) => {
237
+ const ConfettiWrapper = ({ show }) => {
180
238
  const { width, height } = useWindowSize();
181
239
  if (!show)
182
240
  return null;
183
- return (React.createElement(Confetti, Object.assign({ width: width, height: height }, confettiProps)));
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 extractMetrics = (state) => {
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
- if (savedMetrics) {
202
- return JSON.parse(savedMetrics);
203
- }
204
- return extractMetrics(initialState);
249
+ return savedMetrics ? JSON.parse(savedMetrics) : initialState;
205
250
  });
206
- const [achievedAchievements, setAchievedAchievements] = React.useState(() => {
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) && !achievedAchievements.includes(condition.data.id)) {
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 updatedAchievements = [...achievedAchievements, ...newAchievements.map(a => a.id)];
226
- setAchievedAchievements(updatedAchievements);
227
- localStorage.setItem(`${storageKey}-achievements`, JSON.stringify(updatedAchievements));
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, achievedAchievements, storageKey]);
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
- setCurrentAchievement(achievementQueue[0]);
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
- achievedAchievements,
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, { show: !!currentAchievement, achievement: currentAchievement, onClose: () => {
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, { show: showBadges, achievements: getAchievements(achievedAchievements), onClose: () => setShowBadges(false) }),
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;