@umituz/react-native-gamification 1.1.0 → 1.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/LICENSE +2 -0
- package/README.md +2 -0
- package/package.json +3 -1
- package/src/domain/entities/Achievement.ts +52 -0
- package/src/domain/entities/Leaderboard.ts +56 -0
- package/src/domain/entities/Level.ts +2 -0
- package/src/domain/entities/Point.ts +2 -0
- package/src/domain/entities/Progress.ts +2 -0
- package/src/domain/entities/Reward.ts +83 -0
- package/src/domain/entities/Streak.ts +116 -0
- package/src/domain/repositories/IGamificationRepository.ts +2 -0
- package/src/index.ts +36 -0
- package/src/infrastructure/repositories/StorageGamificationRepository.ts +2 -0
- package/src/infrastructure/storage/GamificationStore.ts +2 -0
- package/src/presentation/hooks/useAchievements.ts +2 -0
- package/src/presentation/hooks/useGamification.ts +2 -0
- package/src/presentation/hooks/useLevel.ts +2 -0
- package/src/presentation/hooks/usePoints.ts +2 -0
- package/src/presentation/hooks/useProgress.ts +2 -0
- package/src/presentation/hooks/useRewards.ts +2 -0
- package/src/presentation/hooks/useStreaks.ts +2 -0
package/LICENSE
CHANGED
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-gamification",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Comprehensive gamification system for React Native apps with achievements, points, levels, streaks, leaderboards, rewards, and progress tracking",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -53,3 +53,5 @@
|
|
|
53
53
|
"LICENSE"
|
|
54
54
|
]
|
|
55
55
|
}
|
|
56
|
+
|
|
57
|
+
|
|
@@ -44,3 +44,55 @@ export interface AchievementProgress {
|
|
|
44
44
|
unlocked: boolean;
|
|
45
45
|
}
|
|
46
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
|
+
|
|
@@ -43,3 +43,59 @@ export interface LeaderboardRanking {
|
|
|
43
43
|
belowUsers: number; // Number of users below
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
export type LeaderboardCategory = 'points' | 'achievements' | 'streaks' | 'activity';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Factory function to create leaderboard entry
|
|
50
|
+
*/
|
|
51
|
+
export function createLeaderboardEntry(
|
|
52
|
+
props: Omit<LeaderboardEntry, 'id' | 'createdDate' | 'updatedDate' | 'change'> & { previousRank?: number }
|
|
53
|
+
): LeaderboardEntry {
|
|
54
|
+
const now = new Date().toISOString();
|
|
55
|
+
const change = props.previousRank ? props.previousRank - props.rank : 0;
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
...props,
|
|
59
|
+
id: `entry-${Date.now()}-${Math.random().toString(36).substring(7)}`,
|
|
60
|
+
createdDate: now,
|
|
61
|
+
updatedDate: now,
|
|
62
|
+
metadata: {
|
|
63
|
+
...props.metadata,
|
|
64
|
+
change,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Calculate rank change indicator
|
|
71
|
+
*/
|
|
72
|
+
export function getRankChangeIndicator(change: number): '↑' | '↓' | '=' {
|
|
73
|
+
if (change > 0) return '↑'; // Improved (moved up)
|
|
74
|
+
if (change < 0) return '↓'; // Declined (moved down)
|
|
75
|
+
return '='; // No change
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get top N entries from leaderboard
|
|
80
|
+
*/
|
|
81
|
+
export function getTopEntries(
|
|
82
|
+
leaderboard: Leaderboard,
|
|
83
|
+
limit: number = 10
|
|
84
|
+
): LeaderboardEntry[] {
|
|
85
|
+
return leaderboard.entries
|
|
86
|
+
.sort((a, b) => a.rank - b.rank)
|
|
87
|
+
.slice(0, limit);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Find user's position in leaderboard
|
|
92
|
+
*/
|
|
93
|
+
export function findUserRank(
|
|
94
|
+
leaderboard: Leaderboard,
|
|
95
|
+
userId: string
|
|
96
|
+
): LeaderboardEntry | null {
|
|
97
|
+
return leaderboard.entries.find((entry) => entry.userId === userId) || null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Represents rewards in the gamification system
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import type { PointTransaction } from './Point';
|
|
8
|
+
|
|
7
9
|
export interface Reward {
|
|
8
10
|
id: string;
|
|
9
11
|
userId: string;
|
|
@@ -46,3 +48,84 @@ export interface RewardClaim {
|
|
|
46
48
|
metadata?: Record<string, any>;
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
export type RewardRarity = 'common' | 'uncommon' | 'rare' | 'epic' | 'legendary';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Factory function to create reward
|
|
55
|
+
*/
|
|
56
|
+
export function createReward(
|
|
57
|
+
props: Omit<Reward, 'id' | 'unlocked' | 'claimed' | 'createdDate' | 'updatedDate'>
|
|
58
|
+
): Reward {
|
|
59
|
+
const now = new Date().toISOString();
|
|
60
|
+
return {
|
|
61
|
+
...props,
|
|
62
|
+
id: `reward-${Date.now()}-${Math.random().toString(36).substring(7)}`,
|
|
63
|
+
unlocked: false,
|
|
64
|
+
claimed: false,
|
|
65
|
+
createdDate: now,
|
|
66
|
+
updatedDate: now,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Factory function to create points transaction
|
|
72
|
+
*/
|
|
73
|
+
export function createPointsTransaction(
|
|
74
|
+
userId: string,
|
|
75
|
+
amount: number,
|
|
76
|
+
type: 'earn' | 'spend' | 'bonus',
|
|
77
|
+
source: string,
|
|
78
|
+
description: string
|
|
79
|
+
): PointTransaction {
|
|
80
|
+
const now = new Date().toISOString();
|
|
81
|
+
return {
|
|
82
|
+
id: `transaction-${Date.now()}-${Math.random().toString(36).substring(7)}`,
|
|
83
|
+
userId,
|
|
84
|
+
amount: type === 'spend' ? -Math.abs(amount) : Math.abs(amount),
|
|
85
|
+
source,
|
|
86
|
+
description,
|
|
87
|
+
balance: 0, // Will be calculated by repository
|
|
88
|
+
createdDate: now,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Calculate user level from total experience/points
|
|
94
|
+
*/
|
|
95
|
+
export function calculateLevel(totalPoints: number): number {
|
|
96
|
+
// Simple level formula: level = floor(sqrt(points / 100))
|
|
97
|
+
return Math.floor(Math.sqrt(totalPoints / 100));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Calculate points needed for next level
|
|
102
|
+
*/
|
|
103
|
+
export function calculateNextLevelPoints(currentLevel: number): number {
|
|
104
|
+
const nextLevel = currentLevel + 1;
|
|
105
|
+
return Math.pow(nextLevel, 2) * 100;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Check if reward is expired
|
|
110
|
+
*/
|
|
111
|
+
export function isRewardExpired(reward: Reward): boolean {
|
|
112
|
+
if (!reward.expiresAt) return false;
|
|
113
|
+
return new Date() > new Date(reward.expiresAt);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get points value for rarity
|
|
118
|
+
*/
|
|
119
|
+
export function getPointsForRarity(rarity: RewardRarity): number {
|
|
120
|
+
const pointsMap: Record<RewardRarity, number> = {
|
|
121
|
+
common: 50,
|
|
122
|
+
uncommon: 100,
|
|
123
|
+
rare: 250,
|
|
124
|
+
epic: 500,
|
|
125
|
+
legendary: 1000,
|
|
126
|
+
};
|
|
127
|
+
return pointsMap[rarity];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
|
|
@@ -36,3 +36,119 @@ export interface StreakProgress {
|
|
|
36
36
|
nextMilestone?: number; // Next milestone value
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
export interface StreakMilestone {
|
|
40
|
+
days: number;
|
|
41
|
+
name: string;
|
|
42
|
+
reward: number; // Points awarded
|
|
43
|
+
achieved: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Factory function to create streak entity
|
|
48
|
+
*/
|
|
49
|
+
export function createStreakEntity(userId: string, type: string): Streak {
|
|
50
|
+
const now = new Date().toISOString();
|
|
51
|
+
return {
|
|
52
|
+
id: `streak-${Date.now()}-${Math.random().toString(36).substring(7)}`,
|
|
53
|
+
userId,
|
|
54
|
+
type,
|
|
55
|
+
currentStreak: 0,
|
|
56
|
+
longestStreak: 0,
|
|
57
|
+
lastActivityDate: now,
|
|
58
|
+
isActive: false,
|
|
59
|
+
createdDate: now,
|
|
60
|
+
updatedDate: now,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check if streak should continue (activity within last 24 hours)
|
|
66
|
+
*/
|
|
67
|
+
export function isStreakActive(streak: Streak): boolean {
|
|
68
|
+
const now = new Date();
|
|
69
|
+
const lastActivity = new Date(streak.lastActivityDate);
|
|
70
|
+
const hoursSinceLastActivity = (now.getTime() - lastActivity.getTime()) / (1000 * 60 * 60);
|
|
71
|
+
|
|
72
|
+
return hoursSinceLastActivity <= 24;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Check if streak is broken (no activity for more than 48 hours)
|
|
77
|
+
*/
|
|
78
|
+
export function isStreakBroken(streak: Streak): boolean {
|
|
79
|
+
const now = new Date();
|
|
80
|
+
const lastActivity = new Date(streak.lastActivityDate);
|
|
81
|
+
const hoursSinceLastActivity = (now.getTime() - lastActivity.getTime()) / (1000 * 60 * 60);
|
|
82
|
+
|
|
83
|
+
return hoursSinceLastActivity > 48;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Update streak with new activity
|
|
88
|
+
*/
|
|
89
|
+
export function updateStreakWithActivity(streak: Streak): Streak {
|
|
90
|
+
const now = new Date().toISOString();
|
|
91
|
+
|
|
92
|
+
// Check if streak is broken
|
|
93
|
+
if (isStreakBroken(streak)) {
|
|
94
|
+
return {
|
|
95
|
+
...streak,
|
|
96
|
+
currentStreak: 1,
|
|
97
|
+
longestStreak: Math.max(1, streak.longestStreak),
|
|
98
|
+
lastActivityDate: now,
|
|
99
|
+
isActive: true,
|
|
100
|
+
updatedDate: now,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Continue streak
|
|
105
|
+
const newStreak = streak.currentStreak + 1;
|
|
106
|
+
const newLongestStreak = Math.max(newStreak, streak.longestStreak);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
...streak,
|
|
110
|
+
currentStreak: newStreak,
|
|
111
|
+
longestStreak: newLongestStreak,
|
|
112
|
+
lastActivityDate: now,
|
|
113
|
+
isActive: true,
|
|
114
|
+
updatedDate: now,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get streak milestones
|
|
120
|
+
*/
|
|
121
|
+
export function getStreakMilestones(currentStreak: number): StreakMilestone[] {
|
|
122
|
+
const milestones: StreakMilestone[] = [
|
|
123
|
+
{ days: 7, name: 'Week Warrior', reward: 50, achieved: currentStreak >= 7 },
|
|
124
|
+
{ days: 14, name: 'Fortnight Fighter', reward: 100, achieved: currentStreak >= 14 },
|
|
125
|
+
{ days: 30, name: 'Monthly Master', reward: 250, achieved: currentStreak >= 30 },
|
|
126
|
+
{ days: 60, name: 'Consistency Champion', reward: 500, achieved: currentStreak >= 60 },
|
|
127
|
+
{ days: 100, name: 'Century Star', reward: 1000, achieved: currentStreak >= 100 },
|
|
128
|
+
{ days: 365, name: 'Yearly Legend', reward: 5000, achieved: currentStreak >= 365 },
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
return milestones;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get next milestone for motivation
|
|
136
|
+
*/
|
|
137
|
+
export function getNextMilestone(currentStreak: number): StreakMilestone | null {
|
|
138
|
+
const milestones = getStreakMilestones(currentStreak);
|
|
139
|
+
return milestones.find((m) => !m.achieved) || null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Calculate days until streak break
|
|
144
|
+
*/
|
|
145
|
+
export function getDaysUntilStreakBreak(streak: Streak): number {
|
|
146
|
+
const now = new Date();
|
|
147
|
+
const lastActivity = new Date(streak.lastActivityDate);
|
|
148
|
+
const hoursRemaining = 48 - (now.getTime() - lastActivity.getTime()) / (1000 * 60 * 60);
|
|
149
|
+
|
|
150
|
+
return Math.max(0, Math.ceil(hoursRemaining / 24));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,14 @@ export type {
|
|
|
15
15
|
Achievement,
|
|
16
16
|
AchievementDefinition,
|
|
17
17
|
AchievementProgress,
|
|
18
|
+
AchievementDifficulty,
|
|
19
|
+
AchievementCategory,
|
|
20
|
+
} from "./domain/entities/Achievement";
|
|
21
|
+
export {
|
|
22
|
+
createAchievement,
|
|
23
|
+
isAchievementComplete,
|
|
24
|
+
calculateAchievementProgress,
|
|
25
|
+
getPointsForDifficulty,
|
|
18
26
|
} from "./domain/entities/Achievement";
|
|
19
27
|
|
|
20
28
|
export type {
|
|
@@ -33,18 +41,44 @@ export type {
|
|
|
33
41
|
Streak,
|
|
34
42
|
StreakDefinition,
|
|
35
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,
|
|
36
54
|
} from "./domain/entities/Streak";
|
|
37
55
|
|
|
38
56
|
export type {
|
|
39
57
|
Leaderboard,
|
|
40
58
|
LeaderboardEntry,
|
|
41
59
|
LeaderboardRanking,
|
|
60
|
+
LeaderboardCategory,
|
|
61
|
+
} from "./domain/entities/Leaderboard";
|
|
62
|
+
export {
|
|
63
|
+
createLeaderboardEntry,
|
|
64
|
+
getRankChangeIndicator,
|
|
65
|
+
getTopEntries,
|
|
66
|
+
findUserRank,
|
|
42
67
|
} from "./domain/entities/Leaderboard";
|
|
43
68
|
|
|
44
69
|
export type {
|
|
45
70
|
Reward,
|
|
46
71
|
RewardDefinition,
|
|
47
72
|
RewardClaim,
|
|
73
|
+
RewardRarity,
|
|
74
|
+
} from "./domain/entities/Reward";
|
|
75
|
+
export {
|
|
76
|
+
createReward,
|
|
77
|
+
createPointsTransaction,
|
|
78
|
+
calculateLevel,
|
|
79
|
+
calculateNextLevelPoints,
|
|
80
|
+
isRewardExpired,
|
|
81
|
+
getPointsForRarity,
|
|
48
82
|
} from "./domain/entities/Reward";
|
|
49
83
|
|
|
50
84
|
export type {
|
|
@@ -87,3 +121,5 @@ export { useStreaks } from "./presentation/hooks/useStreaks";
|
|
|
87
121
|
export { useRewards } from "./presentation/hooks/useRewards";
|
|
88
122
|
export { useProgress } from "./presentation/hooks/useProgress";
|
|
89
123
|
|
|
124
|
+
|
|
125
|
+
|