react-achievements 1.3.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 +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
|
@@ -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';
|
|
@@ -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,25 +30,14 @@ 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
43
|
const [unlockedAchievements, setUnlockedAchievements] = useState<string[]>(() => {
|
|
@@ -78,16 +69,20 @@ export const AchievementProvider: React.FC<AchievementProviderProps> = ({
|
|
|
78
69
|
|
|
79
70
|
if (newAchievements.length > 0) {
|
|
80
71
|
const newlyUnlockedIds = newAchievements.map(a => a.id);
|
|
81
|
-
setUnlockedAchievements(prev =>
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
+
});
|
|
87
82
|
setAchievementQueue(prevQueue => [...prevQueue, ...newAchievements]);
|
|
88
83
|
setShowConfetti(true);
|
|
89
84
|
}
|
|
90
|
-
}, [config, metrics, unlockedAchievements,
|
|
85
|
+
}, [config, metrics, unlockedAchievements, storageKey]);
|
|
91
86
|
|
|
92
87
|
useEffect(() => {
|
|
93
88
|
checkAchievements();
|
|
@@ -107,9 +102,7 @@ export const AchievementProvider: React.FC<AchievementProviderProps> = ({
|
|
|
107
102
|
}
|
|
108
103
|
}, [achievementQueue, currentAchievement, storageKey]);
|
|
109
104
|
|
|
110
|
-
const showBadgesModal = () =>
|
|
111
|
-
setShowBadges(true);
|
|
112
|
-
};
|
|
105
|
+
const showBadgesModal = () => setShowBadges(true);
|
|
113
106
|
|
|
114
107
|
const getAchievements = (achievedIds: string[]) => {
|
|
115
108
|
return Object.values(config).flatMap(conditions =>
|
|
@@ -131,19 +124,11 @@ export const AchievementProvider: React.FC<AchievementProviderProps> = ({
|
|
|
131
124
|
showBadgesModal
|
|
132
125
|
};
|
|
133
126
|
|
|
134
|
-
useEffect(() => {
|
|
135
|
-
if (newlyUnlockedAchievements.length > 0) {
|
|
136
|
-
const achievementsToShow = getAchievements(newlyUnlockedAchievements);
|
|
137
|
-
setAchievementQueue(achievementsToShow);
|
|
138
|
-
setShowConfetti(true);
|
|
139
|
-
}
|
|
140
|
-
}, []); // Run this effect only on component mount
|
|
141
|
-
|
|
142
127
|
return (
|
|
143
128
|
<AchievementContext.Provider value={contextValue}>
|
|
144
129
|
{children}
|
|
145
130
|
<AchievementModal
|
|
146
|
-
|
|
131
|
+
isOpen={!!currentAchievement}
|
|
147
132
|
achievement={currentAchievement}
|
|
148
133
|
onClose={() => {
|
|
149
134
|
setCurrentAchievement(null);
|
|
@@ -151,13 +136,19 @@ export const AchievementProvider: React.FC<AchievementProviderProps> = ({
|
|
|
151
136
|
setShowConfetti(false);
|
|
152
137
|
}
|
|
153
138
|
}}
|
|
139
|
+
styles={mergedStyles.achievementModal}
|
|
154
140
|
/>
|
|
155
141
|
<BadgesModal
|
|
156
|
-
|
|
142
|
+
isOpen={showBadges}
|
|
157
143
|
achievements={getAchievements(unlockedAchievements)}
|
|
158
144
|
onClose={() => setShowBadges(false)}
|
|
145
|
+
styles={mergedStyles.badgesModal}
|
|
146
|
+
/>
|
|
147
|
+
<BadgesButton
|
|
148
|
+
onClick={showBadgesModal}
|
|
149
|
+
position={badgesButtonPosition}
|
|
150
|
+
styles={mergedStyles.badgesButton}
|
|
159
151
|
/>
|
|
160
|
-
<BadgesButton onClick={showBadgesModal} position={badgesButtonPosition} />
|
|
161
152
|
<ConfettiWrapper show={showConfetti || achievementQueue.length > 0} />
|
|
162
153
|
</AchievementContext.Provider>
|
|
163
154
|
);
|
|
@@ -169,4 +160,26 @@ export const useAchievement = () => {
|
|
|
169
160
|
throw new Error('useAchievement must be used within an AchievementProvider');
|
|
170
161
|
}
|
|
171
162
|
return context;
|
|
172
|
-
};
|
|
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
|
}
|