react-achievements 2.1.0 → 2.1.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/dist/hooks/useAchievement.d.ts +1 -1
- package/dist/index.cjs.js +268 -230
- package/dist/index.esm.js +272 -234
- package/dist/providers/AchievementProvider.d.ts +4 -4
- package/dist/redux/achievementSlice.d.ts +4 -4
- package/dist/types.d.ts +8 -0
- package/package.json +10 -11
- package/rollup.config.mjs +6 -0
- package/src/providers/AchievementProvider.tsx +148 -124
- package/src/redux/achievementSlice.ts +7 -2
- package/src/types.ts +10 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { AchievementProviderProps, AchievementMetrics
|
|
2
|
+
import { AchievementProviderProps, AchievementMetrics } from '../types';
|
|
3
3
|
export interface AchievementContextType {
|
|
4
|
-
updateMetrics: (newMetrics:
|
|
4
|
+
updateMetrics: (newMetrics: AchievementMetrics | ((prevMetrics: AchievementMetrics) => AchievementMetrics)) => void;
|
|
5
5
|
unlockedAchievements: string[];
|
|
6
6
|
resetStorage: () => void;
|
|
7
7
|
}
|
|
8
8
|
export declare const AchievementContext: React.Context<AchievementContextType | undefined>;
|
|
9
9
|
export declare const useAchievementContext: () => AchievementContextType;
|
|
10
|
-
|
|
11
|
-
export
|
|
10
|
+
declare const AchievementProvider: React.FC<AchievementProviderProps>;
|
|
11
|
+
export { AchievementProvider };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { PayloadAction } from '@reduxjs/toolkit';
|
|
2
|
-
import {
|
|
2
|
+
import { InitialAchievementMetrics, AchievementMetrics, SerializedAchievementConfiguration } from '../types';
|
|
3
3
|
export interface AchievementState {
|
|
4
|
-
config:
|
|
4
|
+
config: SerializedAchievementConfiguration;
|
|
5
5
|
metrics: AchievementMetrics;
|
|
6
6
|
unlockedAchievements: string[];
|
|
7
7
|
previouslyAwardedAchievements: string[];
|
|
@@ -9,7 +9,7 @@ export interface AchievementState {
|
|
|
9
9
|
}
|
|
10
10
|
export declare const achievementSlice: import("@reduxjs/toolkit").Slice<AchievementState, {
|
|
11
11
|
initialize: (state: import("immer/dist/internal").WritableDraft<AchievementState>, action: PayloadAction<{
|
|
12
|
-
config:
|
|
12
|
+
config: SerializedAchievementConfiguration;
|
|
13
13
|
initialState?: InitialAchievementMetrics & {
|
|
14
14
|
previouslyAwardedAchievements?: string[];
|
|
15
15
|
};
|
|
@@ -21,7 +21,7 @@ export declare const achievementSlice: import("@reduxjs/toolkit").Slice<Achievem
|
|
|
21
21
|
resetAchievements: (state: import("immer/dist/internal").WritableDraft<AchievementState>) => void;
|
|
22
22
|
}, "achievements">;
|
|
23
23
|
export declare const initialize: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
|
|
24
|
-
config:
|
|
24
|
+
config: SerializedAchievementConfiguration;
|
|
25
25
|
initialState?: InitialAchievementMetrics & {
|
|
26
26
|
previouslyAwardedAchievements?: string[];
|
|
27
27
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -27,3 +27,11 @@ export interface AchievementUnlockCondition<T extends AchievementMetricValue> {
|
|
|
27
27
|
isConditionMet: (value: T) => boolean;
|
|
28
28
|
achievementDetails: AchievementDetails;
|
|
29
29
|
}
|
|
30
|
+
export interface SerializedAchievementUnlockCondition {
|
|
31
|
+
achievementDetails: AchievementDetails;
|
|
32
|
+
conditionType: 'number' | 'string' | 'boolean' | 'date';
|
|
33
|
+
conditionValue: any;
|
|
34
|
+
}
|
|
35
|
+
export interface SerializedAchievementConfiguration {
|
|
36
|
+
[metricName: string]: SerializedAchievementUnlockCondition[];
|
|
37
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-achievements",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "This package allows users to transpose a React achievements engine over their React apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"@mui/icons-material": "^6.4.8",
|
|
27
27
|
"@rollup/plugin-commonjs": "^26.0.1",
|
|
28
28
|
"@rollup/plugin-node-resolve": "^15.2.3",
|
|
29
|
+
"@rollup/plugin-replace": "^6.0.2",
|
|
29
30
|
"@storybook/addon-essentials": "^8.6.8",
|
|
30
31
|
"@storybook/addon-interactions": "^8.6.8",
|
|
31
32
|
"@storybook/addon-links": "^8.6.8",
|
|
@@ -35,23 +36,21 @@
|
|
|
35
36
|
"@storybook/react": "^8.6.8",
|
|
36
37
|
"@storybook/react-webpack5": "^8.6.8",
|
|
37
38
|
"@storybook/test": "^8.6.8",
|
|
38
|
-
"rollup": "^4.19.0",
|
|
39
|
-
"rollup-plugin-typescript2": "^0.36.0",
|
|
40
|
-
"storybook": "^8.6.8",
|
|
41
|
-
"typescript": "^5.5.4",
|
|
42
39
|
"@types/jest": "^29.5.12",
|
|
43
40
|
"@types/node": "^20.14.12",
|
|
44
41
|
"@types/react": "^18.3.3",
|
|
45
|
-
"@types/react-dom": "^18.3.0"
|
|
42
|
+
"@types/react-dom": "^18.3.0",
|
|
43
|
+
"rollup": "^4.19.0",
|
|
44
|
+
"rollup-plugin-typescript2": "^0.36.0",
|
|
45
|
+
"storybook": "^8.6.8",
|
|
46
|
+
"typescript": "^5.5.4"
|
|
46
47
|
},
|
|
47
48
|
"peerDependencies": {
|
|
49
|
+
"@reduxjs/toolkit": "^1.0.0",
|
|
48
50
|
"react": "^18.0.0",
|
|
51
|
+
"react-confetti": "^6.0.0",
|
|
49
52
|
"react-dom": "^18.0.0",
|
|
50
53
|
"react-redux": "^8.0.0 || ^9.0.0",
|
|
51
|
-
"@reduxjs/toolkit": "^1.0.0",
|
|
52
|
-
"react-confetti": "^6.0.0",
|
|
53
54
|
"react-use": "^17.0.0"
|
|
54
|
-
},
|
|
55
|
-
"dependencies": {
|
|
56
55
|
}
|
|
57
|
-
}
|
|
56
|
+
}
|
package/rollup.config.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import resolve from '@rollup/plugin-node-resolve';
|
|
2
2
|
import commonjs from '@rollup/plugin-commonjs';
|
|
3
3
|
import typescript from 'rollup-plugin-typescript2';
|
|
4
|
+
import replace from '@rollup/plugin-replace';
|
|
4
5
|
|
|
6
|
+
// Add the replace plugin to handle `this` issues in libraries like @reduxjs/toolkit
|
|
5
7
|
export default {
|
|
6
8
|
input: 'src/index.ts',
|
|
7
9
|
output: [
|
|
@@ -15,6 +17,10 @@ export default {
|
|
|
15
17
|
}
|
|
16
18
|
],
|
|
17
19
|
plugins: [
|
|
20
|
+
replace({
|
|
21
|
+
preventAssignment: true,
|
|
22
|
+
'this': 'undefined',
|
|
23
|
+
}),
|
|
18
24
|
resolve(),
|
|
19
25
|
commonjs(),
|
|
20
26
|
typescript({ tsconfig: './tsconfig.json' })
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useEffect, useCallback } from 'react';
|
|
1
|
+
import React, { useEffect, useCallback, useState, useMemo } from 'react';
|
|
2
2
|
import { useSelector, useDispatch } from 'react-redux';
|
|
3
3
|
import { RootState, AppDispatch } from '../redux/store';
|
|
4
4
|
import { initialize, setMetrics, unlockAchievement, resetAchievements, markAchievementAsAwarded } from '../redux/achievementSlice';
|
|
@@ -6,19 +6,21 @@ import { addNotification, clearNotifications } from '../redux/notificationSlice'
|
|
|
6
6
|
import {
|
|
7
7
|
AchievementDetails,
|
|
8
8
|
AchievementMetricValue,
|
|
9
|
-
|
|
9
|
+
SerializedAchievementUnlockCondition,
|
|
10
|
+
SerializedAchievementConfiguration,
|
|
10
11
|
AchievementProviderProps,
|
|
11
|
-
AchievementMetrics
|
|
12
|
+
AchievementMetrics,
|
|
13
|
+
AchievementConfiguration,
|
|
14
|
+
AchievementUnlockCondition,
|
|
12
15
|
} from '../types';
|
|
13
|
-
import { defaultStyles, Styles } from '../defaultStyles';
|
|
14
16
|
import AchievementModal from '../components/AchievementModal';
|
|
15
|
-
import BadgesModal from '../components/BadgesModal';
|
|
16
17
|
import BadgesButton from '../components/BadgesButton';
|
|
18
|
+
import BadgesModal from '../components/BadgesModal';
|
|
17
19
|
import ConfettiWrapper from '../components/ConfettiWrapper';
|
|
18
|
-
import {
|
|
20
|
+
import { defaultStyles } from '../defaultStyles';
|
|
19
21
|
|
|
20
22
|
export interface AchievementContextType {
|
|
21
|
-
updateMetrics: (newMetrics:
|
|
23
|
+
updateMetrics: (newMetrics: AchievementMetrics | ((prevMetrics: AchievementMetrics) => AchievementMetrics)) => void;
|
|
22
24
|
unlockedAchievements: string[];
|
|
23
25
|
resetStorage: () => void;
|
|
24
26
|
}
|
|
@@ -33,87 +35,105 @@ export const useAchievementContext = () => {
|
|
|
33
35
|
return context;
|
|
34
36
|
};
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
38
|
+
const AchievementProvider: React.FC<AchievementProviderProps> = ({
|
|
39
|
+
children,
|
|
40
|
+
config,
|
|
41
|
+
initialState = {},
|
|
42
|
+
storageKey = 'react-achievements',
|
|
43
|
+
badgesButtonPosition = 'top-right',
|
|
44
|
+
styles = {},
|
|
45
|
+
icons = {},
|
|
46
|
+
}) => {
|
|
45
47
|
const dispatch: AppDispatch = useDispatch();
|
|
46
48
|
const metrics = useSelector((state: RootState) => state.achievements.metrics);
|
|
47
49
|
const unlockedAchievementIds = useSelector((state: RootState) => state.achievements.unlockedAchievements);
|
|
48
|
-
const
|
|
50
|
+
const previouslyAwardedAchievements = useSelector((state: RootState) => state.achievements.previouslyAwardedAchievements);
|
|
49
51
|
const notifications = useSelector((state: RootState) => state.notifications.notifications);
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
const [
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
52
|
+
const [currentAchievement, setCurrentAchievement] = useState<AchievementDetails | null>(null);
|
|
53
|
+
const [showBadges, setShowBadges] = useState(false);
|
|
54
|
+
const [showConfetti, setShowConfetti] = useState(false);
|
|
55
|
+
|
|
56
|
+
const serializeConfig = (config: AchievementConfiguration): SerializedAchievementConfiguration => {
|
|
57
|
+
const serializedConfig: SerializedAchievementConfiguration = {};
|
|
58
|
+
Object.entries(config).forEach(([metricName, conditions]) => {
|
|
59
|
+
serializedConfig[metricName] = (conditions as AchievementUnlockCondition<AchievementMetricValue>[]).map((condition: AchievementUnlockCondition<AchievementMetricValue>) => {
|
|
60
|
+
// Analyze the isConditionMet function to determine type and value
|
|
61
|
+
const funcString = condition.isConditionMet.toString();
|
|
62
|
+
let conditionType: 'number' | 'string' | 'boolean' | 'date';
|
|
63
|
+
let conditionValue: any;
|
|
64
|
+
|
|
65
|
+
if (funcString.includes('typeof value === "number"') || funcString.includes('typeof value === \'number\'')) {
|
|
66
|
+
conditionType = 'number';
|
|
67
|
+
const matches = funcString.match(/value\s*>=?\s*(\d+)/);
|
|
68
|
+
conditionValue = matches ? parseInt(matches[1]) : 0;
|
|
69
|
+
} else if (funcString.includes('typeof value === "string"') || funcString.includes('typeof value === \'string\'')) {
|
|
70
|
+
conditionType = 'string';
|
|
71
|
+
const matches = funcString.match(/value\s*===?\s*['"](.+)['"]/);
|
|
72
|
+
conditionValue = matches ? matches[1] : '';
|
|
73
|
+
} else if (funcString.includes('typeof value === "boolean"') || funcString.includes('typeof value === \'boolean\'')) {
|
|
74
|
+
conditionType = 'boolean';
|
|
75
|
+
conditionValue = funcString.includes('=== true');
|
|
76
|
+
} else if (funcString.includes('instanceof Date')) {
|
|
77
|
+
conditionType = 'date';
|
|
78
|
+
const matches = funcString.match(/new Date\(['"](.+)['"]\)/);
|
|
79
|
+
conditionValue = matches ? matches[1] : new Date().toISOString();
|
|
80
|
+
} else {
|
|
81
|
+
// Default to number type if we can't determine the type
|
|
82
|
+
conditionType = 'number';
|
|
83
|
+
conditionValue = 1;
|
|
84
|
+
}
|
|
64
85
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
86
|
+
return {
|
|
87
|
+
achievementDetails: condition.achievementDetails,
|
|
88
|
+
conditionType,
|
|
89
|
+
conditionValue,
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
return serializedConfig;
|
|
94
|
+
};
|
|
69
95
|
|
|
70
|
-
|
|
71
|
-
dispatch(initialize({ config, initialState, storageKey }));
|
|
72
|
-
}, [dispatch, config, initialState, storageKey]);
|
|
96
|
+
const serializedConfig = useMemo(() => serializeConfig(config), [config]);
|
|
73
97
|
|
|
74
98
|
const checkAchievements = useCallback(() => {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
99
|
+
const newAchievements: AchievementDetails[] = [];
|
|
100
|
+
Object.entries(serializedConfig).forEach(([metricName, conditions]) => {
|
|
101
|
+
const metricValues = metrics[metricName];
|
|
102
|
+
if (!metricValues) return;
|
|
103
|
+
conditions.forEach((condition) => {
|
|
104
|
+
const isConditionMet = (value: AchievementMetricValue) => {
|
|
105
|
+
switch (condition.conditionType) {
|
|
106
|
+
case 'number':
|
|
107
|
+
return typeof value === 'number' && value >= condition.conditionValue;
|
|
108
|
+
case 'string':
|
|
109
|
+
return typeof value === 'string' && value === condition.conditionValue;
|
|
110
|
+
case 'boolean':
|
|
111
|
+
return typeof value === 'boolean' && value === condition.conditionValue;
|
|
112
|
+
case 'date':
|
|
113
|
+
return value instanceof Date &&
|
|
114
|
+
value.getTime() >= new Date(condition.conditionValue).getTime();
|
|
115
|
+
default:
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
if (
|
|
120
|
+
metricValues.some((value: AchievementMetricValue) => isConditionMet(value)) &&
|
|
121
|
+
!unlockedAchievementIds.includes(condition.achievementDetails.achievementId) &&
|
|
122
|
+
!previouslyAwardedAchievements.includes(condition.achievementDetails.achievementId)
|
|
123
|
+
) {
|
|
124
|
+
newAchievements.push(condition.achievementDetails);
|
|
88
125
|
}
|
|
89
|
-
|
|
90
|
-
conditions
|
|
91
|
-
.filter(condition => !previouslyAwardedAchievementIds.includes(condition.achievementDetails.achievementId))
|
|
92
|
-
.forEach((condition) => {
|
|
93
|
-
if (
|
|
94
|
-
metricValues.some((value: AchievementMetricValue) => condition.isConditionMet(value)) &&
|
|
95
|
-
!unlockedAchievementIds.includes(condition.achievementDetails.achievementId)
|
|
96
|
-
) {
|
|
97
|
-
dispatch(unlockAchievement(condition.achievementDetails.achievementId));
|
|
98
|
-
newAchievementsToAward.push(condition.achievementDetails);
|
|
99
|
-
} else if (
|
|
100
|
-
metricValues.some((value: AchievementMetricValue) => condition.isConditionMet(value)) &&
|
|
101
|
-
unlockedAchievementIds.includes(condition.achievementDetails.achievementId) &&
|
|
102
|
-
!previouslyAwardedAchievementIds.includes(condition.achievementDetails.achievementId)
|
|
103
|
-
) {
|
|
104
|
-
newAchievementsToAward.push(condition.achievementDetails);
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
126
|
});
|
|
108
|
-
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
dispatch(
|
|
127
|
+
});
|
|
128
|
+
if (newAchievements.length > 0) {
|
|
129
|
+
newAchievements.forEach((achievement) => {
|
|
130
|
+
dispatch(unlockAchievement(achievement.achievementId));
|
|
112
131
|
dispatch(markAchievementAsAwarded(achievement.achievementId));
|
|
132
|
+
dispatch(addNotification(achievement));
|
|
113
133
|
});
|
|
114
134
|
setShowConfetti(true);
|
|
115
135
|
}
|
|
116
|
-
}, [
|
|
136
|
+
}, [serializedConfig, metrics, unlockedAchievementIds, previouslyAwardedAchievements, dispatch]);
|
|
117
137
|
|
|
118
138
|
useEffect(() => {
|
|
119
139
|
checkAchievements();
|
|
@@ -127,71 +147,75 @@ export const AchievementProvider: React.FC<AchievementProviderProps> = ({
|
|
|
127
147
|
|
|
128
148
|
const showBadgesModal = () => setShowBadges(true);
|
|
129
149
|
|
|
130
|
-
|
|
131
|
-
(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
},
|
|
138
|
-
[config]
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
const unlockedAchievementsDetails = getAchievements(unlockedAchievementIds);
|
|
142
|
-
const previouslyAwardedAchievementsDetails = getAchievements(previouslyAwardedAchievementIds);
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
dispatch(initialize({
|
|
152
|
+
config: serializedConfig,
|
|
153
|
+
initialState,
|
|
154
|
+
storageKey,
|
|
155
|
+
}));
|
|
156
|
+
}, [dispatch, serializedConfig, initialState, storageKey]);
|
|
143
157
|
|
|
144
158
|
return (
|
|
145
|
-
<AchievementContext.Provider
|
|
159
|
+
<AchievementContext.Provider
|
|
160
|
+
value={{
|
|
161
|
+
updateMetrics: (newMetrics) => {
|
|
162
|
+
if (typeof newMetrics === 'function') {
|
|
163
|
+
const currentMetrics = metrics;
|
|
164
|
+
const updatedMetrics = newMetrics(currentMetrics);
|
|
165
|
+
dispatch(setMetrics(updatedMetrics));
|
|
166
|
+
} else {
|
|
167
|
+
dispatch(setMetrics(newMetrics));
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
unlockedAchievements: unlockedAchievementIds,
|
|
171
|
+
resetStorage: () => {
|
|
172
|
+
localStorage.removeItem(storageKey);
|
|
173
|
+
dispatch(resetAchievements());
|
|
174
|
+
},
|
|
175
|
+
}}
|
|
176
|
+
>
|
|
146
177
|
{children}
|
|
178
|
+
<ConfettiWrapper show={showConfetti} />
|
|
147
179
|
<AchievementModal
|
|
148
180
|
isOpen={!!currentAchievement}
|
|
149
181
|
achievement={currentAchievement}
|
|
150
182
|
onClose={() => {
|
|
151
183
|
setCurrentAchievement(null);
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
184
|
+
dispatch(clearNotifications());
|
|
185
|
+
setShowConfetti(false);
|
|
155
186
|
}}
|
|
156
|
-
styles={
|
|
157
|
-
icons={
|
|
158
|
-
/>
|
|
159
|
-
<BadgesModal
|
|
160
|
-
isOpen={showBadges}
|
|
161
|
-
achievements={previouslyAwardedAchievementsDetails}
|
|
162
|
-
onClose={() => setShowBadges(false)}
|
|
163
|
-
styles={mergedStyles.badgesModal}
|
|
164
|
-
icons={mergedIcons}
|
|
187
|
+
styles={styles.achievementModal || defaultStyles.achievementModal}
|
|
188
|
+
icons={icons}
|
|
165
189
|
/>
|
|
166
190
|
<BadgesButton
|
|
167
191
|
onClick={showBadgesModal}
|
|
168
192
|
position={badgesButtonPosition}
|
|
169
|
-
styles={
|
|
170
|
-
unlockedAchievements={
|
|
193
|
+
styles={styles.badgesButton || defaultStyles.badgesButton}
|
|
194
|
+
unlockedAchievements={[...unlockedAchievementIds, ...previouslyAwardedAchievements]
|
|
195
|
+
.filter((id, index, self) => self.indexOf(id) === index) // Remove duplicates
|
|
196
|
+
.map(id => {
|
|
197
|
+
const achievement = Object.values(serializedConfig)
|
|
198
|
+
.flat()
|
|
199
|
+
.find(condition => condition.achievementDetails.achievementId === id);
|
|
200
|
+
return achievement?.achievementDetails;
|
|
201
|
+
}).filter((a): a is AchievementDetails => !!a)}
|
|
202
|
+
/>
|
|
203
|
+
<BadgesModal
|
|
204
|
+
isOpen={showBadges}
|
|
205
|
+
achievements={[...unlockedAchievementIds, ...previouslyAwardedAchievements]
|
|
206
|
+
.filter((id, index, self) => self.indexOf(id) === index) // Remove duplicates
|
|
207
|
+
.map(id => {
|
|
208
|
+
const achievement = Object.values(serializedConfig)
|
|
209
|
+
.flat()
|
|
210
|
+
.find(condition => condition.achievementDetails.achievementId === id);
|
|
211
|
+
return achievement?.achievementDetails;
|
|
212
|
+
}).filter((a): a is AchievementDetails => !!a)}
|
|
213
|
+
onClose={() => setShowBadges(false)}
|
|
214
|
+
styles={styles.badgesModal || defaultStyles.badgesModal}
|
|
215
|
+
icons={icons}
|
|
171
216
|
/>
|
|
172
|
-
<ConfettiWrapper show={showConfetti || notifications.length > 0} />
|
|
173
217
|
</AchievementContext.Provider>
|
|
174
218
|
);
|
|
175
219
|
};
|
|
176
220
|
|
|
177
|
-
|
|
178
|
-
return item && typeof item === 'object' && !Array.isArray(item);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export function mergeDeep(target: any, source: any) {
|
|
182
|
-
const output = { ...target };
|
|
183
|
-
if (isObject(target) && isObject(source)) {
|
|
184
|
-
Object.keys(source).forEach((key) => {
|
|
185
|
-
if (isObject(source[key])) {
|
|
186
|
-
if (!(key in target)) {
|
|
187
|
-
output[key] = source[key];
|
|
188
|
-
} else {
|
|
189
|
-
output[key] = mergeDeep(target[key], source[key]);
|
|
190
|
-
}
|
|
191
|
-
} else {
|
|
192
|
-
output[key] = source[key];
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
return output;
|
|
197
|
-
}
|
|
221
|
+
export { AchievementProvider };
|
|
@@ -4,10 +4,14 @@ import {
|
|
|
4
4
|
AchievementConfiguration,
|
|
5
5
|
InitialAchievementMetrics,
|
|
6
6
|
AchievementMetrics,
|
|
7
|
+
AchievementDetails,
|
|
8
|
+
SerializedAchievementConfiguration,
|
|
7
9
|
} from '../types';
|
|
8
10
|
|
|
11
|
+
|
|
12
|
+
|
|
9
13
|
export interface AchievementState {
|
|
10
|
-
config:
|
|
14
|
+
config: SerializedAchievementConfiguration;
|
|
11
15
|
metrics: AchievementMetrics;
|
|
12
16
|
unlockedAchievements: string[];
|
|
13
17
|
previouslyAwardedAchievements: string[];
|
|
@@ -26,7 +30,8 @@ export const achievementSlice = createSlice({
|
|
|
26
30
|
name: 'achievements',
|
|
27
31
|
initialState,
|
|
28
32
|
reducers: {
|
|
29
|
-
initialize: (state, action: PayloadAction<{ config:
|
|
33
|
+
initialize: (state, action: PayloadAction<{ config: SerializedAchievementConfiguration; initialState?: InitialAchievementMetrics & { previouslyAwardedAchievements?: string[] }; storageKey: string }>) => {
|
|
34
|
+
|
|
30
35
|
state.config = action.payload.config;
|
|
31
36
|
state.storageKey = action.payload.storageKey;
|
|
32
37
|
const storedState = action.payload.storageKey ? localStorage.getItem(action.payload.storageKey) : null;
|
package/src/types.ts
CHANGED
|
@@ -31,4 +31,14 @@ export interface AchievementProviderProps {
|
|
|
31
31
|
export interface AchievementUnlockCondition<T extends AchievementMetricValue> {
|
|
32
32
|
isConditionMet: (value: T) => boolean;
|
|
33
33
|
achievementDetails: AchievementDetails;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SerializedAchievementUnlockCondition {
|
|
37
|
+
achievementDetails: AchievementDetails;
|
|
38
|
+
conditionType: 'number' | 'string' | 'boolean' | 'date';
|
|
39
|
+
conditionValue: any;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SerializedAchievementConfiguration {
|
|
43
|
+
[metricName: string]: SerializedAchievementUnlockCondition[];
|
|
34
44
|
}
|