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