react-achievements 1.3.0 → 1.3.2
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 +308 -101
- 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 +2 -0
- package/dist/defaultStyles.d.ts +28 -0
- package/dist/index.cjs.js +173 -110
- package/dist/index.esm.js +173 -110
- package/dist/types.d.ts +6 -7
- 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 +50 -37
- 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,32 +232,19 @@ 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
249
|
const [unlockedAchievements, setUnlockedAchievements] = useState(() => {
|
|
205
250
|
const saved = localStorage.getItem(`${storageKey}-unlocked-achievements`);
|
|
@@ -225,14 +270,20 @@ const AchievementProvider = ({ children, config, initialState = {}, storageKey =
|
|
|
225
270
|
});
|
|
226
271
|
if (newAchievements.length > 0) {
|
|
227
272
|
const newlyUnlockedIds = newAchievements.map(a => a.id);
|
|
228
|
-
setUnlockedAchievements(prev =>
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
+
});
|
|
232
283
|
setAchievementQueue(prevQueue => [...prevQueue, ...newAchievements]);
|
|
233
284
|
setShowConfetti(true);
|
|
234
285
|
}
|
|
235
|
-
}, [config, metrics, unlockedAchievements,
|
|
286
|
+
}, [config, metrics, unlockedAchievements, storageKey]);
|
|
236
287
|
useEffect(() => {
|
|
237
288
|
checkAchievements();
|
|
238
289
|
}, [metrics, checkAchievements]);
|
|
@@ -248,9 +299,7 @@ const AchievementProvider = ({ children, config, initialState = {}, storageKey =
|
|
|
248
299
|
});
|
|
249
300
|
}
|
|
250
301
|
}, [achievementQueue, currentAchievement, storageKey]);
|
|
251
|
-
const showBadgesModal = () =>
|
|
252
|
-
setShowBadges(true);
|
|
253
|
-
};
|
|
302
|
+
const showBadgesModal = () => setShowBadges(true);
|
|
254
303
|
const getAchievements = (achievedIds) => {
|
|
255
304
|
return Object.values(config).flatMap(conditions => conditions.filter(c => achievedIds.includes(c.data.id)).map(c => c.data));
|
|
256
305
|
};
|
|
@@ -267,23 +316,16 @@ const AchievementProvider = ({ children, config, initialState = {}, storageKey =
|
|
|
267
316
|
checkAchievements,
|
|
268
317
|
showBadgesModal
|
|
269
318
|
};
|
|
270
|
-
useEffect(() => {
|
|
271
|
-
if (newlyUnlockedAchievements.length > 0) {
|
|
272
|
-
const achievementsToShow = getAchievements(newlyUnlockedAchievements);
|
|
273
|
-
setAchievementQueue(achievementsToShow);
|
|
274
|
-
setShowConfetti(true);
|
|
275
|
-
}
|
|
276
|
-
}, []); // Run this effect only on component mount
|
|
277
319
|
return (React.createElement(AchievementContext.Provider, { value: contextValue },
|
|
278
320
|
children,
|
|
279
|
-
React.createElement(AchievementModal$1, {
|
|
321
|
+
React.createElement(AchievementModal$1, { isOpen: !!currentAchievement, achievement: currentAchievement, onClose: () => {
|
|
280
322
|
setCurrentAchievement(null);
|
|
281
323
|
if (achievementQueue.length === 0 && newlyUnlockedAchievements.length === 0) {
|
|
282
324
|
setShowConfetti(false);
|
|
283
325
|
}
|
|
284
|
-
} }),
|
|
285
|
-
React.createElement(BadgesModal$1, {
|
|
286
|
-
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 }),
|
|
287
329
|
React.createElement(ConfettiWrapper, { show: showConfetti || achievementQueue.length > 0 })));
|
|
288
330
|
};
|
|
289
331
|
const useAchievement = () => {
|
|
@@ -293,5 +335,26 @@ const useAchievement = () => {
|
|
|
293
335
|
}
|
|
294
336
|
return context;
|
|
295
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
|
+
}
|
|
296
359
|
|
|
297
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;
|