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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react';
|
|
2
2
|
import { Metrics, AchievementConfig, AchievementData, MetricValue } from '../types';
|
|
3
|
+
import { defaultStyles, Styles } from '../defaultStyles';
|
|
3
4
|
import AchievementModal from '../components/AchievementModal';
|
|
4
5
|
import BadgesModal from '../components/BadgesModal';
|
|
5
6
|
import BadgesButton from '../components/BadgesButton';
|
|
@@ -8,7 +9,7 @@ import ConfettiWrapper from '../components/ConfettiWrapper';
|
|
|
8
9
|
interface AchievementContextProps {
|
|
9
10
|
metrics: Metrics;
|
|
10
11
|
setMetrics: (metrics: Metrics | ((prevMetrics: Metrics) => Metrics)) => void;
|
|
11
|
-
|
|
12
|
+
unlockedAchievements: string[];
|
|
12
13
|
checkAchievements: () => void;
|
|
13
14
|
showBadgesModal: () => void;
|
|
14
15
|
}
|
|
@@ -19,6 +20,7 @@ interface AchievementProviderProps {
|
|
|
19
20
|
initialState?: Record<string, MetricValue>;
|
|
20
21
|
storageKey?: string;
|
|
21
22
|
badgesButtonPosition?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
23
|
+
styles?: Partial<Styles>;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
const AchievementContext = createContext<AchievementContextProps | undefined>(undefined);
|
|
@@ -28,29 +30,23 @@ export const AchievementProvider: React.FC<AchievementProviderProps> = ({
|
|
|
28
30
|
config,
|
|
29
31
|
initialState = {},
|
|
30
32
|
storageKey = 'react-achievements',
|
|
31
|
-
badgesButtonPosition = 'top-right'
|
|
33
|
+
badgesButtonPosition = 'top-right',
|
|
34
|
+
styles = {},
|
|
32
35
|
}) => {
|
|
33
|
-
const
|
|
34
|
-
return Object.keys(config).reduce((acc, key) => {
|
|
35
|
-
if (key in state) {
|
|
36
|
-
acc[key] = state[key];
|
|
37
|
-
} else {
|
|
38
|
-
acc[key] = [];
|
|
39
|
-
}
|
|
40
|
-
return acc;
|
|
41
|
-
}, {} as Metrics);
|
|
42
|
-
};
|
|
36
|
+
const mergedStyles = React.useMemo(() => mergeDeep(defaultStyles, styles), [styles]);
|
|
43
37
|
|
|
44
38
|
const [metrics, setMetrics] = useState<Metrics>(() => {
|
|
45
39
|
const savedMetrics = localStorage.getItem(`${storageKey}-metrics`);
|
|
46
|
-
|
|
47
|
-
return JSON.parse(savedMetrics);
|
|
48
|
-
}
|
|
49
|
-
return extractMetrics(initialState);
|
|
40
|
+
return savedMetrics ? JSON.parse(savedMetrics) : initialState;
|
|
50
41
|
});
|
|
51
42
|
|
|
52
|
-
const [
|
|
53
|
-
const saved = localStorage.getItem(`${storageKey}-achievements`);
|
|
43
|
+
const [unlockedAchievements, setUnlockedAchievements] = useState<string[]>(() => {
|
|
44
|
+
const saved = localStorage.getItem(`${storageKey}-unlocked-achievements`);
|
|
45
|
+
return saved ? JSON.parse(saved) : [];
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const [newlyUnlockedAchievements, setNewlyUnlockedAchievements] = useState<string[]>(() => {
|
|
49
|
+
const saved = localStorage.getItem(`${storageKey}-newly-unlocked-achievements`);
|
|
54
50
|
return saved ? JSON.parse(saved) : [];
|
|
55
51
|
});
|
|
56
52
|
|
|
@@ -65,20 +61,28 @@ export const AchievementProvider: React.FC<AchievementProviderProps> = ({
|
|
|
65
61
|
Object.entries(config).forEach(([metricKey, conditions]) => {
|
|
66
62
|
const metricValue = metrics[metricKey];
|
|
67
63
|
conditions.forEach(condition => {
|
|
68
|
-
if (condition.check(metricValue) && !
|
|
64
|
+
if (condition.check(metricValue) && !unlockedAchievements.includes(condition.data.id)) {
|
|
69
65
|
newAchievements.push(condition.data);
|
|
70
66
|
}
|
|
71
67
|
});
|
|
72
68
|
});
|
|
73
69
|
|
|
74
70
|
if (newAchievements.length > 0) {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
71
|
+
const newlyUnlockedIds = newAchievements.map(a => a.id);
|
|
72
|
+
setUnlockedAchievements(prev => {
|
|
73
|
+
const updated = [...prev, ...newlyUnlockedIds];
|
|
74
|
+
localStorage.setItem(`${storageKey}-unlocked-achievements`, JSON.stringify(updated));
|
|
75
|
+
return updated;
|
|
76
|
+
});
|
|
77
|
+
setNewlyUnlockedAchievements(prev => {
|
|
78
|
+
const updated = [...prev, ...newlyUnlockedIds];
|
|
79
|
+
localStorage.setItem(`${storageKey}-newly-unlocked-achievements`, JSON.stringify(updated));
|
|
80
|
+
return updated;
|
|
81
|
+
});
|
|
78
82
|
setAchievementQueue(prevQueue => [...prevQueue, ...newAchievements]);
|
|
79
83
|
setShowConfetti(true);
|
|
80
84
|
}
|
|
81
|
-
}, [config, metrics,
|
|
85
|
+
}, [config, metrics, unlockedAchievements, storageKey]);
|
|
82
86
|
|
|
83
87
|
useEffect(() => {
|
|
84
88
|
checkAchievements();
|
|
@@ -86,14 +90,19 @@ export const AchievementProvider: React.FC<AchievementProviderProps> = ({
|
|
|
86
90
|
|
|
87
91
|
useEffect(() => {
|
|
88
92
|
if (achievementQueue.length > 0 && !currentAchievement) {
|
|
89
|
-
|
|
93
|
+
const nextAchievement = achievementQueue[0];
|
|
94
|
+
setCurrentAchievement(nextAchievement);
|
|
90
95
|
setAchievementQueue(prevQueue => prevQueue.slice(1));
|
|
96
|
+
|
|
97
|
+
setNewlyUnlockedAchievements(prev => {
|
|
98
|
+
const updated = prev.filter(id => id !== nextAchievement.id);
|
|
99
|
+
localStorage.setItem(`${storageKey}-newly-unlocked-achievements`, JSON.stringify(updated));
|
|
100
|
+
return updated;
|
|
101
|
+
});
|
|
91
102
|
}
|
|
92
|
-
}, [achievementQueue, currentAchievement]);
|
|
103
|
+
}, [achievementQueue, currentAchievement, storageKey]);
|
|
93
104
|
|
|
94
|
-
const showBadgesModal = () =>
|
|
95
|
-
setShowBadges(true);
|
|
96
|
-
};
|
|
105
|
+
const showBadgesModal = () => setShowBadges(true);
|
|
97
106
|
|
|
98
107
|
const getAchievements = (achievedIds: string[]) => {
|
|
99
108
|
return Object.values(config).flatMap(conditions =>
|
|
@@ -110,7 +119,7 @@ export const AchievementProvider: React.FC<AchievementProviderProps> = ({
|
|
|
110
119
|
return updatedMetrics;
|
|
111
120
|
});
|
|
112
121
|
},
|
|
113
|
-
|
|
122
|
+
unlockedAchievements,
|
|
114
123
|
checkAchievements,
|
|
115
124
|
showBadgesModal
|
|
116
125
|
};
|
|
@@ -119,21 +128,27 @@ export const AchievementProvider: React.FC<AchievementProviderProps> = ({
|
|
|
119
128
|
<AchievementContext.Provider value={contextValue}>
|
|
120
129
|
{children}
|
|
121
130
|
<AchievementModal
|
|
122
|
-
|
|
131
|
+
isOpen={!!currentAchievement}
|
|
123
132
|
achievement={currentAchievement}
|
|
124
133
|
onClose={() => {
|
|
125
134
|
setCurrentAchievement(null);
|
|
126
|
-
if (achievementQueue.length === 0) {
|
|
135
|
+
if (achievementQueue.length === 0 && newlyUnlockedAchievements.length === 0) {
|
|
127
136
|
setShowConfetti(false);
|
|
128
137
|
}
|
|
129
138
|
}}
|
|
139
|
+
styles={mergedStyles.achievementModal}
|
|
130
140
|
/>
|
|
131
141
|
<BadgesModal
|
|
132
|
-
|
|
133
|
-
achievements={getAchievements(
|
|
142
|
+
isOpen={showBadges}
|
|
143
|
+
achievements={getAchievements(unlockedAchievements)}
|
|
134
144
|
onClose={() => setShowBadges(false)}
|
|
145
|
+
styles={mergedStyles.badgesModal}
|
|
146
|
+
/>
|
|
147
|
+
<BadgesButton
|
|
148
|
+
onClick={showBadgesModal}
|
|
149
|
+
position={badgesButtonPosition}
|
|
150
|
+
styles={mergedStyles.badgesButton}
|
|
135
151
|
/>
|
|
136
|
-
<BadgesButton onClick={showBadgesModal} position={badgesButtonPosition} />
|
|
137
152
|
<ConfettiWrapper show={showConfetti || achievementQueue.length > 0} />
|
|
138
153
|
</AchievementContext.Provider>
|
|
139
154
|
);
|
|
@@ -145,4 +160,26 @@ export const useAchievement = () => {
|
|
|
145
160
|
throw new Error('useAchievement must be used within an AchievementProvider');
|
|
146
161
|
}
|
|
147
162
|
return context;
|
|
148
|
-
};
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// Helper function to deep merge objects
|
|
166
|
+
function mergeDeep(target: any, source: any) {
|
|
167
|
+
const output = Object.assign({}, target);
|
|
168
|
+
if (isObject(target) && isObject(source)) {
|
|
169
|
+
Object.keys(source).forEach(key => {
|
|
170
|
+
if (isObject(source[key])) {
|
|
171
|
+
if (!(key in target))
|
|
172
|
+
Object.assign(output, { [key]: source[key] });
|
|
173
|
+
else
|
|
174
|
+
output[key] = mergeDeep(target[key], source[key]);
|
|
175
|
+
} else {
|
|
176
|
+
Object.assign(output, { [key]: source[key] });
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return output;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function isObject(item: any) {
|
|
184
|
+
return (item && typeof item === 'object' && !Array.isArray(item));
|
|
185
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
type StyleObject = { [key: string]: string | number };
|
|
2
|
+
|
|
3
|
+
interface ModalStyles {
|
|
4
|
+
overlay: StyleObject;
|
|
5
|
+
content: StyleObject;
|
|
6
|
+
title: StyleObject;
|
|
7
|
+
icon: StyleObject;
|
|
8
|
+
description: StyleObject;
|
|
9
|
+
button: StyleObject;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface BadgesModalStyles {
|
|
13
|
+
overlay: StyleObject;
|
|
14
|
+
content: StyleObject;
|
|
15
|
+
title: StyleObject;
|
|
16
|
+
badgeContainer: StyleObject;
|
|
17
|
+
badge: StyleObject;
|
|
18
|
+
badgeIcon: StyleObject;
|
|
19
|
+
badgeTitle: StyleObject;
|
|
20
|
+
button: StyleObject;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface Styles {
|
|
24
|
+
achievementModal: ModalStyles;
|
|
25
|
+
badgesModal: BadgesModalStyles;
|
|
26
|
+
badgesButton: StyleObject;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const defaultStyles: Styles = {
|
|
30
|
+
achievementModal: {
|
|
31
|
+
overlay: {
|
|
32
|
+
position: 'fixed',
|
|
33
|
+
top: 0,
|
|
34
|
+
left: 0,
|
|
35
|
+
right: 0,
|
|
36
|
+
bottom: 0,
|
|
37
|
+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
38
|
+
display: 'flex',
|
|
39
|
+
alignItems: 'center',
|
|
40
|
+
justifyContent: 'center',
|
|
41
|
+
},
|
|
42
|
+
content: {
|
|
43
|
+
backgroundColor: '#ffffff',
|
|
44
|
+
borderRadius: '8px',
|
|
45
|
+
padding: '20px',
|
|
46
|
+
maxWidth: '400px',
|
|
47
|
+
width: '100%',
|
|
48
|
+
},
|
|
49
|
+
title: {
|
|
50
|
+
fontSize: '24px',
|
|
51
|
+
fontWeight: 'bold',
|
|
52
|
+
marginBottom: '10px',
|
|
53
|
+
},
|
|
54
|
+
icon: {
|
|
55
|
+
width: '50px',
|
|
56
|
+
height: '50px',
|
|
57
|
+
marginBottom: '10px',
|
|
58
|
+
},
|
|
59
|
+
description: {
|
|
60
|
+
fontSize: '16px',
|
|
61
|
+
marginBottom: '20px',
|
|
62
|
+
},
|
|
63
|
+
button: {
|
|
64
|
+
backgroundColor: '#007bff',
|
|
65
|
+
color: '#ffffff',
|
|
66
|
+
padding: '10px 20px',
|
|
67
|
+
borderRadius: '4px',
|
|
68
|
+
border: 'none',
|
|
69
|
+
cursor: 'pointer',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
badgesModal: {
|
|
73
|
+
overlay: {
|
|
74
|
+
position: 'fixed',
|
|
75
|
+
top: 0,
|
|
76
|
+
left: 0,
|
|
77
|
+
right: 0,
|
|
78
|
+
bottom: 0,
|
|
79
|
+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
80
|
+
display: 'flex',
|
|
81
|
+
alignItems: 'center',
|
|
82
|
+
justifyContent: 'center',
|
|
83
|
+
},
|
|
84
|
+
content: {
|
|
85
|
+
backgroundColor: '#ffffff',
|
|
86
|
+
borderRadius: '8px',
|
|
87
|
+
padding: '20px',
|
|
88
|
+
maxWidth: '600px',
|
|
89
|
+
width: '100%',
|
|
90
|
+
maxHeight: '80vh',
|
|
91
|
+
overflowY: 'auto',
|
|
92
|
+
},
|
|
93
|
+
title: {
|
|
94
|
+
fontSize: '24px',
|
|
95
|
+
fontWeight: 'bold',
|
|
96
|
+
marginBottom: '20px',
|
|
97
|
+
},
|
|
98
|
+
badgeContainer: {
|
|
99
|
+
display: 'flex',
|
|
100
|
+
flexWrap: 'wrap',
|
|
101
|
+
justifyContent: 'center',
|
|
102
|
+
},
|
|
103
|
+
badge: {
|
|
104
|
+
display: 'flex',
|
|
105
|
+
flexDirection: 'column',
|
|
106
|
+
alignItems: 'center',
|
|
107
|
+
margin: '10px',
|
|
108
|
+
},
|
|
109
|
+
badgeIcon: {
|
|
110
|
+
width: '50px',
|
|
111
|
+
height: '50px',
|
|
112
|
+
marginBottom: '5px',
|
|
113
|
+
},
|
|
114
|
+
badgeTitle: {
|
|
115
|
+
fontSize: '14px',
|
|
116
|
+
textAlign: 'center',
|
|
117
|
+
},
|
|
118
|
+
button: {
|
|
119
|
+
backgroundColor: '#007bff',
|
|
120
|
+
color: '#ffffff',
|
|
121
|
+
padding: '10px 20px',
|
|
122
|
+
borderRadius: '4px',
|
|
123
|
+
border: 'none',
|
|
124
|
+
cursor: 'pointer',
|
|
125
|
+
marginTop: '20px',
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
badgesButton: {
|
|
129
|
+
position: 'fixed',
|
|
130
|
+
padding: '10px 20px',
|
|
131
|
+
backgroundColor: '#007bff',
|
|
132
|
+
color: '#ffffff',
|
|
133
|
+
border: 'none',
|
|
134
|
+
borderRadius: '4px',
|
|
135
|
+
cursor: 'pointer',
|
|
136
|
+
zIndex: 1000,
|
|
137
|
+
},
|
|
138
|
+
};
|
package/src/types.ts
CHANGED
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
export type MetricValueItem = number | boolean | string | any;
|
|
2
|
-
|
|
3
|
-
export type MetricValue = MetricValueItem[];
|
|
4
|
-
|
|
5
|
-
export interface Metrics {
|
|
6
|
-
[key: string]: MetricValue;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
1
|
export interface AchievementData {
|
|
10
2
|
id: string;
|
|
11
3
|
title: string;
|
|
@@ -13,11 +5,17 @@ export interface AchievementData {
|
|
|
13
5
|
icon: string;
|
|
14
6
|
}
|
|
15
7
|
|
|
8
|
+
export type MetricValue = number | string | boolean | Date;
|
|
9
|
+
|
|
10
|
+
export interface Metrics {
|
|
11
|
+
[key: string]: MetricValue[];
|
|
12
|
+
}
|
|
13
|
+
|
|
16
14
|
export interface AchievementCondition {
|
|
17
|
-
check: (
|
|
15
|
+
check: (value: MetricValue[]) => boolean;
|
|
18
16
|
data: AchievementData;
|
|
19
17
|
}
|
|
20
18
|
|
|
21
19
|
export interface AchievementConfig {
|
|
22
|
-
[
|
|
20
|
+
[key: string]: AchievementCondition[];
|
|
23
21
|
}
|