react-achievements 2.1.0 → 2.2.0
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 +183 -338
- package/dist/defaultStyles.d.ts +0 -9
- package/dist/hooks/useAchievement.d.ts +1 -1
- package/dist/index.cjs.js +290 -262
- package/dist/index.d.ts +3 -2
- package/dist/index.esm.js +293 -265
- package/dist/providers/AchievementProvider.d.ts +5 -4
- package/dist/redux/achievementSlice.d.ts +5 -6
- package/dist/types.d.ts +8 -0
- package/package.json +18 -9
- package/rollup.config.mjs +11 -0
- package/src/defaultStyles.ts +0 -52
- package/src/hooks/useAchievement.ts +8 -11
- package/src/index.ts +14 -8
- package/src/providers/AchievementProvider.tsx +147 -142
- package/src/redux/achievementSlice.ts +68 -45
- package/src/redux/notificationSlice.ts +5 -5
- package/src/redux/store.ts +1 -5
- package/src/types.ts +12 -7
- package/tsconfig.json +3 -1
- package/demo/README.md +0 -8
- package/demo/eslint.config.js +0 -38
- package/demo/index.html +0 -13
- package/demo/package-lock.json +0 -12053
- package/demo/package.json +0 -47
- package/demo/public/vite.svg +0 -1
- package/demo/src/AchievementConfig.ts +0 -37
- package/demo/src/App.css +0 -42
- package/demo/src/App.jsx +0 -89
- package/demo/src/assets/achievements/explorer.webp +0 -0
- package/demo/src/assets/achievements/seaoned_warrior.webp +0 -0
- package/demo/src/assets/achievements/warrior.webp +0 -0
- package/demo/src/assets/react.svg +0 -1
- package/demo/src/index.css +0 -68
- package/demo/src/main.jsx +0 -10
- package/demo/vite.config.js +0 -7
- package/src/components/AchievementModal.tsx +0 -57
- package/src/hooks/useAchievementState.ts +0 -12
|
@@ -1,86 +1,109 @@
|
|
|
1
1
|
// src/redux/achievementSlice.ts
|
|
2
2
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
|
3
3
|
import {
|
|
4
|
-
AchievementConfiguration,
|
|
5
4
|
InitialAchievementMetrics,
|
|
6
5
|
AchievementMetrics,
|
|
6
|
+
AchievementDetails,
|
|
7
|
+
AchievementMetricValue,
|
|
7
8
|
} from '../types';
|
|
8
9
|
|
|
10
|
+
// Helper function to serialize dates
|
|
11
|
+
const serializeValue = (value: AchievementMetricValue): string | number | boolean => {
|
|
12
|
+
if (value instanceof Date) {
|
|
13
|
+
return value.toISOString();
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Helper function to process metrics for storage
|
|
19
|
+
const processMetrics = (metrics: AchievementMetrics): Record<string, (string | number | boolean)[]> => {
|
|
20
|
+
return Object.entries(metrics).reduce((acc, [key, values]) => ({
|
|
21
|
+
...acc,
|
|
22
|
+
[key]: values.map(serializeValue)
|
|
23
|
+
}), {});
|
|
24
|
+
};
|
|
25
|
+
|
|
9
26
|
export interface AchievementState {
|
|
10
|
-
|
|
11
|
-
metrics: AchievementMetrics;
|
|
27
|
+
metrics: Record<string, (string | number | boolean)[]>;
|
|
12
28
|
unlockedAchievements: string[];
|
|
13
|
-
previouslyAwardedAchievements: string[];
|
|
14
29
|
storageKey: string | null;
|
|
30
|
+
pendingNotifications: AchievementDetails[];
|
|
15
31
|
}
|
|
16
32
|
|
|
17
33
|
const initialState: AchievementState = {
|
|
18
|
-
config: {},
|
|
19
34
|
metrics: {},
|
|
20
35
|
unlockedAchievements: [],
|
|
21
|
-
previouslyAwardedAchievements: [], // Initialize as empty
|
|
22
36
|
storageKey: null,
|
|
37
|
+
pendingNotifications: [],
|
|
23
38
|
};
|
|
24
39
|
|
|
25
40
|
export const achievementSlice = createSlice({
|
|
26
41
|
name: 'achievements',
|
|
27
42
|
initialState,
|
|
28
43
|
reducers: {
|
|
29
|
-
initialize: (state, action: PayloadAction<{
|
|
30
|
-
|
|
44
|
+
initialize: (state, action: PayloadAction<{
|
|
45
|
+
initialState?: InitialAchievementMetrics & { unlockedAchievements?: string[] };
|
|
46
|
+
storageKey: string
|
|
47
|
+
}>) => {
|
|
31
48
|
state.storageKey = action.payload.storageKey;
|
|
32
|
-
const storedState = action.payload.storageKey ? localStorage.getItem(action.payload.storageKey) : null;
|
|
33
49
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
} catch (error) {
|
|
47
|
-
console.error('Error parsing stored achievement state:', error);
|
|
48
|
-
state.metrics = initialMetrics;
|
|
49
|
-
state.unlockedAchievements = [];
|
|
50
|
-
state.previouslyAwardedAchievements = initialAwarded;
|
|
50
|
+
// Load from storage first
|
|
51
|
+
if (action.payload.storageKey) {
|
|
52
|
+
const stored = localStorage.getItem(action.payload.storageKey);
|
|
53
|
+
if (stored) {
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(stored);
|
|
56
|
+
state.metrics = parsed.metrics || {};
|
|
57
|
+
state.unlockedAchievements = parsed.unlockedAchievements || [];
|
|
58
|
+
return;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('Error parsing stored achievements:', error);
|
|
61
|
+
}
|
|
51
62
|
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// If no storage or parse error, use initial state
|
|
66
|
+
if (action.payload.initialState) {
|
|
67
|
+
const { unlockedAchievements, ...metrics } = action.payload.initialState;
|
|
68
|
+
state.metrics = Object.entries(metrics).reduce((acc, [key, value]) => ({
|
|
69
|
+
...acc,
|
|
70
|
+
[key]: Array.isArray(value) ? value.map(serializeValue) : [serializeValue(value as AchievementMetricValue)]
|
|
71
|
+
}), {});
|
|
72
|
+
state.unlockedAchievements = unlockedAchievements || [];
|
|
56
73
|
}
|
|
57
74
|
},
|
|
75
|
+
|
|
58
76
|
setMetrics: (state, action: PayloadAction<AchievementMetrics>) => {
|
|
59
|
-
state.metrics = action.payload;
|
|
77
|
+
state.metrics = processMetrics(action.payload);
|
|
60
78
|
if (state.storageKey) {
|
|
61
|
-
localStorage.setItem(state.storageKey, JSON.stringify({
|
|
79
|
+
localStorage.setItem(state.storageKey, JSON.stringify({
|
|
80
|
+
metrics: state.metrics,
|
|
81
|
+
unlockedAchievements: state.unlockedAchievements
|
|
82
|
+
}));
|
|
62
83
|
}
|
|
63
84
|
},
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
85
|
+
|
|
86
|
+
unlockAchievement: (state, action: PayloadAction<AchievementDetails>) => {
|
|
87
|
+
if (!state.unlockedAchievements.includes(action.payload.achievementId)) {
|
|
88
|
+
state.unlockedAchievements.push(action.payload.achievementId);
|
|
89
|
+
state.pendingNotifications.push(action.payload);
|
|
67
90
|
if (state.storageKey) {
|
|
68
|
-
localStorage.setItem(state.storageKey, JSON.stringify({
|
|
91
|
+
localStorage.setItem(state.storageKey, JSON.stringify({
|
|
92
|
+
metrics: state.metrics,
|
|
93
|
+
unlockedAchievements: state.unlockedAchievements
|
|
94
|
+
}));
|
|
69
95
|
}
|
|
70
96
|
}
|
|
71
97
|
},
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (state.storageKey) {
|
|
76
|
-
localStorage.setItem(state.storageKey, JSON.stringify({ achievements: { metrics: state.metrics, unlockedAchievements: state.unlockedAchievements, previouslyAwardedAchievements: state.previouslyAwardedAchievements } }));
|
|
77
|
-
}
|
|
78
|
-
}
|
|
98
|
+
|
|
99
|
+
clearNotifications: (state) => {
|
|
100
|
+
state.pendingNotifications = [];
|
|
79
101
|
},
|
|
102
|
+
|
|
80
103
|
resetAchievements: (state) => {
|
|
81
104
|
state.metrics = {};
|
|
82
105
|
state.unlockedAchievements = [];
|
|
83
|
-
state.
|
|
106
|
+
state.pendingNotifications = [];
|
|
84
107
|
if (state.storageKey) {
|
|
85
108
|
localStorage.removeItem(state.storageKey);
|
|
86
109
|
}
|
|
@@ -88,6 +111,6 @@ export const achievementSlice = createSlice({
|
|
|
88
111
|
},
|
|
89
112
|
});
|
|
90
113
|
|
|
91
|
-
export const { initialize, setMetrics,
|
|
114
|
+
export const { initialize, setMetrics, resetAchievements, unlockAchievement, clearNotifications } = achievementSlice.actions;
|
|
92
115
|
|
|
93
116
|
export default achievementSlice.reducer;
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
|
2
|
-
import { AchievementDetails } from '../types';
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
notifications:
|
|
3
|
+
interface NotificationState {
|
|
4
|
+
notifications: string[];
|
|
6
5
|
}
|
|
7
6
|
|
|
8
7
|
const initialState: NotificationState = {
|
|
9
8
|
notifications: [],
|
|
10
9
|
};
|
|
11
10
|
|
|
12
|
-
const notificationSlice = createSlice({
|
|
11
|
+
export const notificationSlice = createSlice({
|
|
13
12
|
name: 'notifications',
|
|
14
13
|
initialState,
|
|
15
14
|
reducers: {
|
|
16
|
-
addNotification: (state, action: PayloadAction<
|
|
15
|
+
addNotification: (state, action: PayloadAction<string>) => {
|
|
17
16
|
state.notifications.push(action.payload);
|
|
18
17
|
},
|
|
19
18
|
clearNotifications: (state) => {
|
|
@@ -23,4 +22,5 @@ const notificationSlice = createSlice({
|
|
|
23
22
|
});
|
|
24
23
|
|
|
25
24
|
export const { addNotification, clearNotifications } = notificationSlice.actions;
|
|
25
|
+
|
|
26
26
|
export default notificationSlice.reducer;
|
package/src/redux/store.ts
CHANGED
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
import { configureStore } from '@reduxjs/toolkit';
|
|
2
|
-
import achievementReducer from '
|
|
3
|
-
import notificationReducer from '../redux/notificationSlice';
|
|
2
|
+
import achievementReducer from './achievementSlice';
|
|
4
3
|
import { AchievementState } from './achievementSlice';
|
|
5
|
-
import { NotificationState } from './notificationSlice';
|
|
6
4
|
|
|
7
5
|
export interface RootState {
|
|
8
6
|
achievements: AchievementState;
|
|
9
|
-
notifications: NotificationState;
|
|
10
7
|
}
|
|
11
8
|
|
|
12
9
|
const store = configureStore({
|
|
13
10
|
reducer: {
|
|
14
11
|
achievements: achievementReducer,
|
|
15
|
-
notifications: notificationReducer,
|
|
16
12
|
},
|
|
17
13
|
});
|
|
18
14
|
|
package/src/types.ts
CHANGED
|
@@ -2,6 +2,11 @@ import {Styles} from "./defaultStyles";
|
|
|
2
2
|
|
|
3
3
|
export type AchievementMetricValue = number | string | boolean | Date;
|
|
4
4
|
|
|
5
|
+
export interface AchievementState {
|
|
6
|
+
metrics: Record<string, AchievementMetricValue[]>;
|
|
7
|
+
unlockedAchievements: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
5
10
|
export interface AchievementDetails {
|
|
6
11
|
achievementId: string;
|
|
7
12
|
achievementTitle: string;
|
|
@@ -11,8 +16,13 @@ export interface AchievementDetails {
|
|
|
11
16
|
|
|
12
17
|
export type AchievementIconRecord = Record<string, string>;
|
|
13
18
|
|
|
19
|
+
export interface AchievementUnlockCondition {
|
|
20
|
+
isConditionMet: (value: AchievementMetricValue, state?: AchievementState) => boolean;
|
|
21
|
+
achievementDetails: AchievementDetails;
|
|
22
|
+
}
|
|
23
|
+
|
|
14
24
|
export interface AchievementConfiguration {
|
|
15
|
-
[metricName: string]:
|
|
25
|
+
[metricName: string]: AchievementUnlockCondition[];
|
|
16
26
|
}
|
|
17
27
|
|
|
18
28
|
export type InitialAchievementMetrics = Record<string, AchievementMetricValue | AchievementMetricValue[] | undefined>;
|
|
@@ -21,14 +31,9 @@ export type AchievementMetrics = Record<string, AchievementMetricValue[]>;
|
|
|
21
31
|
export interface AchievementProviderProps {
|
|
22
32
|
children: React.ReactNode;
|
|
23
33
|
config: AchievementConfiguration;
|
|
24
|
-
initialState?: InitialAchievementMetrics & { previouslyAwardedAchievements?: string[] };
|
|
34
|
+
initialState?: InitialAchievementMetrics & { previouslyAwardedAchievements?: string[] };
|
|
25
35
|
storageKey?: string;
|
|
26
36
|
badgesButtonPosition?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
27
37
|
styles?: Partial<Styles>;
|
|
28
38
|
icons?: Record<string, string>;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface AchievementUnlockCondition<T extends AchievementMetricValue> {
|
|
32
|
-
isConditionMet: (value: T) => boolean;
|
|
33
|
-
achievementDetails: AchievementDetails;
|
|
34
39
|
}
|
package/tsconfig.json
CHANGED
package/demo/README.md
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
# React + Vite
|
|
2
|
-
|
|
3
|
-
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
|
4
|
-
|
|
5
|
-
Currently, two official plugins are available:
|
|
6
|
-
|
|
7
|
-
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
|
8
|
-
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
package/demo/eslint.config.js
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import js from '@eslint/js'
|
|
2
|
-
import globals from 'globals'
|
|
3
|
-
import react from 'eslint-plugin-react'
|
|
4
|
-
import reactHooks from 'eslint-plugin-react-hooks'
|
|
5
|
-
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
6
|
-
|
|
7
|
-
export default [
|
|
8
|
-
{
|
|
9
|
-
files: ['**/*.{js,jsx}'],
|
|
10
|
-
ignores: ['dist'],
|
|
11
|
-
languageOptions: {
|
|
12
|
-
ecmaVersion: 2020,
|
|
13
|
-
globals: globals.browser,
|
|
14
|
-
parserOptions: {
|
|
15
|
-
ecmaVersion: 'latest',
|
|
16
|
-
ecmaFeatures: { jsx: true },
|
|
17
|
-
sourceType: 'module',
|
|
18
|
-
},
|
|
19
|
-
},
|
|
20
|
-
settings: { react: { version: '18.3' } },
|
|
21
|
-
plugins: {
|
|
22
|
-
react,
|
|
23
|
-
'react-hooks': reactHooks,
|
|
24
|
-
'react-refresh': reactRefresh,
|
|
25
|
-
},
|
|
26
|
-
rules: {
|
|
27
|
-
...js.configs.recommended.rules,
|
|
28
|
-
...react.configs.recommended.rules,
|
|
29
|
-
...react.configs['jsx-runtime'].rules,
|
|
30
|
-
...reactHooks.configs.recommended.rules,
|
|
31
|
-
'react/jsx-no-target-blank': 'off',
|
|
32
|
-
'react-refresh/only-export-components': [
|
|
33
|
-
'warn',
|
|
34
|
-
{ allowConstantExport: true },
|
|
35
|
-
],
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
]
|
package/demo/index.html
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
-
<title>Vite + React</title>
|
|
8
|
-
</head>
|
|
9
|
-
<body>
|
|
10
|
-
<div id="root"></div>
|
|
11
|
-
<script type="module" src="/src/main.jsx"></script>
|
|
12
|
-
</body>
|
|
13
|
-
</html>
|