react-achievements 1.0.2 → 1.0.4
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/badges.d.ts +2 -2
- package/dist/components/Achievement.d.ts +2 -2
- package/dist/components/AchievementModal.d.ts +4 -3
- package/dist/components/BadgesButton.d.ts +7 -0
- package/dist/components/BadgesModal.d.ts +9 -0
- package/dist/components/ConfettiWrapper.d.ts +2 -0
- package/dist/components/Progress.d.ts +6 -0
- package/dist/context/AchievementContext.d.ts +14 -12
- package/dist/index.cjs.js +134 -106
- package/dist/index.d.ts +3 -2
- package/dist/index.esm.js +134 -106
- package/dist/levels.d.ts +3 -3
- package/dist/types.d.ts +17 -0
- package/package.json +3 -2
- package/public/badges/icon1.svg +1 -0
- package/src/components/AchievementModal.tsx +10 -7
- package/src/components/BadgesButton.tsx +29 -0
- package/src/components/BadgesModal.tsx +58 -0
- package/src/components/ConfettiWrapper.tsx +13 -3
- package/src/context/AchievementContext.tsx +78 -37
- package/src/index.ts +11 -2
- package/src/{custom.d.ts → react-confetti.d.ts} +2 -4
- package/src/react-use.d.ts +4 -0
- package/src/types.ts +21 -0
- package/tsconfig.json +3 -1
- package/src/badges.ts +0 -24
- package/src/components/Achievement.tsx +0 -29
- package/src/components/Badge.tsx +0 -34
- package/src/levels.ts +0 -13
package/dist/badges.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { AchievementData } from '../types';
|
|
2
3
|
interface AchievementModalProps {
|
|
3
4
|
show: boolean;
|
|
4
|
-
|
|
5
|
+
achievement: AchievementData | null;
|
|
5
6
|
onClose: () => void;
|
|
6
7
|
}
|
|
7
|
-
declare const
|
|
8
|
-
export default
|
|
8
|
+
declare const _default: React.NamedExoticComponent<AchievementModalProps>;
|
|
9
|
+
export default _default;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { AchievementData } from '../types';
|
|
3
|
+
interface BadgesModalProps {
|
|
4
|
+
show: boolean;
|
|
5
|
+
achievements: AchievementData[];
|
|
6
|
+
onClose: () => void;
|
|
7
|
+
}
|
|
8
|
+
declare const _default: React.NamedExoticComponent<BadgesModalProps>;
|
|
9
|
+
export default _default;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { ConfettiProps } from 'react-confetti';
|
|
2
3
|
interface ConfettiWrapperProps {
|
|
3
4
|
show: boolean;
|
|
5
|
+
confettiProps?: Partial<ConfettiProps>;
|
|
4
6
|
}
|
|
5
7
|
declare const ConfettiWrapper: React.FC<ConfettiWrapperProps>;
|
|
6
8
|
export default ConfettiWrapper;
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import React, { ReactNode } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
import { LevelConfig } from '../levels';
|
|
2
|
+
import { Metrics, AchievementConfig } from '../types';
|
|
4
3
|
interface AchievementContextProps {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
handleAchieve: (level: number, message: string) => void;
|
|
4
|
+
metrics: Metrics;
|
|
5
|
+
setMetrics: (metrics: Metrics) => void;
|
|
6
|
+
achievedAchievements: string[];
|
|
7
|
+
checkAchievements: () => void;
|
|
8
|
+
showBadgesModal: () => void;
|
|
11
9
|
}
|
|
12
|
-
|
|
10
|
+
interface AchievementProviderProps {
|
|
13
11
|
children: ReactNode;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
config: AchievementConfig;
|
|
13
|
+
storageKey?: string;
|
|
14
|
+
badgesButtonPosition?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
15
|
+
}
|
|
16
|
+
export declare const AchievementProvider: React.FC<AchievementProviderProps>;
|
|
17
|
+
export declare const useAchievement: () => AchievementContextProps;
|
|
18
|
+
export {};
|
package/dist/index.cjs.js
CHANGED
|
@@ -2,27 +2,94 @@
|
|
|
2
2
|
|
|
3
3
|
var React = require('react');
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
5
|
+
const AchievementModal = ({ show, achievement, onClose }) => {
|
|
6
|
+
if (!show || !achievement)
|
|
7
|
+
return null;
|
|
8
|
+
const modalStyle = {
|
|
9
|
+
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)',
|
|
17
|
+
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"))));
|
|
36
|
+
};
|
|
37
|
+
var AchievementModal$1 = React.memo(AchievementModal);
|
|
38
|
+
|
|
39
|
+
const BadgesModal = ({ show, achievements, onClose }) => {
|
|
40
|
+
if (!show)
|
|
41
|
+
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("p", null, achievement.description))))),
|
|
73
|
+
React.createElement("button", { onClick: onClose, style: { marginTop: '20px' } }, "Close"))));
|
|
74
|
+
};
|
|
75
|
+
var BadgesModal$1 = React.memo(BadgesModal);
|
|
76
|
+
|
|
77
|
+
const BadgesButton = ({ onClick, position }) => {
|
|
78
|
+
const buttonStyle = {
|
|
79
|
+
position: 'fixed',
|
|
80
|
+
[position.split('-')[0]]: '20px',
|
|
81
|
+
[position.split('-')[1]]: '20px',
|
|
82
|
+
padding: '10px 20px',
|
|
83
|
+
backgroundColor: '#007bff',
|
|
84
|
+
color: '#fff',
|
|
85
|
+
border: 'none',
|
|
86
|
+
borderRadius: '5px',
|
|
87
|
+
cursor: 'pointer',
|
|
88
|
+
zIndex: 998,
|
|
89
|
+
};
|
|
90
|
+
return (React.createElement("button", { style: buttonStyle, onClick: onClick }, "View Achievements"));
|
|
91
|
+
};
|
|
92
|
+
var BadgesButton$1 = React.memo(BadgesButton);
|
|
26
93
|
|
|
27
94
|
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
|
|
28
95
|
|
|
@@ -110,87 +177,63 @@ var useWindowSize = function (initialWidth, initialHeight) {
|
|
|
110
177
|
return state;
|
|
111
178
|
};
|
|
112
179
|
|
|
113
|
-
const ConfettiWrapper = ({ show }) => {
|
|
180
|
+
const ConfettiWrapper = ({ show, confettiProps }) => {
|
|
114
181
|
const { width, height } = useWindowSize();
|
|
115
|
-
return show ? React.createElement(Confetti, { width: width, height: height }) : null;
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
const AchievementModal = ({ show, message, onClose }) => {
|
|
119
182
|
if (!show)
|
|
120
183
|
return null;
|
|
121
|
-
|
|
122
|
-
position: 'fixed',
|
|
123
|
-
top: '50%',
|
|
124
|
-
left: '50%',
|
|
125
|
-
transform: 'translate(-50%, -50%)',
|
|
126
|
-
backgroundColor: '#fff',
|
|
127
|
-
padding: '20px',
|
|
128
|
-
borderRadius: '8px',
|
|
129
|
-
boxShadow: '0 0 10px rgba(0,0,0,0.1)',
|
|
130
|
-
zIndex: 1000,
|
|
131
|
-
};
|
|
132
|
-
const overlayStyle = {
|
|
133
|
-
position: 'fixed',
|
|
134
|
-
top: 0,
|
|
135
|
-
left: 0,
|
|
136
|
-
right: 0,
|
|
137
|
-
bottom: 0,
|
|
138
|
-
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
139
|
-
zIndex: 999,
|
|
140
|
-
};
|
|
141
|
-
return (React.createElement(React.Fragment, null,
|
|
142
|
-
React.createElement("div", { style: overlayStyle, onClick: onClose }),
|
|
143
|
-
React.createElement("div", { style: modalStyle },
|
|
144
|
-
React.createElement("h2", null, "Achievement Unlocked!"),
|
|
145
|
-
React.createElement("p", null, message),
|
|
146
|
-
React.createElement("button", { onClick: onClose }, "Okay"))));
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
const Badge = ({ icon, title, description, position = 'top-right' }) => {
|
|
150
|
-
const badgeStyle = {
|
|
151
|
-
position: 'fixed',
|
|
152
|
-
[position.split('-')[0]]: '10px',
|
|
153
|
-
[position.split('-')[1]]: '10px',
|
|
154
|
-
display: 'flex',
|
|
155
|
-
flexDirection: 'column',
|
|
156
|
-
alignItems: 'center',
|
|
157
|
-
backgroundColor: '#fff',
|
|
158
|
-
border: '1px solid #ccc',
|
|
159
|
-
borderRadius: '8px',
|
|
160
|
-
padding: '10px',
|
|
161
|
-
boxShadow: '0 0 10px rgba(0,0,0,0.1)',
|
|
162
|
-
};
|
|
163
|
-
return (React.createElement("div", { style: badgeStyle },
|
|
164
|
-
React.createElement("img", { src: icon, alt: title, style: { width: '50px', height: '50px' } }),
|
|
165
|
-
React.createElement("h4", null, title),
|
|
166
|
-
React.createElement("p", null, description)));
|
|
184
|
+
return (React.createElement(Confetti, Object.assign({ width: width, height: height }, confettiProps)));
|
|
167
185
|
};
|
|
168
186
|
|
|
169
187
|
const AchievementContext = React.createContext(undefined);
|
|
170
|
-
const AchievementProvider = ({ children }) => {
|
|
171
|
-
const [
|
|
172
|
-
const [
|
|
188
|
+
const AchievementProvider = ({ children, config, storageKey = 'react-achievements', badgesButtonPosition = 'top-right' }) => {
|
|
189
|
+
const [metrics, setMetrics] = React.useState({});
|
|
190
|
+
const [achievedAchievements, setAchievedAchievements] = React.useState(() => {
|
|
191
|
+
const saved = localStorage.getItem(`${storageKey}-achievements`);
|
|
192
|
+
return saved ? JSON.parse(saved) : [];
|
|
193
|
+
});
|
|
194
|
+
const [newAchievement, setNewAchievement] = React.useState(null);
|
|
195
|
+
const [showBadges, setShowBadges] = React.useState(false);
|
|
173
196
|
const [showConfetti, setShowConfetti] = React.useState(false);
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
197
|
+
const checkAchievements = () => {
|
|
198
|
+
const newAchievements = [];
|
|
199
|
+
Object.entries(config).forEach(([metricKey, conditions]) => {
|
|
200
|
+
const metricValue = metrics[metricKey];
|
|
201
|
+
conditions.forEach(condition => {
|
|
202
|
+
if (condition.check(metricValue) && !achievedAchievements.includes(condition.data.id)) {
|
|
203
|
+
newAchievements.push(condition.data);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
if (newAchievements.length > 0) {
|
|
208
|
+
const updatedAchievements = [...achievedAchievements, ...newAchievements.map(a => a.id)];
|
|
209
|
+
setAchievedAchievements(updatedAchievements);
|
|
210
|
+
localStorage.setItem(`${storageKey}-achievements`, JSON.stringify(updatedAchievements));
|
|
211
|
+
setNewAchievement(newAchievements[0]);
|
|
178
212
|
setShowConfetti(true);
|
|
179
|
-
setModalMessage(message);
|
|
180
213
|
}
|
|
181
214
|
};
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
215
|
+
const showBadgesModal = () => {
|
|
216
|
+
setShowBadges(true);
|
|
217
|
+
};
|
|
218
|
+
const contextValue = {
|
|
219
|
+
metrics,
|
|
220
|
+
setMetrics: (newMetrics) => {
|
|
221
|
+
setMetrics(newMetrics);
|
|
222
|
+
checkAchievements();
|
|
223
|
+
},
|
|
224
|
+
achievedAchievements,
|
|
225
|
+
checkAchievements,
|
|
226
|
+
showBadgesModal
|
|
185
227
|
};
|
|
186
|
-
return (React.createElement(AchievementContext.Provider, { value:
|
|
228
|
+
return (React.createElement(AchievementContext.Provider, { value: contextValue },
|
|
187
229
|
children,
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
React.createElement(
|
|
193
|
-
React.createElement(
|
|
230
|
+
React.createElement(AchievementModal$1, { show: !!newAchievement, achievement: newAchievement, onClose: () => {
|
|
231
|
+
setNewAchievement(null);
|
|
232
|
+
setShowConfetti(false);
|
|
233
|
+
} }),
|
|
234
|
+
React.createElement(BadgesModal$1, { show: showBadges, achievements: Object.values(config).flatMap(conditions => conditions.filter(c => achievedAchievements.includes(c.data.id)).map(c => c.data)), onClose: () => setShowBadges(false) }),
|
|
235
|
+
React.createElement(BadgesButton$1, { onClick: showBadgesModal, position: badgesButtonPosition }),
|
|
236
|
+
React.createElement(ConfettiWrapper, { show: showConfetti })));
|
|
194
237
|
};
|
|
195
238
|
const useAchievement = () => {
|
|
196
239
|
const context = React.useContext(AchievementContext);
|
|
@@ -200,21 +243,6 @@ const useAchievement = () => {
|
|
|
200
243
|
return context;
|
|
201
244
|
};
|
|
202
245
|
|
|
203
|
-
const Achievement = ({ metric, threshold, onAchieve, message, children }) => {
|
|
204
|
-
const { setMetric, achievedLevels, levels, handleAchieve } = useAchievement();
|
|
205
|
-
React.useEffect(() => {
|
|
206
|
-
if (metric >= threshold && !achievedLevels.includes(threshold)) {
|
|
207
|
-
onAchieve();
|
|
208
|
-
const levelConfig = levels.find(level => level.threshold === threshold);
|
|
209
|
-
if (levelConfig) {
|
|
210
|
-
setMetric(metric);
|
|
211
|
-
handleAchieve(levelConfig.level, message);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}, [metric, threshold, onAchieve, setMetric, achievedLevels, levels, message, handleAchieve]);
|
|
215
|
-
return React.createElement("div", null, children);
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
exports.Achievement = Achievement;
|
|
219
246
|
exports.AchievementProvider = AchievementProvider;
|
|
247
|
+
exports.ConfettiWrapper = ConfettiWrapper;
|
|
220
248
|
exports.useAchievement = useAchievement;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { AchievementProvider, useAchievement } from './context/AchievementContext';
|
|
2
|
-
import
|
|
3
|
-
|
|
2
|
+
import { Metrics, AchievementConfig, AchievementData, AchievementCondition } from './types';
|
|
3
|
+
import ConfettiWrapper from './components/ConfettiWrapper';
|
|
4
|
+
export { AchievementProvider, useAchievement, Metrics, AchievementConfig, AchievementData, AchievementCondition, ConfettiWrapper };
|
package/dist/index.esm.js
CHANGED
|
@@ -1,26 +1,93 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState, useCallback, createContext, useContext } from 'react';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
3
|
+
const AchievementModal = ({ show, achievement, onClose }) => {
|
|
4
|
+
if (!show || !achievement)
|
|
5
|
+
return null;
|
|
6
|
+
const modalStyle = {
|
|
7
|
+
position: 'fixed',
|
|
8
|
+
top: '50%',
|
|
9
|
+
left: '50%',
|
|
10
|
+
transform: 'translate(-50%, -50%)',
|
|
11
|
+
backgroundColor: '#fff',
|
|
12
|
+
padding: '20px',
|
|
13
|
+
borderRadius: '8px',
|
|
14
|
+
boxShadow: '0 0 10px rgba(0,0,0,0.1)',
|
|
15
|
+
zIndex: 1000,
|
|
16
|
+
};
|
|
17
|
+
const overlayStyle = {
|
|
18
|
+
position: 'fixed',
|
|
19
|
+
top: 0,
|
|
20
|
+
left: 0,
|
|
21
|
+
right: 0,
|
|
22
|
+
bottom: 0,
|
|
23
|
+
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
24
|
+
zIndex: 999,
|
|
25
|
+
};
|
|
26
|
+
return (React.createElement(React.Fragment, null,
|
|
27
|
+
React.createElement("div", { style: overlayStyle, onClick: onClose }),
|
|
28
|
+
React.createElement("div", { style: modalStyle, role: "dialog", "aria-modal": "true", "aria-labelledby": "achievement-title" },
|
|
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"))));
|
|
34
|
+
};
|
|
35
|
+
var AchievementModal$1 = React.memo(AchievementModal);
|
|
36
|
+
|
|
37
|
+
const BadgesModal = ({ show, achievements, onClose }) => {
|
|
38
|
+
if (!show)
|
|
39
|
+
return null;
|
|
40
|
+
const modalStyle = {
|
|
41
|
+
position: 'fixed',
|
|
42
|
+
top: '50%',
|
|
43
|
+
left: '50%',
|
|
44
|
+
transform: 'translate(-50%, -50%)',
|
|
45
|
+
backgroundColor: '#fff',
|
|
46
|
+
padding: '20px',
|
|
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("p", null, achievement.description))))),
|
|
71
|
+
React.createElement("button", { onClick: onClose, style: { marginTop: '20px' } }, "Close"))));
|
|
72
|
+
};
|
|
73
|
+
var BadgesModal$1 = React.memo(BadgesModal);
|
|
74
|
+
|
|
75
|
+
const BadgesButton = ({ onClick, position }) => {
|
|
76
|
+
const buttonStyle = {
|
|
77
|
+
position: 'fixed',
|
|
78
|
+
[position.split('-')[0]]: '20px',
|
|
79
|
+
[position.split('-')[1]]: '20px',
|
|
80
|
+
padding: '10px 20px',
|
|
81
|
+
backgroundColor: '#007bff',
|
|
82
|
+
color: '#fff',
|
|
83
|
+
border: 'none',
|
|
84
|
+
borderRadius: '5px',
|
|
85
|
+
cursor: 'pointer',
|
|
86
|
+
zIndex: 998,
|
|
87
|
+
};
|
|
88
|
+
return (React.createElement("button", { style: buttonStyle, onClick: onClick }, "View Achievements"));
|
|
89
|
+
};
|
|
90
|
+
var BadgesButton$1 = React.memo(BadgesButton);
|
|
24
91
|
|
|
25
92
|
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
|
|
26
93
|
|
|
@@ -108,87 +175,63 @@ var useWindowSize = function (initialWidth, initialHeight) {
|
|
|
108
175
|
return state;
|
|
109
176
|
};
|
|
110
177
|
|
|
111
|
-
const ConfettiWrapper = ({ show }) => {
|
|
178
|
+
const ConfettiWrapper = ({ show, confettiProps }) => {
|
|
112
179
|
const { width, height } = useWindowSize();
|
|
113
|
-
return show ? React.createElement(Confetti, { width: width, height: height }) : null;
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
const AchievementModal = ({ show, message, onClose }) => {
|
|
117
180
|
if (!show)
|
|
118
181
|
return null;
|
|
119
|
-
|
|
120
|
-
position: 'fixed',
|
|
121
|
-
top: '50%',
|
|
122
|
-
left: '50%',
|
|
123
|
-
transform: 'translate(-50%, -50%)',
|
|
124
|
-
backgroundColor: '#fff',
|
|
125
|
-
padding: '20px',
|
|
126
|
-
borderRadius: '8px',
|
|
127
|
-
boxShadow: '0 0 10px rgba(0,0,0,0.1)',
|
|
128
|
-
zIndex: 1000,
|
|
129
|
-
};
|
|
130
|
-
const overlayStyle = {
|
|
131
|
-
position: 'fixed',
|
|
132
|
-
top: 0,
|
|
133
|
-
left: 0,
|
|
134
|
-
right: 0,
|
|
135
|
-
bottom: 0,
|
|
136
|
-
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
137
|
-
zIndex: 999,
|
|
138
|
-
};
|
|
139
|
-
return (React.createElement(React.Fragment, null,
|
|
140
|
-
React.createElement("div", { style: overlayStyle, onClick: onClose }),
|
|
141
|
-
React.createElement("div", { style: modalStyle },
|
|
142
|
-
React.createElement("h2", null, "Achievement Unlocked!"),
|
|
143
|
-
React.createElement("p", null, message),
|
|
144
|
-
React.createElement("button", { onClick: onClose }, "Okay"))));
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
const Badge = ({ icon, title, description, position = 'top-right' }) => {
|
|
148
|
-
const badgeStyle = {
|
|
149
|
-
position: 'fixed',
|
|
150
|
-
[position.split('-')[0]]: '10px',
|
|
151
|
-
[position.split('-')[1]]: '10px',
|
|
152
|
-
display: 'flex',
|
|
153
|
-
flexDirection: 'column',
|
|
154
|
-
alignItems: 'center',
|
|
155
|
-
backgroundColor: '#fff',
|
|
156
|
-
border: '1px solid #ccc',
|
|
157
|
-
borderRadius: '8px',
|
|
158
|
-
padding: '10px',
|
|
159
|
-
boxShadow: '0 0 10px rgba(0,0,0,0.1)',
|
|
160
|
-
};
|
|
161
|
-
return (React.createElement("div", { style: badgeStyle },
|
|
162
|
-
React.createElement("img", { src: icon, alt: title, style: { width: '50px', height: '50px' } }),
|
|
163
|
-
React.createElement("h4", null, title),
|
|
164
|
-
React.createElement("p", null, description)));
|
|
182
|
+
return (React.createElement(Confetti, Object.assign({ width: width, height: height }, confettiProps)));
|
|
165
183
|
};
|
|
166
184
|
|
|
167
185
|
const AchievementContext = createContext(undefined);
|
|
168
|
-
const AchievementProvider = ({ children }) => {
|
|
169
|
-
const [
|
|
170
|
-
const [
|
|
186
|
+
const AchievementProvider = ({ children, config, storageKey = 'react-achievements', badgesButtonPosition = 'top-right' }) => {
|
|
187
|
+
const [metrics, setMetrics] = useState({});
|
|
188
|
+
const [achievedAchievements, setAchievedAchievements] = useState(() => {
|
|
189
|
+
const saved = localStorage.getItem(`${storageKey}-achievements`);
|
|
190
|
+
return saved ? JSON.parse(saved) : [];
|
|
191
|
+
});
|
|
192
|
+
const [newAchievement, setNewAchievement] = useState(null);
|
|
193
|
+
const [showBadges, setShowBadges] = useState(false);
|
|
171
194
|
const [showConfetti, setShowConfetti] = useState(false);
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
195
|
+
const checkAchievements = () => {
|
|
196
|
+
const newAchievements = [];
|
|
197
|
+
Object.entries(config).forEach(([metricKey, conditions]) => {
|
|
198
|
+
const metricValue = metrics[metricKey];
|
|
199
|
+
conditions.forEach(condition => {
|
|
200
|
+
if (condition.check(metricValue) && !achievedAchievements.includes(condition.data.id)) {
|
|
201
|
+
newAchievements.push(condition.data);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
if (newAchievements.length > 0) {
|
|
206
|
+
const updatedAchievements = [...achievedAchievements, ...newAchievements.map(a => a.id)];
|
|
207
|
+
setAchievedAchievements(updatedAchievements);
|
|
208
|
+
localStorage.setItem(`${storageKey}-achievements`, JSON.stringify(updatedAchievements));
|
|
209
|
+
setNewAchievement(newAchievements[0]);
|
|
176
210
|
setShowConfetti(true);
|
|
177
|
-
setModalMessage(message);
|
|
178
211
|
}
|
|
179
212
|
};
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
213
|
+
const showBadgesModal = () => {
|
|
214
|
+
setShowBadges(true);
|
|
215
|
+
};
|
|
216
|
+
const contextValue = {
|
|
217
|
+
metrics,
|
|
218
|
+
setMetrics: (newMetrics) => {
|
|
219
|
+
setMetrics(newMetrics);
|
|
220
|
+
checkAchievements();
|
|
221
|
+
},
|
|
222
|
+
achievedAchievements,
|
|
223
|
+
checkAchievements,
|
|
224
|
+
showBadgesModal
|
|
183
225
|
};
|
|
184
|
-
return (React.createElement(AchievementContext.Provider, { value:
|
|
226
|
+
return (React.createElement(AchievementContext.Provider, { value: contextValue },
|
|
185
227
|
children,
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
React.createElement(
|
|
191
|
-
React.createElement(
|
|
228
|
+
React.createElement(AchievementModal$1, { show: !!newAchievement, achievement: newAchievement, onClose: () => {
|
|
229
|
+
setNewAchievement(null);
|
|
230
|
+
setShowConfetti(false);
|
|
231
|
+
} }),
|
|
232
|
+
React.createElement(BadgesModal$1, { show: showBadges, achievements: Object.values(config).flatMap(conditions => conditions.filter(c => achievedAchievements.includes(c.data.id)).map(c => c.data)), onClose: () => setShowBadges(false) }),
|
|
233
|
+
React.createElement(BadgesButton$1, { onClick: showBadgesModal, position: badgesButtonPosition }),
|
|
234
|
+
React.createElement(ConfettiWrapper, { show: showConfetti })));
|
|
192
235
|
};
|
|
193
236
|
const useAchievement = () => {
|
|
194
237
|
const context = useContext(AchievementContext);
|
|
@@ -198,19 +241,4 @@ const useAchievement = () => {
|
|
|
198
241
|
return context;
|
|
199
242
|
};
|
|
200
243
|
|
|
201
|
-
|
|
202
|
-
const { setMetric, achievedLevels, levels, handleAchieve } = useAchievement();
|
|
203
|
-
React.useEffect(() => {
|
|
204
|
-
if (metric >= threshold && !achievedLevels.includes(threshold)) {
|
|
205
|
-
onAchieve();
|
|
206
|
-
const levelConfig = levels.find(level => level.threshold === threshold);
|
|
207
|
-
if (levelConfig) {
|
|
208
|
-
setMetric(metric);
|
|
209
|
-
handleAchieve(levelConfig.level, message);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}, [metric, threshold, onAchieve, setMetric, achievedLevels, levels, message, handleAchieve]);
|
|
213
|
-
return React.createElement("div", null, children);
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
export { Achievement, AchievementProvider, useAchievement };
|
|
244
|
+
export { AchievementProvider, ConfettiWrapper, useAchievement };
|
package/dist/levels.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
interface LevelConfig {
|
|
1
|
+
export interface LevelConfig {
|
|
2
2
|
level: number;
|
|
3
3
|
threshold: number;
|
|
4
4
|
badgeId: string;
|
|
5
5
|
}
|
|
6
|
-
declare const
|
|
7
|
-
export {
|
|
6
|
+
declare const defaultLevels: LevelConfig[];
|
|
7
|
+
export { defaultLevels };
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type MetricValue = number | boolean | string | any;
|
|
2
|
+
export interface Metrics {
|
|
3
|
+
[key: string]: MetricValue;
|
|
4
|
+
}
|
|
5
|
+
export interface AchievementData {
|
|
6
|
+
id: string;
|
|
7
|
+
title: string;
|
|
8
|
+
description: string;
|
|
9
|
+
icon: string;
|
|
10
|
+
}
|
|
11
|
+
export interface AchievementCondition {
|
|
12
|
+
check: (metricValue: MetricValue) => boolean;
|
|
13
|
+
data: AchievementData;
|
|
14
|
+
}
|
|
15
|
+
export interface AchievementConfig {
|
|
16
|
+
[metricKey: string]: AchievementCondition[];
|
|
17
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-achievements",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "This package allows users to transpose a React achievements engine over their React apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"types": "dist/index.d.ts",
|
|
13
13
|
"scripts": {
|
|
14
14
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
15
|
-
"build": "rollup -c"
|
|
15
|
+
"build": "rollup -c",
|
|
16
|
+
"deploy": "npm run build && npm publish"
|
|
16
17
|
},
|
|
17
18
|
"author": "David Brown",
|
|
18
19
|
"license": "ISC",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e8eaed"><path d="M560-440q-50 0-85-35t-35-85q0-50 35-85t85-35q50 0 85 35t35 85q0 50-35 85t-85 35ZM280-320q-33 0-56.5-23.5T200-400v-320q0-33 23.5-56.5T280-800h560q33 0 56.5 23.5T920-720v320q0 33-23.5 56.5T840-320H280Zm80-80h400q0-33 23.5-56.5T840-480v-160q-33 0-56.5-23.5T760-720H360q0 33-23.5 56.5T280-640v160q33 0 56.5 23.5T360-400Zm440 240H120q-33 0-56.5-23.5T40-240v-440h80v440h680v80ZM280-400v-320 320Z"/></svg>
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { AchievementData } from '../types';
|
|
2
3
|
|
|
3
4
|
interface AchievementModalProps {
|
|
4
5
|
show: boolean;
|
|
5
|
-
|
|
6
|
+
achievement: AchievementData | null;
|
|
6
7
|
onClose: () => void;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
|
-
const AchievementModal: React.FC<AchievementModalProps> = ({ show,
|
|
10
|
-
if (!show) return null;
|
|
10
|
+
const AchievementModal: React.FC<AchievementModalProps> = ({ show, achievement, onClose }) => {
|
|
11
|
+
if (!show || !achievement) return null;
|
|
11
12
|
|
|
12
13
|
const modalStyle: React.CSSProperties = {
|
|
13
14
|
position: 'fixed',
|
|
@@ -34,13 +35,15 @@ const AchievementModal: React.FC<AchievementModalProps> = ({ show, message, onCl
|
|
|
34
35
|
return (
|
|
35
36
|
<>
|
|
36
37
|
<div style={overlayStyle} onClick={onClose} />
|
|
37
|
-
<div style={modalStyle}>
|
|
38
|
-
<h2>Achievement Unlocked!</h2>
|
|
39
|
-
<
|
|
38
|
+
<div style={modalStyle} role="dialog" aria-modal="true" aria-labelledby="achievement-title">
|
|
39
|
+
<h2 id="achievement-title">Achievement Unlocked!</h2>
|
|
40
|
+
<img src={achievement.icon} alt={achievement.title} style={{ width: '50px', height: '50px' }} />
|
|
41
|
+
<h3>{achievement.title}</h3>
|
|
42
|
+
<p>{achievement.description}</p>
|
|
40
43
|
<button onClick={onClose}>Okay</button>
|
|
41
44
|
</div>
|
|
42
45
|
</>
|
|
43
46
|
);
|
|
44
47
|
};
|
|
45
48
|
|
|
46
|
-
export default AchievementModal;
|
|
49
|
+
export default React.memo(AchievementModal);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface BadgesButtonProps {
|
|
4
|
+
onClick: () => void;
|
|
5
|
+
position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const BadgesButton: React.FC<BadgesButtonProps> = ({ onClick, position }) => {
|
|
9
|
+
const buttonStyle: React.CSSProperties = {
|
|
10
|
+
position: 'fixed',
|
|
11
|
+
[position.split('-')[0]]: '20px',
|
|
12
|
+
[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
|
+
};
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<button style={buttonStyle} onClick={onClick}>
|
|
24
|
+
View Achievements
|
|
25
|
+
</button>
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export default React.memo(BadgesButton);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { AchievementData } from '../types';
|
|
3
|
+
|
|
4
|
+
interface BadgesModalProps {
|
|
5
|
+
show: boolean;
|
|
6
|
+
achievements: AchievementData[];
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const BadgesModal: React.FC<BadgesModalProps> = ({ show, achievements, onClose }) => {
|
|
11
|
+
if (!show) return null;
|
|
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
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<>
|
|
40
|
+
<div style={overlayStyle} onClick={onClose} />
|
|
41
|
+
<div style={modalStyle} role="dialog" aria-modal="true" aria-labelledby="badges-title">
|
|
42
|
+
<h2 id="badges-title">Your Achievements</h2>
|
|
43
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'center' }}>
|
|
44
|
+
{achievements.map(achievement => (
|
|
45
|
+
<div key={achievement.id} style={{ margin: '10px', textAlign: 'center' }}>
|
|
46
|
+
<img src={achievement.icon} alt={achievement.title} style={{ width: '50px', height: '50px' }} />
|
|
47
|
+
<h4>{achievement.title}</h4>
|
|
48
|
+
<p>{achievement.description}</p>
|
|
49
|
+
</div>
|
|
50
|
+
))}
|
|
51
|
+
</div>
|
|
52
|
+
<button onClick={onClose} style={{ marginTop: '20px' }}>Close</button>
|
|
53
|
+
</div>
|
|
54
|
+
</>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export default React.memo(BadgesModal);
|
|
@@ -1,15 +1,25 @@
|
|
|
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';
|
|
4
5
|
|
|
5
6
|
interface ConfettiWrapperProps {
|
|
6
7
|
show: boolean;
|
|
8
|
+
confettiProps?: Partial<ConfettiProps>;
|
|
7
9
|
}
|
|
8
10
|
|
|
9
|
-
const ConfettiWrapper: React.FC<ConfettiWrapperProps> = ({ show }) => {
|
|
11
|
+
const ConfettiWrapper: React.FC<ConfettiWrapperProps> = ({ show, confettiProps }) => {
|
|
10
12
|
const { width, height } = useWindowSize();
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
if (!show) return null;
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<Confetti
|
|
18
|
+
width={width}
|
|
19
|
+
height={height}
|
|
20
|
+
{...confettiProps}
|
|
21
|
+
/>
|
|
22
|
+
);
|
|
13
23
|
};
|
|
14
24
|
|
|
15
|
-
export default ConfettiWrapper;
|
|
25
|
+
export default ConfettiWrapper;
|
|
@@ -1,65 +1,106 @@
|
|
|
1
1
|
import React, { createContext, useContext, useState, ReactNode } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
import { levels, LevelConfig } from '../levels';
|
|
4
|
-
import ConfettiWrapper from '../components/ConfettiWrapper';
|
|
2
|
+
import { Metrics, AchievementConfig, AchievementData } from '../types';
|
|
5
3
|
import AchievementModal from '../components/AchievementModal';
|
|
6
|
-
import
|
|
4
|
+
import BadgesModal from '../components/BadgesModal';
|
|
5
|
+
import BadgesButton from '../components/BadgesButton';
|
|
6
|
+
import ConfettiWrapper from '../components/ConfettiWrapper';
|
|
7
7
|
|
|
8
8
|
interface AchievementContextProps {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
metrics: Metrics;
|
|
10
|
+
setMetrics: (metrics: Metrics) => void;
|
|
11
|
+
achievedAchievements: string[];
|
|
12
|
+
checkAchievements: () => void;
|
|
13
|
+
showBadgesModal: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface AchievementProviderProps {
|
|
17
|
+
children: ReactNode;
|
|
18
|
+
config: AchievementConfig;
|
|
19
|
+
storageKey?: string;
|
|
20
|
+
badgesButtonPosition?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
const AchievementContext = createContext<AchievementContextProps | undefined>(undefined);
|
|
18
24
|
|
|
19
|
-
const AchievementProvider: React.FC<
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
export const AchievementProvider: React.FC<AchievementProviderProps> = ({
|
|
26
|
+
children,
|
|
27
|
+
config,
|
|
28
|
+
storageKey = 'react-achievements',
|
|
29
|
+
badgesButtonPosition = 'top-right'
|
|
30
|
+
}) => {
|
|
31
|
+
const [metrics, setMetrics] = useState<Metrics>({});
|
|
32
|
+
const [achievedAchievements, setAchievedAchievements] = useState<string[]>(() => {
|
|
33
|
+
const saved = localStorage.getItem(`${storageKey}-achievements`);
|
|
34
|
+
return saved ? JSON.parse(saved) : [];
|
|
35
|
+
});
|
|
36
|
+
const [newAchievement, setNewAchievement] = useState<AchievementData | null>(null);
|
|
37
|
+
const [showBadges, setShowBadges] = useState(false);
|
|
22
38
|
const [showConfetti, setShowConfetti] = useState(false);
|
|
23
|
-
const [modalMessage, setModalMessage] = useState('');
|
|
24
39
|
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
40
|
+
const checkAchievements = () => {
|
|
41
|
+
const newAchievements: AchievementData[] = [];
|
|
42
|
+
|
|
43
|
+
Object.entries(config).forEach(([metricKey, conditions]) => {
|
|
44
|
+
const metricValue = metrics[metricKey];
|
|
45
|
+
conditions.forEach(condition => {
|
|
46
|
+
if (condition.check(metricValue) && !achievedAchievements.includes(condition.data.id)) {
|
|
47
|
+
newAchievements.push(condition.data);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (newAchievements.length > 0) {
|
|
53
|
+
const updatedAchievements = [...achievedAchievements, ...newAchievements.map(a => a.id)];
|
|
54
|
+
setAchievedAchievements(updatedAchievements);
|
|
55
|
+
localStorage.setItem(`${storageKey}-achievements`, JSON.stringify(updatedAchievements));
|
|
56
|
+
setNewAchievement(newAchievements[0]);
|
|
28
57
|
setShowConfetti(true);
|
|
29
|
-
setModalMessage(message);
|
|
30
58
|
}
|
|
31
59
|
};
|
|
32
60
|
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
61
|
+
const showBadgesModal = () => {
|
|
62
|
+
setShowBadges(true);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const contextValue: AchievementContextProps = {
|
|
66
|
+
metrics,
|
|
67
|
+
setMetrics: (newMetrics: Metrics) => {
|
|
68
|
+
setMetrics(newMetrics);
|
|
69
|
+
checkAchievements();
|
|
70
|
+
},
|
|
71
|
+
achievedAchievements,
|
|
72
|
+
checkAchievements,
|
|
73
|
+
showBadgesModal
|
|
36
74
|
};
|
|
37
75
|
|
|
38
76
|
return (
|
|
39
|
-
<AchievementContext.Provider value={
|
|
77
|
+
<AchievementContext.Provider value={contextValue}>
|
|
40
78
|
{children}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
79
|
+
<AchievementModal
|
|
80
|
+
show={!!newAchievement}
|
|
81
|
+
achievement={newAchievement}
|
|
82
|
+
onClose={() => {
|
|
83
|
+
setNewAchievement(null);
|
|
84
|
+
setShowConfetti(false);
|
|
85
|
+
}}
|
|
86
|
+
/>
|
|
87
|
+
<BadgesModal
|
|
88
|
+
show={showBadges}
|
|
89
|
+
achievements={Object.values(config).flatMap(conditions =>
|
|
90
|
+
conditions.filter(c => achievedAchievements.includes(c.data.id)).map(c => c.data)
|
|
91
|
+
)}
|
|
92
|
+
onClose={() => setShowBadges(false)}
|
|
93
|
+
/>
|
|
94
|
+
<BadgesButton onClick={showBadgesModal} position={badgesButtonPosition} />
|
|
51
95
|
<ConfettiWrapper show={showConfetti} />
|
|
52
|
-
<AchievementModal show={!!modalMessage} message={modalMessage} onClose={handleCloseModal} />
|
|
53
96
|
</AchievementContext.Provider>
|
|
54
97
|
);
|
|
55
98
|
};
|
|
56
99
|
|
|
57
|
-
const useAchievement = () => {
|
|
100
|
+
export const useAchievement = () => {
|
|
58
101
|
const context = useContext(AchievementContext);
|
|
59
102
|
if (context === undefined) {
|
|
60
103
|
throw new Error('useAchievement must be used within an AchievementProvider');
|
|
61
104
|
}
|
|
62
105
|
return context;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
export { AchievementProvider, useAchievement };
|
|
106
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import { AchievementProvider, useAchievement } from './context/AchievementContext';
|
|
2
|
-
import
|
|
2
|
+
import { Metrics, AchievementConfig, AchievementData, AchievementCondition } from './types';
|
|
3
|
+
import ConfettiWrapper from './components/ConfettiWrapper';
|
|
3
4
|
|
|
4
|
-
export {
|
|
5
|
+
export {
|
|
6
|
+
AchievementProvider,
|
|
7
|
+
useAchievement,
|
|
8
|
+
Metrics,
|
|
9
|
+
AchievementConfig,
|
|
10
|
+
AchievementData,
|
|
11
|
+
AchievementCondition,
|
|
12
|
+
ConfettiWrapper
|
|
13
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
declare module 'react-confetti' {
|
|
2
2
|
import { ComponentType } from 'react';
|
|
3
3
|
|
|
4
|
-
interface ConfettiProps {
|
|
4
|
+
export interface ConfettiProps {
|
|
5
5
|
width?: number;
|
|
6
6
|
height?: number;
|
|
7
7
|
numberOfPieces?: number;
|
|
@@ -16,6 +16,4 @@ declare module 'react-confetti' {
|
|
|
16
16
|
|
|
17
17
|
const Confetti: ComponentType<ConfettiProps>;
|
|
18
18
|
export default Confetti;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
declare module 'react-use'
|
|
19
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type MetricValue = number | boolean | string | any;
|
|
2
|
+
|
|
3
|
+
export interface Metrics {
|
|
4
|
+
[key: string]: MetricValue;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface AchievementData {
|
|
8
|
+
id: string;
|
|
9
|
+
title: string;
|
|
10
|
+
description: string;
|
|
11
|
+
icon: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface AchievementCondition {
|
|
15
|
+
check: (metricValue: MetricValue) => boolean;
|
|
16
|
+
data: AchievementData;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface AchievementConfig {
|
|
20
|
+
[metricKey: string]: AchievementCondition[];
|
|
21
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -105,5 +105,7 @@
|
|
|
105
105
|
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
|
106
106
|
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
|
107
107
|
},
|
|
108
|
-
"include": [
|
|
108
|
+
"include": [
|
|
109
|
+
"src",
|
|
110
|
+
"react-confetti.d.ts"]
|
|
109
111
|
}
|
package/src/badges.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
interface BadgeConfig {
|
|
2
|
-
id: string;
|
|
3
|
-
icon: string;
|
|
4
|
-
title: string;
|
|
5
|
-
description: string;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
const defaultBadges: BadgeConfig[] = [
|
|
9
|
-
{
|
|
10
|
-
id: 'beginner',
|
|
11
|
-
icon: '/path/to/beginner-icon.png',
|
|
12
|
-
title: 'Beginner',
|
|
13
|
-
description: 'Achieved beginner level',
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
id: 'intermediate',
|
|
17
|
-
icon: '/path/to/intermediate-icon.png',
|
|
18
|
-
title: 'Intermediate',
|
|
19
|
-
description: 'Achieved intermediate level',
|
|
20
|
-
},
|
|
21
|
-
// Add more badges as needed
|
|
22
|
-
];
|
|
23
|
-
|
|
24
|
-
export { defaultBadges, BadgeConfig };
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { useAchievement } from '../context/AchievementContext';
|
|
3
|
-
|
|
4
|
-
interface AchievementProps {
|
|
5
|
-
metric: number;
|
|
6
|
-
threshold: number;
|
|
7
|
-
onAchieve: () => void;
|
|
8
|
-
message: string;
|
|
9
|
-
children: React.ReactNode;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const Achievement: React.FC<AchievementProps> = ({ metric, threshold, onAchieve, message, children }) => {
|
|
13
|
-
const { setMetric, achievedLevels, levels, handleAchieve } = useAchievement();
|
|
14
|
-
|
|
15
|
-
React.useEffect(() => {
|
|
16
|
-
if (metric >= threshold && !achievedLevels.includes(threshold)) {
|
|
17
|
-
onAchieve();
|
|
18
|
-
const levelConfig = levels.find(level => level.threshold === threshold);
|
|
19
|
-
if (levelConfig) {
|
|
20
|
-
setMetric(metric);
|
|
21
|
-
handleAchieve(levelConfig.level, message);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}, [metric, threshold, onAchieve, setMetric, achievedLevels, levels, message, handleAchieve]);
|
|
25
|
-
|
|
26
|
-
return <div>{children}</div>;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export default Achievement;
|
package/src/components/Badge.tsx
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
|
|
3
|
-
interface BadgeProps {
|
|
4
|
-
icon: string;
|
|
5
|
-
title: string;
|
|
6
|
-
description: string;
|
|
7
|
-
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const Badge: React.FC<BadgeProps> = ({ icon, title, description, position = 'top-right' }) => {
|
|
11
|
-
const badgeStyle: React.CSSProperties = {
|
|
12
|
-
position: 'fixed',
|
|
13
|
-
[position.split('-')[0]]: '10px',
|
|
14
|
-
[position.split('-')[1]]: '10px',
|
|
15
|
-
display: 'flex',
|
|
16
|
-
flexDirection: 'column',
|
|
17
|
-
alignItems: 'center',
|
|
18
|
-
backgroundColor: '#fff',
|
|
19
|
-
border: '1px solid #ccc',
|
|
20
|
-
borderRadius: '8px',
|
|
21
|
-
padding: '10px',
|
|
22
|
-
boxShadow: '0 0 10px rgba(0,0,0,0.1)',
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
return (
|
|
26
|
-
<div style={badgeStyle}>
|
|
27
|
-
<img src={icon} alt={title} style={{ width: '50px', height: '50px' }} />
|
|
28
|
-
<h4>{title}</h4>
|
|
29
|
-
<p>{description}</p>
|
|
30
|
-
</div>
|
|
31
|
-
);
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export default Badge;
|
package/src/levels.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
interface LevelConfig {
|
|
2
|
-
level: number;
|
|
3
|
-
threshold: number;
|
|
4
|
-
badgeId: string;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
const levels: LevelConfig[] = [
|
|
8
|
-
{ level: 1, threshold: 10, badgeId: 'beginner' },
|
|
9
|
-
{ level: 2, threshold: 50, badgeId: 'intermediate' },
|
|
10
|
-
// Add more levels as needed
|
|
11
|
-
];
|
|
12
|
-
|
|
13
|
-
export { levels, LevelConfig };
|