@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
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Leaderboard Entity
|
|
3
|
-
*
|
|
4
|
-
* Represents leaderboard entries in the gamification system
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export interface LeaderboardEntry {
|
|
8
|
-
id: string;
|
|
9
|
-
userId: string;
|
|
10
|
-
leaderboardId: string; // Leaderboard identifier (e.g., "global", "weekly", "monthly")
|
|
11
|
-
rank: number; // Current rank (1-based)
|
|
12
|
-
score: number; // Score for ranking
|
|
13
|
-
metric: string; // Metric used for ranking (e.g., "points", "experience", "streak")
|
|
14
|
-
displayName?: string; // User's display name
|
|
15
|
-
avatar?: string; // User's avatar URL
|
|
16
|
-
metadata?: Record<string, any>;
|
|
17
|
-
period?: "daily" | "weekly" | "monthly" | "all-time"; // Time period for leaderboard
|
|
18
|
-
periodStart?: string; // Start date of the period
|
|
19
|
-
periodEnd?: string; // End date of the period
|
|
20
|
-
createdDate: string;
|
|
21
|
-
updatedDate: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface Leaderboard {
|
|
25
|
-
id: string;
|
|
26
|
-
name: string;
|
|
27
|
-
description?: string;
|
|
28
|
-
metric: string; // Metric used for ranking
|
|
29
|
-
period?: "daily" | "weekly" | "monthly" | "all-time";
|
|
30
|
-
periodStart?: string;
|
|
31
|
-
periodEnd?: string;
|
|
32
|
-
entries: LeaderboardEntry[];
|
|
33
|
-
totalParticipants: number;
|
|
34
|
-
lastUpdated: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface LeaderboardRanking {
|
|
38
|
-
userId: string;
|
|
39
|
-
rank: number;
|
|
40
|
-
score: number;
|
|
41
|
-
percentile: number; // Percentile rank (0-100)
|
|
42
|
-
aboveUsers: number; // Number of users above
|
|
43
|
-
belowUsers: number; // Number of users below
|
|
44
|
-
}
|
|
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
|
-
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Level Entity
|
|
3
|
-
*
|
|
4
|
-
* Represents user levels in the gamification system
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export interface Level {
|
|
8
|
-
id: string;
|
|
9
|
-
userId: string;
|
|
10
|
-
currentLevel: number;
|
|
11
|
-
currentExperience: number; // Current XP in current level
|
|
12
|
-
totalExperience: number; // Total XP accumulated
|
|
13
|
-
experienceToNextLevel: number; // XP needed for next level
|
|
14
|
-
levelProgress: number; // Percentage progress in current level (0-100)
|
|
15
|
-
metadata?: Record<string, any>;
|
|
16
|
-
createdDate: string;
|
|
17
|
-
updatedDate: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface LevelDefinition {
|
|
21
|
-
level: number;
|
|
22
|
-
experienceRequired: number; // Total XP required to reach this level
|
|
23
|
-
title?: string;
|
|
24
|
-
description?: string;
|
|
25
|
-
rewards?: string[]; // Reward IDs or types
|
|
26
|
-
metadata?: Record<string, any>;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface LevelProgress {
|
|
30
|
-
userId: string;
|
|
31
|
-
currentLevel: number;
|
|
32
|
-
currentExperience: number;
|
|
33
|
-
totalExperience: number;
|
|
34
|
-
experienceToNextLevel: number;
|
|
35
|
-
levelProgress: number;
|
|
36
|
-
canLevelUp: boolean;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Point Entity
|
|
3
|
-
*
|
|
4
|
-
* Represents points in the gamification system
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export interface Point {
|
|
8
|
-
id: string;
|
|
9
|
-
userId: string;
|
|
10
|
-
amount: number;
|
|
11
|
-
source: string; // Source of points (e.g., "achievement", "action", "reward")
|
|
12
|
-
sourceId?: string; // ID of the source (e.g., achievement ID)
|
|
13
|
-
category?: string; // Category for grouping points
|
|
14
|
-
description?: string;
|
|
15
|
-
metadata?: Record<string, any>;
|
|
16
|
-
createdDate: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface PointBalance {
|
|
20
|
-
userId: string;
|
|
21
|
-
total: number;
|
|
22
|
-
byCategory: Record<string, number>; // Points grouped by category
|
|
23
|
-
lastUpdated: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface PointTransaction {
|
|
27
|
-
id: string;
|
|
28
|
-
userId: string;
|
|
29
|
-
amount: number; // Can be negative for deductions
|
|
30
|
-
source: string;
|
|
31
|
-
sourceId?: string;
|
|
32
|
-
category?: string;
|
|
33
|
-
description?: string;
|
|
34
|
-
balance: number; // Balance after this transaction
|
|
35
|
-
metadata?: Record<string, any>;
|
|
36
|
-
createdDate: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Progress Entity
|
|
3
|
-
*
|
|
4
|
-
* Represents user progress tracking in the gamification system
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export interface Progress {
|
|
8
|
-
id: string;
|
|
9
|
-
userId: string;
|
|
10
|
-
metric: string; // Metric identifier (e.g., "goals_completed", "sessions_attended")
|
|
11
|
-
currentValue: number; // Current value
|
|
12
|
-
targetValue?: number; // Target value (optional)
|
|
13
|
-
progress: number; // Percentage progress (0-100)
|
|
14
|
-
unit?: string; // Unit of measurement (e.g., "times", "hours", "days")
|
|
15
|
-
category?: string; // Category for grouping
|
|
16
|
-
period?: "daily" | "weekly" | "monthly" | "all-time"; // Time period
|
|
17
|
-
periodStart?: string; // Start date of the period
|
|
18
|
-
periodEnd?: string; // End date of the period
|
|
19
|
-
metadata?: Record<string, any>;
|
|
20
|
-
createdDate: string;
|
|
21
|
-
updatedDate: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface ProgressUpdate {
|
|
25
|
-
userId: string;
|
|
26
|
-
metric: string;
|
|
27
|
-
increment: number; // Amount to increment
|
|
28
|
-
category?: string;
|
|
29
|
-
period?: "daily" | "weekly" | "monthly" | "all-time";
|
|
30
|
-
metadata?: Record<string, any>;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface ProgressMilestone {
|
|
34
|
-
metric: string;
|
|
35
|
-
value: number;
|
|
36
|
-
title: string;
|
|
37
|
-
description?: string;
|
|
38
|
-
reward?: string; // Reward ID or type
|
|
39
|
-
metadata?: Record<string, any>;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reward Entity
|
|
3
|
-
*
|
|
4
|
-
* Represents rewards in the gamification system
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { PointTransaction } from './Point';
|
|
8
|
-
|
|
9
|
-
export interface Reward {
|
|
10
|
-
id: string;
|
|
11
|
-
userId: string;
|
|
12
|
-
type: string; // Reward type (e.g., "badge", "unlock", "discount", "item")
|
|
13
|
-
title: string;
|
|
14
|
-
description?: string;
|
|
15
|
-
icon?: string;
|
|
16
|
-
category?: string;
|
|
17
|
-
pointsCost?: number; // Points required to claim
|
|
18
|
-
levelRequired?: number; // Level required to claim
|
|
19
|
-
unlocked: boolean;
|
|
20
|
-
unlockedDate?: string;
|
|
21
|
-
claimed: boolean;
|
|
22
|
-
claimedDate?: string;
|
|
23
|
-
expiresAt?: string; // Expiration date for time-limited rewards
|
|
24
|
-
metadata?: Record<string, any>;
|
|
25
|
-
createdDate: string;
|
|
26
|
-
updatedDate: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface RewardDefinition {
|
|
30
|
-
type: string;
|
|
31
|
-
title: string;
|
|
32
|
-
description?: string;
|
|
33
|
-
icon?: string;
|
|
34
|
-
category?: string;
|
|
35
|
-
pointsCost?: number;
|
|
36
|
-
levelRequired?: number;
|
|
37
|
-
rarity?: "common" | "rare" | "epic" | "legendary";
|
|
38
|
-
expiresAt?: string;
|
|
39
|
-
metadata?: Record<string, any>;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface RewardClaim {
|
|
43
|
-
id: string;
|
|
44
|
-
userId: string;
|
|
45
|
-
rewardId: string;
|
|
46
|
-
pointsSpent?: number;
|
|
47
|
-
claimedDate: string;
|
|
48
|
-
metadata?: Record<string, any>;
|
|
49
|
-
}
|
|
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
|
-
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Streak Entity
|
|
3
|
-
*
|
|
4
|
-
* Represents user streaks in the gamification system
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export interface Streak {
|
|
8
|
-
id: string;
|
|
9
|
-
userId: string;
|
|
10
|
-
type: string; // Streak type (e.g., "daily_login", "daily_goal", "weekly_workout")
|
|
11
|
-
currentStreak: number; // Current consecutive days/actions
|
|
12
|
-
longestStreak: number; // Longest streak ever achieved
|
|
13
|
-
lastActivityDate: string; // Last date when streak was maintained
|
|
14
|
-
isActive: boolean; // Whether streak is currently active
|
|
15
|
-
metadata?: Record<string, any>;
|
|
16
|
-
createdDate: string;
|
|
17
|
-
updatedDate: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface StreakDefinition {
|
|
21
|
-
type: string;
|
|
22
|
-
name: string;
|
|
23
|
-
description?: string;
|
|
24
|
-
resetOnMiss: boolean; // Whether streak resets if missed
|
|
25
|
-
timezone?: string; // Timezone for daily streaks
|
|
26
|
-
metadata?: Record<string, any>;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface StreakProgress {
|
|
30
|
-
userId: string;
|
|
31
|
-
type: string;
|
|
32
|
-
currentStreak: number;
|
|
33
|
-
longestStreak: number;
|
|
34
|
-
isActive: boolean;
|
|
35
|
-
daysUntilMilestone?: number; // Days until next milestone (7, 30, 100, etc.)
|
|
36
|
-
nextMilestone?: number; // Next milestone value
|
|
37
|
-
}
|
|
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
|
-
|