@umituz/react-native-gamification 1.2.0 → 1.3.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/LICENSE +5 -0
- package/README.md +5 -0
- package/package.json +8 -14
- package/src/components/AchievementCard.tsx +142 -0
- package/src/components/AchievementToast.tsx +122 -0
- package/src/components/LevelProgress.tsx +129 -0
- package/src/components/PointsBadge.tsx +60 -0
- package/src/components/StreakDisplay.tsx +119 -0
- package/src/components/index.ts +10 -0
- package/src/hooks/useGamification.ts +91 -0
- package/src/index.ts +35 -111
- package/src/store/gamificationStore.ts +167 -0
- package/src/types/index.ts +103 -0
- package/src/utils/calculations.ts +85 -0
- package/src/domain/entities/Achievement.ts +0 -98
- package/src/domain/entities/Leaderboard.ts +0 -101
- package/src/domain/entities/Level.ts +0 -40
- package/src/domain/entities/Point.ts +0 -40
- package/src/domain/entities/Progress.ts +0 -43
- package/src/domain/entities/Reward.ts +0 -131
- package/src/domain/entities/Streak.ts +0 -154
- package/src/domain/repositories/IGamificationRepository.ts +0 -235
- package/src/infrastructure/repositories/StorageGamificationRepository.ts +0 -734
- package/src/infrastructure/storage/GamificationStore.ts +0 -350
- package/src/presentation/hooks/useAchievements.ts +0 -37
- package/src/presentation/hooks/useGamification.ts +0 -93
- package/src/presentation/hooks/useLevel.ts +0 -39
- package/src/presentation/hooks/usePoints.ts +0 -36
- package/src/presentation/hooks/useProgress.ts +0 -33
- package/src/presentation/hooks/useRewards.ts +0 -43
- package/src/presentation/hooks/useStreaks.ts +0 -36
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useGamification Hook
|
|
3
|
+
* Main hook for gamification operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useCallback, useEffect, useMemo } from "react";
|
|
7
|
+
import { useGamificationStore } from "../store/gamificationStore";
|
|
8
|
+
import { calculateLevel } from "../utils/calculations";
|
|
9
|
+
import type { GamificationConfig, LevelState, Achievement } from "../types";
|
|
10
|
+
|
|
11
|
+
export interface UseGamificationReturn {
|
|
12
|
+
// State
|
|
13
|
+
points: number;
|
|
14
|
+
totalTasksCompleted: number;
|
|
15
|
+
level: LevelState;
|
|
16
|
+
streak: { current: number; longest: number };
|
|
17
|
+
achievements: Achievement[];
|
|
18
|
+
isLoading: boolean;
|
|
19
|
+
isInitialized: boolean;
|
|
20
|
+
|
|
21
|
+
// Actions
|
|
22
|
+
initialize: (config: GamificationConfig) => Promise<void>;
|
|
23
|
+
completeTask: () => void;
|
|
24
|
+
addPoints: (amount: number) => void;
|
|
25
|
+
reset: () => Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const useGamification = (
|
|
29
|
+
config?: GamificationConfig
|
|
30
|
+
): UseGamificationReturn => {
|
|
31
|
+
const store = useGamificationStore();
|
|
32
|
+
|
|
33
|
+
// Auto-initialize if config provided
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (config && !store.isInitialized) {
|
|
36
|
+
store.initialize(config);
|
|
37
|
+
}
|
|
38
|
+
}, [config, store.isInitialized]);
|
|
39
|
+
|
|
40
|
+
// Calculate level from config
|
|
41
|
+
const level = useMemo((): LevelState => {
|
|
42
|
+
if (!config?.levels.length) {
|
|
43
|
+
return {
|
|
44
|
+
currentLevel: 1,
|
|
45
|
+
currentPoints: store.points,
|
|
46
|
+
pointsToNext: 50,
|
|
47
|
+
progress: Math.min(100, (store.points / 50) * 100),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return calculateLevel(store.points, config.levels);
|
|
51
|
+
}, [store.points, config?.levels]);
|
|
52
|
+
|
|
53
|
+
const completeTask = useCallback(() => {
|
|
54
|
+
store.completeTask();
|
|
55
|
+
}, [store.completeTask]);
|
|
56
|
+
|
|
57
|
+
const addPoints = useCallback(
|
|
58
|
+
(amount: number) => {
|
|
59
|
+
store.addPoints(amount);
|
|
60
|
+
},
|
|
61
|
+
[store.addPoints]
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const initialize = useCallback(
|
|
65
|
+
async (cfg: GamificationConfig) => {
|
|
66
|
+
await store.initialize(cfg);
|
|
67
|
+
},
|
|
68
|
+
[store.initialize]
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const reset = useCallback(async () => {
|
|
72
|
+
await store.reset();
|
|
73
|
+
}, [store.reset]);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
points: store.points,
|
|
77
|
+
totalTasksCompleted: store.totalTasksCompleted,
|
|
78
|
+
level,
|
|
79
|
+
streak: {
|
|
80
|
+
current: store.streak.current,
|
|
81
|
+
longest: store.streak.longest,
|
|
82
|
+
},
|
|
83
|
+
achievements: store.achievements,
|
|
84
|
+
isLoading: store.isLoading,
|
|
85
|
+
isInitialized: store.isInitialized,
|
|
86
|
+
initialize,
|
|
87
|
+
completeTask,
|
|
88
|
+
addPoints,
|
|
89
|
+
reset,
|
|
90
|
+
};
|
|
91
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -1,125 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @umituz/react-native-gamification
|
|
2
|
+
* @umituz/react-native-gamification
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* import { useGamification, useAchievements, usePoints } from '@umituz/react-native-gamification';
|
|
4
|
+
* Generic gamification system for React Native apps
|
|
5
|
+
* All text via props - NO hardcoded strings
|
|
6
|
+
* Designed for 100+ apps
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
|
-
//
|
|
11
|
-
// DOMAIN LAYER - Entities
|
|
12
|
-
// =============================================================================
|
|
13
|
-
|
|
9
|
+
// Types
|
|
14
10
|
export type {
|
|
15
|
-
Achievement,
|
|
16
11
|
AchievementDefinition,
|
|
17
|
-
|
|
18
|
-
AchievementDifficulty,
|
|
19
|
-
AchievementCategory,
|
|
20
|
-
} from "./domain/entities/Achievement";
|
|
21
|
-
export {
|
|
22
|
-
createAchievement,
|
|
23
|
-
isAchievementComplete,
|
|
24
|
-
calculateAchievementProgress,
|
|
25
|
-
getPointsForDifficulty,
|
|
26
|
-
} from "./domain/entities/Achievement";
|
|
27
|
-
|
|
28
|
-
export type {
|
|
29
|
-
Point,
|
|
30
|
-
PointBalance,
|
|
31
|
-
PointTransaction,
|
|
32
|
-
} from "./domain/entities/Point";
|
|
33
|
-
|
|
34
|
-
export type {
|
|
35
|
-
Level,
|
|
12
|
+
Achievement,
|
|
36
13
|
LevelDefinition,
|
|
37
|
-
|
|
38
|
-
|
|
14
|
+
LevelState,
|
|
15
|
+
StreakState,
|
|
16
|
+
GamificationConfig,
|
|
17
|
+
GamificationState,
|
|
18
|
+
GamificationActions,
|
|
19
|
+
GamificationStore,
|
|
20
|
+
} from "./types";
|
|
39
21
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
StreakDefinition,
|
|
43
|
-
StreakProgress,
|
|
44
|
-
StreakMilestone,
|
|
45
|
-
} from "./domain/entities/Streak";
|
|
46
|
-
export {
|
|
47
|
-
createStreakEntity,
|
|
48
|
-
isStreakActive,
|
|
49
|
-
isStreakBroken,
|
|
50
|
-
updateStreakWithActivity,
|
|
51
|
-
getStreakMilestones,
|
|
52
|
-
getNextMilestone,
|
|
53
|
-
getDaysUntilStreakBreak,
|
|
54
|
-
} from "./domain/entities/Streak";
|
|
22
|
+
// Store
|
|
23
|
+
export { useGamificationStore } from "./store/gamificationStore";
|
|
55
24
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
LeaderboardEntry,
|
|
59
|
-
LeaderboardRanking,
|
|
60
|
-
LeaderboardCategory,
|
|
61
|
-
} from "./domain/entities/Leaderboard";
|
|
62
|
-
export {
|
|
63
|
-
createLeaderboardEntry,
|
|
64
|
-
getRankChangeIndicator,
|
|
65
|
-
getTopEntries,
|
|
66
|
-
findUserRank,
|
|
67
|
-
} from "./domain/entities/Leaderboard";
|
|
25
|
+
// Hooks
|
|
26
|
+
export { useGamification, type UseGamificationReturn } from "./hooks/useGamification";
|
|
68
27
|
|
|
69
|
-
|
|
70
|
-
Reward,
|
|
71
|
-
RewardDefinition,
|
|
72
|
-
RewardClaim,
|
|
73
|
-
RewardRarity,
|
|
74
|
-
} from "./domain/entities/Reward";
|
|
28
|
+
// Utils
|
|
75
29
|
export {
|
|
76
|
-
createReward,
|
|
77
|
-
createPointsTransaction,
|
|
78
30
|
calculateLevel,
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
export type {
|
|
85
|
-
Progress,
|
|
86
|
-
ProgressUpdate,
|
|
87
|
-
ProgressMilestone,
|
|
88
|
-
} from "./domain/entities/Progress";
|
|
89
|
-
|
|
90
|
-
// =============================================================================
|
|
91
|
-
// DOMAIN LAYER - Repository Interface
|
|
92
|
-
// =============================================================================
|
|
93
|
-
|
|
94
|
-
export type {
|
|
95
|
-
IGamificationRepository,
|
|
96
|
-
GamificationError,
|
|
97
|
-
GamificationResult,
|
|
98
|
-
} from "./domain/repositories/IGamificationRepository";
|
|
99
|
-
|
|
100
|
-
// =============================================================================
|
|
101
|
-
// INFRASTRUCTURE LAYER
|
|
102
|
-
// =============================================================================
|
|
103
|
-
|
|
104
|
-
export { storageGamificationRepository } from "./infrastructure/repositories/StorageGamificationRepository";
|
|
105
|
-
export { useGamificationStore } from "./infrastructure/storage/GamificationStore";
|
|
106
|
-
|
|
107
|
-
// =============================================================================
|
|
108
|
-
// PRESENTATION LAYER - Hooks
|
|
109
|
-
// =============================================================================
|
|
31
|
+
checkAchievementUnlock,
|
|
32
|
+
updateAchievementProgress,
|
|
33
|
+
isStreakActive,
|
|
34
|
+
isSameDay,
|
|
35
|
+
} from "./utils/calculations";
|
|
110
36
|
|
|
37
|
+
// Components
|
|
111
38
|
export {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
39
|
+
LevelProgress,
|
|
40
|
+
type LevelProgressProps,
|
|
41
|
+
PointsBadge,
|
|
42
|
+
type PointsBadgeProps,
|
|
43
|
+
AchievementCard,
|
|
44
|
+
type AchievementCardProps,
|
|
45
|
+
AchievementToast,
|
|
46
|
+
type AchievementToastProps,
|
|
47
|
+
StreakDisplay,
|
|
48
|
+
type StreakDisplayProps,
|
|
49
|
+
} from "./components";
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gamification Store
|
|
3
|
+
* Zustand store with persist middleware
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { create } from "zustand";
|
|
7
|
+
import { persist, createJSONStorage } from "zustand/middleware";
|
|
8
|
+
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
9
|
+
import type {
|
|
10
|
+
GamificationState,
|
|
11
|
+
GamificationActions,
|
|
12
|
+
GamificationConfig,
|
|
13
|
+
Achievement,
|
|
14
|
+
} from "../types";
|
|
15
|
+
import {
|
|
16
|
+
checkAchievementUnlock,
|
|
17
|
+
updateAchievementProgress,
|
|
18
|
+
isStreakActive,
|
|
19
|
+
isSameDay,
|
|
20
|
+
} from "../utils/calculations";
|
|
21
|
+
|
|
22
|
+
const DEFAULT_STATE: GamificationState = {
|
|
23
|
+
points: 0,
|
|
24
|
+
totalTasksCompleted: 0,
|
|
25
|
+
achievements: [],
|
|
26
|
+
streak: {
|
|
27
|
+
current: 0,
|
|
28
|
+
longest: 0,
|
|
29
|
+
lastActivityDate: null,
|
|
30
|
+
},
|
|
31
|
+
isLoading: false,
|
|
32
|
+
isInitialized: false,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
let currentConfig: GamificationConfig | null = null;
|
|
36
|
+
|
|
37
|
+
export const useGamificationStore = create<
|
|
38
|
+
GamificationState & GamificationActions
|
|
39
|
+
>()(
|
|
40
|
+
persist(
|
|
41
|
+
(set, get) => ({
|
|
42
|
+
...DEFAULT_STATE,
|
|
43
|
+
|
|
44
|
+
initialize: async (config: GamificationConfig) => {
|
|
45
|
+
currentConfig = config;
|
|
46
|
+
const state = get();
|
|
47
|
+
|
|
48
|
+
// Initialize achievements from config
|
|
49
|
+
const achievements: Achievement[] = config.achievements.map((def) => ({
|
|
50
|
+
...def,
|
|
51
|
+
isUnlocked: false,
|
|
52
|
+
progress: 0,
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
// Merge with existing unlocked achievements
|
|
56
|
+
const mergedAchievements = achievements.map((ach) => {
|
|
57
|
+
const existing = state.achievements.find((a) => a.id === ach.id);
|
|
58
|
+
if (existing) {
|
|
59
|
+
return { ...ach, isUnlocked: existing.isUnlocked, progress: existing.progress };
|
|
60
|
+
}
|
|
61
|
+
return ach;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
set({ achievements: mergedAchievements, isInitialized: true });
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
addPoints: (amount: number) => {
|
|
68
|
+
set((state) => ({ points: state.points + amount }));
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
completeTask: () => {
|
|
72
|
+
const state = get();
|
|
73
|
+
const pointsToAdd = currentConfig?.pointsPerAction ?? 15;
|
|
74
|
+
|
|
75
|
+
set({
|
|
76
|
+
totalTasksCompleted: state.totalTasksCompleted + 1,
|
|
77
|
+
points: state.points + pointsToAdd,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Update streak
|
|
81
|
+
get().updateStreak();
|
|
82
|
+
|
|
83
|
+
// Check achievements
|
|
84
|
+
get().checkAchievements();
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
updateStreak: () => {
|
|
88
|
+
const state = get();
|
|
89
|
+
const now = new Date();
|
|
90
|
+
const lastDate = state.streak.lastActivityDate
|
|
91
|
+
? new Date(state.streak.lastActivityDate)
|
|
92
|
+
: null;
|
|
93
|
+
|
|
94
|
+
let newStreak = state.streak.current;
|
|
95
|
+
|
|
96
|
+
if (!lastDate || !isSameDay(lastDate, now)) {
|
|
97
|
+
if (isStreakActive(state.streak.lastActivityDate)) {
|
|
98
|
+
newStreak = state.streak.current + 1;
|
|
99
|
+
} else {
|
|
100
|
+
newStreak = 1;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
set({
|
|
105
|
+
streak: {
|
|
106
|
+
current: newStreak,
|
|
107
|
+
longest: Math.max(state.streak.longest, newStreak),
|
|
108
|
+
lastActivityDate: now.toISOString(),
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
checkAchievements: () => {
|
|
114
|
+
if (!currentConfig) return [];
|
|
115
|
+
|
|
116
|
+
const state = get();
|
|
117
|
+
const newlyUnlocked: Achievement[] = [];
|
|
118
|
+
|
|
119
|
+
const updatedAchievements = state.achievements.map((ach) => {
|
|
120
|
+
if (ach.isUnlocked) return ach;
|
|
121
|
+
|
|
122
|
+
const progress = updateAchievementProgress(
|
|
123
|
+
ach,
|
|
124
|
+
state.totalTasksCompleted,
|
|
125
|
+
state.streak.current
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const shouldUnlock = checkAchievementUnlock(
|
|
129
|
+
ach,
|
|
130
|
+
state.totalTasksCompleted,
|
|
131
|
+
state.streak.current
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
if (shouldUnlock && !ach.isUnlocked) {
|
|
135
|
+
const unlocked = {
|
|
136
|
+
...ach,
|
|
137
|
+
isUnlocked: true,
|
|
138
|
+
unlockedAt: new Date().toISOString(),
|
|
139
|
+
progress: 100,
|
|
140
|
+
};
|
|
141
|
+
newlyUnlocked.push(unlocked);
|
|
142
|
+
return unlocked;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { ...ach, progress };
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
set({ achievements: updatedAchievements });
|
|
149
|
+
return newlyUnlocked;
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
reset: async () => {
|
|
153
|
+
set(DEFAULT_STATE);
|
|
154
|
+
},
|
|
155
|
+
}),
|
|
156
|
+
{
|
|
157
|
+
name: "gamification-storage",
|
|
158
|
+
storage: createJSONStorage(() => AsyncStorage),
|
|
159
|
+
partialize: (state) => ({
|
|
160
|
+
points: state.points,
|
|
161
|
+
totalTasksCompleted: state.totalTasksCompleted,
|
|
162
|
+
achievements: state.achievements,
|
|
163
|
+
streak: state.streak,
|
|
164
|
+
}),
|
|
165
|
+
}
|
|
166
|
+
)
|
|
167
|
+
);
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gamification Types
|
|
3
|
+
* Generic types for 100+ apps - NO app-specific code
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Achievement Definition (provided by app)
|
|
7
|
+
export interface AchievementDefinition {
|
|
8
|
+
id: string;
|
|
9
|
+
threshold: number;
|
|
10
|
+
type: "count" | "streak" | "milestone";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Achievement State (internal)
|
|
14
|
+
export interface Achievement extends AchievementDefinition {
|
|
15
|
+
isUnlocked: boolean;
|
|
16
|
+
unlockedAt?: string;
|
|
17
|
+
progress: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Level Definition (provided by app)
|
|
21
|
+
export interface LevelDefinition {
|
|
22
|
+
level: number;
|
|
23
|
+
minPoints: number;
|
|
24
|
+
maxPoints: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Level State (internal)
|
|
28
|
+
export interface LevelState {
|
|
29
|
+
currentLevel: number;
|
|
30
|
+
currentPoints: number;
|
|
31
|
+
pointsToNext: number;
|
|
32
|
+
progress: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Streak State
|
|
36
|
+
export interface StreakState {
|
|
37
|
+
current: number;
|
|
38
|
+
longest: number;
|
|
39
|
+
lastActivityDate: string | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Gamification Config (provided by app via props)
|
|
43
|
+
export interface GamificationConfig {
|
|
44
|
+
storageKey: string;
|
|
45
|
+
achievements: AchievementDefinition[];
|
|
46
|
+
levels: LevelDefinition[];
|
|
47
|
+
pointsPerAction?: number;
|
|
48
|
+
streakBonusMultiplier?: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Store State
|
|
52
|
+
export interface GamificationState {
|
|
53
|
+
points: number;
|
|
54
|
+
totalTasksCompleted: number;
|
|
55
|
+
achievements: Achievement[];
|
|
56
|
+
streak: StreakState;
|
|
57
|
+
isLoading: boolean;
|
|
58
|
+
isInitialized: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Store Actions
|
|
62
|
+
export interface GamificationActions {
|
|
63
|
+
initialize: (config: GamificationConfig) => Promise<void>;
|
|
64
|
+
addPoints: (amount: number) => void;
|
|
65
|
+
completeTask: () => void;
|
|
66
|
+
updateStreak: () => void;
|
|
67
|
+
checkAchievements: () => Achievement[];
|
|
68
|
+
reset: () => Promise<void>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Combined Store
|
|
72
|
+
export type GamificationStore = GamificationState & GamificationActions;
|
|
73
|
+
|
|
74
|
+
// UI Component Props (all text via props - NO hardcoded strings)
|
|
75
|
+
export interface LevelProgressProps {
|
|
76
|
+
level: number;
|
|
77
|
+
points: number;
|
|
78
|
+
levelTitle: string;
|
|
79
|
+
pointsToNext: number;
|
|
80
|
+
progress: number;
|
|
81
|
+
showPoints?: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface AchievementCardProps {
|
|
85
|
+
title: string;
|
|
86
|
+
description: string;
|
|
87
|
+
icon: string;
|
|
88
|
+
isUnlocked: boolean;
|
|
89
|
+
progress: number;
|
|
90
|
+
threshold: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface StreakDisplayProps {
|
|
94
|
+
current: number;
|
|
95
|
+
longest: number;
|
|
96
|
+
streakLabel: string;
|
|
97
|
+
bestLabel: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface PointsBadgeProps {
|
|
101
|
+
points: number;
|
|
102
|
+
label?: string;
|
|
103
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gamification Calculations
|
|
3
|
+
* Pure utility functions - NO side effects
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { LevelDefinition, LevelState, Achievement, AchievementDefinition } from "../types";
|
|
7
|
+
|
|
8
|
+
export const calculateLevel = (
|
|
9
|
+
points: number,
|
|
10
|
+
levels: LevelDefinition[]
|
|
11
|
+
): LevelState => {
|
|
12
|
+
const sortedLevels = [...levels].sort((a, b) => a.minPoints - b.minPoints);
|
|
13
|
+
|
|
14
|
+
let currentLevelDef = sortedLevels[0];
|
|
15
|
+
let nextLevelDef: LevelDefinition | null = null;
|
|
16
|
+
|
|
17
|
+
for (let i = 0; i < sortedLevels.length; i++) {
|
|
18
|
+
if (points >= sortedLevels[i].minPoints) {
|
|
19
|
+
currentLevelDef = sortedLevels[i];
|
|
20
|
+
nextLevelDef = sortedLevels[i + 1] || null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const pointsInLevel = points - currentLevelDef.minPoints;
|
|
25
|
+
const levelRange = nextLevelDef
|
|
26
|
+
? nextLevelDef.minPoints - currentLevelDef.minPoints
|
|
27
|
+
: 100;
|
|
28
|
+
const progress = Math.min(100, (pointsInLevel / levelRange) * 100);
|
|
29
|
+
const pointsToNext = nextLevelDef
|
|
30
|
+
? nextLevelDef.minPoints - points
|
|
31
|
+
: 0;
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
currentLevel: currentLevelDef.level,
|
|
35
|
+
currentPoints: points,
|
|
36
|
+
pointsToNext,
|
|
37
|
+
progress,
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const checkAchievementUnlock = (
|
|
42
|
+
definition: AchievementDefinition,
|
|
43
|
+
tasksCompleted: number,
|
|
44
|
+
currentStreak: number
|
|
45
|
+
): boolean => {
|
|
46
|
+
switch (definition.type) {
|
|
47
|
+
case "count":
|
|
48
|
+
return tasksCompleted >= definition.threshold;
|
|
49
|
+
case "streak":
|
|
50
|
+
return currentStreak >= definition.threshold;
|
|
51
|
+
case "milestone":
|
|
52
|
+
return tasksCompleted >= definition.threshold;
|
|
53
|
+
default:
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const updateAchievementProgress = (
|
|
59
|
+
definition: AchievementDefinition,
|
|
60
|
+
tasksCompleted: number,
|
|
61
|
+
currentStreak: number
|
|
62
|
+
): number => {
|
|
63
|
+
const value = definition.type === "streak" ? currentStreak : tasksCompleted;
|
|
64
|
+
return Math.min(100, (value / definition.threshold) * 100);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const isStreakActive = (lastActivityDate: string | null): boolean => {
|
|
68
|
+
if (!lastActivityDate) return false;
|
|
69
|
+
|
|
70
|
+
const last = new Date(lastActivityDate);
|
|
71
|
+
const now = new Date();
|
|
72
|
+
const diffDays = Math.floor(
|
|
73
|
+
(now.getTime() - last.getTime()) / (1000 * 60 * 60 * 24)
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
return diffDays <= 1;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const isSameDay = (date1: Date, date2: Date): boolean => {
|
|
80
|
+
return (
|
|
81
|
+
date1.getFullYear() === date2.getFullYear() &&
|
|
82
|
+
date1.getMonth() === date2.getMonth() &&
|
|
83
|
+
date1.getDate() === date2.getDate()
|
|
84
|
+
);
|
|
85
|
+
};
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Achievement Entity
|
|
3
|
-
*
|
|
4
|
-
* Represents a user achievement in the gamification system
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export interface Achievement {
|
|
8
|
-
id: string;
|
|
9
|
-
userId: string;
|
|
10
|
-
type: string; // Achievement type identifier (e.g., "first_goal", "streak_7_days")
|
|
11
|
-
title: string;
|
|
12
|
-
description: string;
|
|
13
|
-
icon?: string;
|
|
14
|
-
category?: string; // Category for grouping achievements
|
|
15
|
-
unlocked: boolean;
|
|
16
|
-
unlockedDate?: string;
|
|
17
|
-
progress: number; // Current progress (0-100 or 0-requirement)
|
|
18
|
-
requirement: number; // Required value to unlock
|
|
19
|
-
points?: number; // Points awarded when unlocked
|
|
20
|
-
rarity?: "common" | "rare" | "epic" | "legendary";
|
|
21
|
-
metadata?: Record<string, any>; // Additional metadata
|
|
22
|
-
createdDate: string;
|
|
23
|
-
updatedDate: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface AchievementDefinition {
|
|
27
|
-
type: string;
|
|
28
|
-
title: string;
|
|
29
|
-
description: string;
|
|
30
|
-
icon?: string;
|
|
31
|
-
category?: string;
|
|
32
|
-
requirement: number;
|
|
33
|
-
points?: number;
|
|
34
|
-
rarity?: "common" | "rare" | "epic" | "legendary";
|
|
35
|
-
metadata?: Record<string, any>;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface AchievementProgress {
|
|
39
|
-
achievementId: string;
|
|
40
|
-
userId: string;
|
|
41
|
-
currentValue: number;
|
|
42
|
-
requirement: number;
|
|
43
|
-
progress: number; // Percentage (0-100)
|
|
44
|
-
unlocked: boolean;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export type AchievementDifficulty = 'easy' | 'medium' | 'hard' | 'legendary';
|
|
48
|
-
export type AchievementCategory = 'milestone' | 'challenge' | 'streak' | 'social' | 'special';
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Factory function to create an achievement
|
|
52
|
-
*/
|
|
53
|
-
export function createAchievement(
|
|
54
|
-
props: Omit<Achievement, 'id' | 'unlocked' | 'createdDate' | 'updatedDate'>
|
|
55
|
-
): Achievement {
|
|
56
|
-
const now = new Date().toISOString();
|
|
57
|
-
return {
|
|
58
|
-
...props,
|
|
59
|
-
id: `achievement-${Date.now()}-${Math.random().toString(36).substring(7)}`,
|
|
60
|
-
unlocked: props.progress >= props.requirement,
|
|
61
|
-
createdDate: now,
|
|
62
|
-
updatedDate: now,
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Check if achievement is complete
|
|
68
|
-
*/
|
|
69
|
-
export function isAchievementComplete(achievement: Achievement): boolean {
|
|
70
|
-
return achievement.progress >= achievement.requirement;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Calculate achievement progress percentage
|
|
75
|
-
*/
|
|
76
|
-
export function calculateAchievementProgress(achievement: Achievement): number {
|
|
77
|
-
if (achievement.requirement === 0) return 0;
|
|
78
|
-
return Math.min(100, Math.round((achievement.progress / achievement.requirement) * 100));
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Get points for achievement difficulty/rarity
|
|
83
|
-
*/
|
|
84
|
-
export function getPointsForDifficulty(rarity: Achievement['rarity'] | AchievementDifficulty): number {
|
|
85
|
-
const pointsMap: Record<string, number> = {
|
|
86
|
-
easy: 10,
|
|
87
|
-
medium: 25,
|
|
88
|
-
hard: 50,
|
|
89
|
-
legendary: 100,
|
|
90
|
-
common: 10,
|
|
91
|
-
rare: 25,
|
|
92
|
-
epic: 50,
|
|
93
|
-
};
|
|
94
|
-
return pointsMap[rarity || 'common'] || 10;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|