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.
@@ -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 extractMetrics = (state: Record<string, MetricValue>): Metrics => {
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
- if (savedMetrics) {
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 => [...prev, ...newlyUnlockedIds]);
82
- setNewlyUnlockedAchievements(prev => [...prev, ...newlyUnlockedIds]);
83
-
84
- localStorage.setItem(`${storageKey}-unlocked-achievements`, JSON.stringify([...unlockedAchievements, ...newlyUnlockedIds]));
85
- localStorage.setItem(`${storageKey}-newly-unlocked-achievements`, JSON.stringify([...newlyUnlockedAchievements, ...newlyUnlockedIds]));
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, newlyUnlockedAchievements, storageKey]);
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
- show={!!currentAchievement}
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
- show={showBadges}
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: (metricValue: MetricValue) => boolean;
15
+ check: (value: MetricValue[]) => boolean;
18
16
  data: AchievementData;
19
17
  }
20
18
 
21
19
  export interface AchievementConfig {
22
- [metricKey: string]: AchievementCondition[];
20
+ [key: string]: AchievementCondition[];
23
21
  }