@umituz/react-native-gamification 1.2.1 → 1.4.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/AchievementItem.tsx +182 -0
- package/src/components/AchievementToast.tsx +122 -0
- package/src/components/GamificationScreen.tsx +236 -0
- package/src/components/LevelProgress.tsx +129 -0
- package/src/components/PointsBadge.tsx +60 -0
- package/src/components/StatsCard.tsx +89 -0
- package/src/components/StreakDisplay.tsx +119 -0
- package/src/components/index.ts +13 -0
- package/src/hooks/useGamification.ts +91 -0
- package/src/index.ts +41 -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 -363
- 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,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GamificationScreen Component
|
|
3
|
+
* Full gamification screen - all text via props
|
|
4
|
+
* Generic for 100+ apps - NO hardcoded strings
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import {
|
|
9
|
+
View,
|
|
10
|
+
Text,
|
|
11
|
+
ScrollView,
|
|
12
|
+
StyleSheet,
|
|
13
|
+
type ViewStyle,
|
|
14
|
+
type TextStyle,
|
|
15
|
+
} from "react-native";
|
|
16
|
+
import { LevelProgress, type LevelProgressProps } from "./LevelProgress";
|
|
17
|
+
import { StatsCard, type StatsCardProps } from "./StatsCard";
|
|
18
|
+
import { AchievementItem, type AchievementItemProps } from "./AchievementItem";
|
|
19
|
+
import { StreakDisplay, type StreakDisplayProps } from "./StreakDisplay";
|
|
20
|
+
|
|
21
|
+
export interface GamificationScreenProps {
|
|
22
|
+
// Section titles (all via props)
|
|
23
|
+
title: string;
|
|
24
|
+
statsTitle: string;
|
|
25
|
+
achievementsTitle: string;
|
|
26
|
+
streakTitle: string;
|
|
27
|
+
|
|
28
|
+
// Level data
|
|
29
|
+
levelProps: Omit<LevelProgressProps, "primaryColor" | "backgroundColor" | "textColor" | "subtextColor">;
|
|
30
|
+
|
|
31
|
+
// Stats data
|
|
32
|
+
stats: Array<Omit<StatsCardProps, "accentColor" | "backgroundColor" | "textColor" | "subtextColor">>;
|
|
33
|
+
|
|
34
|
+
// Achievements data
|
|
35
|
+
achievements: Array<
|
|
36
|
+
Omit<AchievementItemProps, "accentColor" | "backgroundColor" | "textColor" | "subtextColor" | "lockedOpacity">
|
|
37
|
+
>;
|
|
38
|
+
|
|
39
|
+
// Streak data (optional)
|
|
40
|
+
streakProps?: Omit<StreakDisplayProps, "primaryColor" | "backgroundColor" | "textColor" | "subtextColor">;
|
|
41
|
+
|
|
42
|
+
// Empty state
|
|
43
|
+
emptyAchievementsText?: string;
|
|
44
|
+
|
|
45
|
+
// Customization
|
|
46
|
+
containerStyle?: ViewStyle;
|
|
47
|
+
headerStyle?: ViewStyle;
|
|
48
|
+
titleStyle?: TextStyle;
|
|
49
|
+
sectionTitleStyle?: TextStyle;
|
|
50
|
+
|
|
51
|
+
// Colors (applied to all child components)
|
|
52
|
+
accentColor?: string;
|
|
53
|
+
backgroundColor?: string;
|
|
54
|
+
cardBackgroundColor?: string;
|
|
55
|
+
textColor?: string;
|
|
56
|
+
subtextColor?: string;
|
|
57
|
+
|
|
58
|
+
// Header component (optional - for back button etc)
|
|
59
|
+
headerComponent?: React.ReactNode;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const GamificationScreen: React.FC<GamificationScreenProps> = ({
|
|
63
|
+
title,
|
|
64
|
+
statsTitle,
|
|
65
|
+
achievementsTitle,
|
|
66
|
+
streakTitle,
|
|
67
|
+
levelProps,
|
|
68
|
+
stats,
|
|
69
|
+
achievements,
|
|
70
|
+
streakProps,
|
|
71
|
+
emptyAchievementsText,
|
|
72
|
+
containerStyle,
|
|
73
|
+
headerStyle,
|
|
74
|
+
titleStyle,
|
|
75
|
+
sectionTitleStyle,
|
|
76
|
+
accentColor = "#FFD700",
|
|
77
|
+
backgroundColor = "#0A0A0A",
|
|
78
|
+
cardBackgroundColor = "#1A1A1A",
|
|
79
|
+
textColor = "#FFFFFF",
|
|
80
|
+
subtextColor = "#888888",
|
|
81
|
+
headerComponent,
|
|
82
|
+
}) => {
|
|
83
|
+
const unlockedAchievements = achievements.filter((a) => a.isUnlocked);
|
|
84
|
+
const lockedAchievements = achievements.filter((a) => !a.isUnlocked);
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<View style={[styles.container, { backgroundColor }, containerStyle]}>
|
|
88
|
+
{headerComponent}
|
|
89
|
+
|
|
90
|
+
<ScrollView
|
|
91
|
+
style={styles.scrollView}
|
|
92
|
+
contentContainerStyle={styles.scrollContent}
|
|
93
|
+
showsVerticalScrollIndicator={false}
|
|
94
|
+
>
|
|
95
|
+
{/* Header */}
|
|
96
|
+
<View style={[styles.header, headerStyle]}>
|
|
97
|
+
<Text style={[styles.title, { color: textColor }, titleStyle]}>
|
|
98
|
+
{title}
|
|
99
|
+
</Text>
|
|
100
|
+
</View>
|
|
101
|
+
|
|
102
|
+
{/* Level Progress */}
|
|
103
|
+
<View style={styles.section}>
|
|
104
|
+
<LevelProgress
|
|
105
|
+
{...levelProps}
|
|
106
|
+
primaryColor={accentColor}
|
|
107
|
+
backgroundColor={cardBackgroundColor}
|
|
108
|
+
textColor={textColor}
|
|
109
|
+
subtextColor={subtextColor}
|
|
110
|
+
/>
|
|
111
|
+
</View>
|
|
112
|
+
|
|
113
|
+
{/* Streak (if provided) */}
|
|
114
|
+
{streakProps && (
|
|
115
|
+
<View style={styles.section}>
|
|
116
|
+
<Text
|
|
117
|
+
style={[styles.sectionTitle, { color: textColor }, sectionTitleStyle]}
|
|
118
|
+
>
|
|
119
|
+
{streakTitle}
|
|
120
|
+
</Text>
|
|
121
|
+
<StreakDisplay
|
|
122
|
+
{...streakProps}
|
|
123
|
+
primaryColor={accentColor}
|
|
124
|
+
backgroundColor={cardBackgroundColor}
|
|
125
|
+
textColor={textColor}
|
|
126
|
+
subtextColor={subtextColor}
|
|
127
|
+
/>
|
|
128
|
+
</View>
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
{/* Stats Grid */}
|
|
132
|
+
{stats.length > 0 && (
|
|
133
|
+
<View style={styles.section}>
|
|
134
|
+
<Text
|
|
135
|
+
style={[styles.sectionTitle, { color: textColor }, sectionTitleStyle]}
|
|
136
|
+
>
|
|
137
|
+
{statsTitle}
|
|
138
|
+
</Text>
|
|
139
|
+
<View style={styles.statsGrid}>
|
|
140
|
+
{stats.map((stat, index) => (
|
|
141
|
+
<StatsCard
|
|
142
|
+
key={index}
|
|
143
|
+
{...stat}
|
|
144
|
+
accentColor={accentColor}
|
|
145
|
+
backgroundColor={cardBackgroundColor}
|
|
146
|
+
textColor={textColor}
|
|
147
|
+
subtextColor={subtextColor}
|
|
148
|
+
/>
|
|
149
|
+
))}
|
|
150
|
+
</View>
|
|
151
|
+
</View>
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
{/* Achievements */}
|
|
155
|
+
<View style={styles.section}>
|
|
156
|
+
<Text
|
|
157
|
+
style={[styles.sectionTitle, { color: textColor }, sectionTitleStyle]}
|
|
158
|
+
>
|
|
159
|
+
{achievementsTitle}
|
|
160
|
+
</Text>
|
|
161
|
+
|
|
162
|
+
{achievements.length === 0 && emptyAchievementsText ? (
|
|
163
|
+
<Text style={[styles.emptyText, { color: subtextColor }]}>
|
|
164
|
+
{emptyAchievementsText}
|
|
165
|
+
</Text>
|
|
166
|
+
) : (
|
|
167
|
+
<>
|
|
168
|
+
{/* Unlocked achievements first */}
|
|
169
|
+
{unlockedAchievements.map((achievement, index) => (
|
|
170
|
+
<AchievementItem
|
|
171
|
+
key={`unlocked-${index}`}
|
|
172
|
+
{...achievement}
|
|
173
|
+
accentColor={accentColor}
|
|
174
|
+
backgroundColor={cardBackgroundColor}
|
|
175
|
+
textColor={textColor}
|
|
176
|
+
subtextColor={subtextColor}
|
|
177
|
+
/>
|
|
178
|
+
))}
|
|
179
|
+
|
|
180
|
+
{/* Locked achievements */}
|
|
181
|
+
{lockedAchievements.map((achievement, index) => (
|
|
182
|
+
<AchievementItem
|
|
183
|
+
key={`locked-${index}`}
|
|
184
|
+
{...achievement}
|
|
185
|
+
accentColor={accentColor}
|
|
186
|
+
backgroundColor={cardBackgroundColor}
|
|
187
|
+
textColor={textColor}
|
|
188
|
+
subtextColor={subtextColor}
|
|
189
|
+
lockedOpacity={0.6}
|
|
190
|
+
/>
|
|
191
|
+
))}
|
|
192
|
+
</>
|
|
193
|
+
)}
|
|
194
|
+
</View>
|
|
195
|
+
</ScrollView>
|
|
196
|
+
</View>
|
|
197
|
+
);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const styles = StyleSheet.create({
|
|
201
|
+
container: {
|
|
202
|
+
flex: 1,
|
|
203
|
+
},
|
|
204
|
+
scrollView: {
|
|
205
|
+
flex: 1,
|
|
206
|
+
},
|
|
207
|
+
scrollContent: {
|
|
208
|
+
padding: 16,
|
|
209
|
+
paddingBottom: 32,
|
|
210
|
+
},
|
|
211
|
+
header: {
|
|
212
|
+
marginBottom: 20,
|
|
213
|
+
},
|
|
214
|
+
title: {
|
|
215
|
+
fontSize: 28,
|
|
216
|
+
fontWeight: "bold",
|
|
217
|
+
},
|
|
218
|
+
section: {
|
|
219
|
+
marginBottom: 24,
|
|
220
|
+
},
|
|
221
|
+
sectionTitle: {
|
|
222
|
+
fontSize: 18,
|
|
223
|
+
fontWeight: "600",
|
|
224
|
+
marginBottom: 12,
|
|
225
|
+
},
|
|
226
|
+
statsGrid: {
|
|
227
|
+
flexDirection: "row",
|
|
228
|
+
flexWrap: "wrap",
|
|
229
|
+
gap: 12,
|
|
230
|
+
},
|
|
231
|
+
emptyText: {
|
|
232
|
+
fontSize: 14,
|
|
233
|
+
textAlign: "center",
|
|
234
|
+
paddingVertical: 20,
|
|
235
|
+
},
|
|
236
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LevelProgress Component
|
|
3
|
+
* Displays level and progress - all text via props
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, Text, StyleSheet, type ViewStyle, type TextStyle } from "react-native";
|
|
8
|
+
|
|
9
|
+
export interface LevelProgressProps {
|
|
10
|
+
level: number;
|
|
11
|
+
points: number;
|
|
12
|
+
levelTitle: string;
|
|
13
|
+
pointsToNext: number;
|
|
14
|
+
progress: number;
|
|
15
|
+
showPoints?: boolean;
|
|
16
|
+
// Customization
|
|
17
|
+
containerStyle?: ViewStyle;
|
|
18
|
+
titleStyle?: TextStyle;
|
|
19
|
+
subtitleStyle?: TextStyle;
|
|
20
|
+
progressBarStyle?: ViewStyle;
|
|
21
|
+
progressFillStyle?: ViewStyle;
|
|
22
|
+
badgeStyle?: ViewStyle;
|
|
23
|
+
badgeTextStyle?: TextStyle;
|
|
24
|
+
// Colors (design system integration)
|
|
25
|
+
primaryColor?: string;
|
|
26
|
+
secondaryColor?: string;
|
|
27
|
+
backgroundColor?: string;
|
|
28
|
+
textColor?: string;
|
|
29
|
+
subtextColor?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const LevelProgress: React.FC<LevelProgressProps> = ({
|
|
33
|
+
level,
|
|
34
|
+
points,
|
|
35
|
+
levelTitle,
|
|
36
|
+
pointsToNext,
|
|
37
|
+
progress,
|
|
38
|
+
showPoints = true,
|
|
39
|
+
containerStyle,
|
|
40
|
+
titleStyle,
|
|
41
|
+
subtitleStyle,
|
|
42
|
+
progressBarStyle,
|
|
43
|
+
progressFillStyle,
|
|
44
|
+
badgeStyle,
|
|
45
|
+
badgeTextStyle,
|
|
46
|
+
primaryColor = "#FFD700",
|
|
47
|
+
secondaryColor = "#2A2A2A",
|
|
48
|
+
backgroundColor = "#1A1A1A",
|
|
49
|
+
textColor = "#FFFFFF",
|
|
50
|
+
subtextColor = "#888888",
|
|
51
|
+
}) => {
|
|
52
|
+
return (
|
|
53
|
+
<View style={[styles.container, { backgroundColor }, containerStyle]}>
|
|
54
|
+
<View style={styles.header}>
|
|
55
|
+
<View style={styles.titleSection}>
|
|
56
|
+
<Text style={[styles.levelTitle, { color: textColor }, titleStyle]}>
|
|
57
|
+
{levelTitle}
|
|
58
|
+
</Text>
|
|
59
|
+
{showPoints && (
|
|
60
|
+
<Text style={[styles.subtitle, { color: subtextColor }, subtitleStyle]}>
|
|
61
|
+
{points} / {points + pointsToNext}
|
|
62
|
+
</Text>
|
|
63
|
+
)}
|
|
64
|
+
</View>
|
|
65
|
+
|
|
66
|
+
<View style={[styles.badge, { backgroundColor: `${primaryColor}20`, borderColor: `${primaryColor}40` }, badgeStyle]}>
|
|
67
|
+
<Text style={[styles.badgeText, { color: primaryColor }, badgeTextStyle]}>
|
|
68
|
+
{level}
|
|
69
|
+
</Text>
|
|
70
|
+
</View>
|
|
71
|
+
</View>
|
|
72
|
+
|
|
73
|
+
<View style={[styles.progressBar, { backgroundColor: secondaryColor }, progressBarStyle]}>
|
|
74
|
+
<View
|
|
75
|
+
style={[
|
|
76
|
+
styles.progressFill,
|
|
77
|
+
{ width: `${Math.min(100, progress)}%`, backgroundColor: primaryColor },
|
|
78
|
+
progressFillStyle,
|
|
79
|
+
]}
|
|
80
|
+
/>
|
|
81
|
+
</View>
|
|
82
|
+
</View>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const styles = StyleSheet.create({
|
|
87
|
+
container: {
|
|
88
|
+
borderRadius: 16,
|
|
89
|
+
padding: 16,
|
|
90
|
+
},
|
|
91
|
+
header: {
|
|
92
|
+
flexDirection: "row",
|
|
93
|
+
justifyContent: "space-between",
|
|
94
|
+
alignItems: "center",
|
|
95
|
+
marginBottom: 12,
|
|
96
|
+
},
|
|
97
|
+
titleSection: {
|
|
98
|
+
flex: 1,
|
|
99
|
+
},
|
|
100
|
+
levelTitle: {
|
|
101
|
+
fontSize: 18,
|
|
102
|
+
fontWeight: "700",
|
|
103
|
+
},
|
|
104
|
+
subtitle: {
|
|
105
|
+
fontSize: 14,
|
|
106
|
+
marginTop: 2,
|
|
107
|
+
},
|
|
108
|
+
badge: {
|
|
109
|
+
width: 48,
|
|
110
|
+
height: 48,
|
|
111
|
+
borderRadius: 24,
|
|
112
|
+
justifyContent: "center",
|
|
113
|
+
alignItems: "center",
|
|
114
|
+
borderWidth: 2,
|
|
115
|
+
},
|
|
116
|
+
badgeText: {
|
|
117
|
+
fontSize: 18,
|
|
118
|
+
fontWeight: "bold",
|
|
119
|
+
},
|
|
120
|
+
progressBar: {
|
|
121
|
+
height: 8,
|
|
122
|
+
borderRadius: 4,
|
|
123
|
+
overflow: "hidden",
|
|
124
|
+
},
|
|
125
|
+
progressFill: {
|
|
126
|
+
height: "100%",
|
|
127
|
+
borderRadius: 4,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PointsBadge Component
|
|
3
|
+
* Displays points with optional icon - all text via props
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, Text, StyleSheet, type ViewStyle, type TextStyle } from "react-native";
|
|
8
|
+
|
|
9
|
+
export interface PointsBadgeProps {
|
|
10
|
+
points: number;
|
|
11
|
+
icon?: React.ReactNode;
|
|
12
|
+
// Customization
|
|
13
|
+
containerStyle?: ViewStyle;
|
|
14
|
+
textStyle?: TextStyle;
|
|
15
|
+
// Colors
|
|
16
|
+
backgroundColor?: string;
|
|
17
|
+
textColor?: string;
|
|
18
|
+
borderColor?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const PointsBadge: React.FC<PointsBadgeProps> = ({
|
|
22
|
+
points,
|
|
23
|
+
icon,
|
|
24
|
+
containerStyle,
|
|
25
|
+
textStyle,
|
|
26
|
+
backgroundColor = "#FFD70020",
|
|
27
|
+
textColor = "#FFD700",
|
|
28
|
+
borderColor = "#FFD70040",
|
|
29
|
+
}) => {
|
|
30
|
+
return (
|
|
31
|
+
<View
|
|
32
|
+
style={[
|
|
33
|
+
styles.container,
|
|
34
|
+
{ backgroundColor, borderColor },
|
|
35
|
+
containerStyle,
|
|
36
|
+
]}
|
|
37
|
+
>
|
|
38
|
+
{icon}
|
|
39
|
+
<Text style={[styles.text, { color: textColor }, textStyle]}>
|
|
40
|
+
{points}
|
|
41
|
+
</Text>
|
|
42
|
+
</View>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const styles = StyleSheet.create({
|
|
47
|
+
container: {
|
|
48
|
+
flexDirection: "row",
|
|
49
|
+
alignItems: "center",
|
|
50
|
+
gap: 6,
|
|
51
|
+
paddingHorizontal: 12,
|
|
52
|
+
paddingVertical: 6,
|
|
53
|
+
borderRadius: 20,
|
|
54
|
+
borderWidth: 1,
|
|
55
|
+
},
|
|
56
|
+
text: {
|
|
57
|
+
fontSize: 16,
|
|
58
|
+
fontWeight: "bold",
|
|
59
|
+
},
|
|
60
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StatsCard Component
|
|
3
|
+
* Displays a stat with icon - all text via props
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, Text, StyleSheet, type ViewStyle, type TextStyle } from "react-native";
|
|
8
|
+
|
|
9
|
+
export interface StatsCardProps {
|
|
10
|
+
value: number;
|
|
11
|
+
label: string;
|
|
12
|
+
icon: React.ReactNode;
|
|
13
|
+
suffix?: string;
|
|
14
|
+
// Customization
|
|
15
|
+
containerStyle?: ViewStyle;
|
|
16
|
+
valueStyle?: TextStyle;
|
|
17
|
+
labelStyle?: TextStyle;
|
|
18
|
+
// Colors
|
|
19
|
+
accentColor?: string;
|
|
20
|
+
backgroundColor?: string;
|
|
21
|
+
textColor?: string;
|
|
22
|
+
subtextColor?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const StatsCard: React.FC<StatsCardProps> = ({
|
|
26
|
+
value,
|
|
27
|
+
label,
|
|
28
|
+
icon,
|
|
29
|
+
suffix,
|
|
30
|
+
containerStyle,
|
|
31
|
+
valueStyle,
|
|
32
|
+
labelStyle,
|
|
33
|
+
accentColor = "#FFD700",
|
|
34
|
+
backgroundColor = "#1A1A1A",
|
|
35
|
+
textColor = "#FFFFFF",
|
|
36
|
+
subtextColor = "#888888",
|
|
37
|
+
}) => {
|
|
38
|
+
return (
|
|
39
|
+
<View style={[styles.container, { backgroundColor }, containerStyle]}>
|
|
40
|
+
<View style={[styles.iconContainer, { backgroundColor: `${accentColor}15` }]}>
|
|
41
|
+
{icon}
|
|
42
|
+
</View>
|
|
43
|
+
<View style={styles.valueRow}>
|
|
44
|
+
<Text style={[styles.value, { color: textColor }, valueStyle]}>
|
|
45
|
+
{value}
|
|
46
|
+
</Text>
|
|
47
|
+
{suffix && (
|
|
48
|
+
<Text style={[styles.suffix, { color: subtextColor }]}>{suffix}</Text>
|
|
49
|
+
)}
|
|
50
|
+
</View>
|
|
51
|
+
<Text style={[styles.label, { color: subtextColor }, labelStyle]}>
|
|
52
|
+
{label}
|
|
53
|
+
</Text>
|
|
54
|
+
</View>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const styles = StyleSheet.create({
|
|
59
|
+
container: {
|
|
60
|
+
flex: 1,
|
|
61
|
+
minWidth: "45%",
|
|
62
|
+
borderRadius: 12,
|
|
63
|
+
padding: 12,
|
|
64
|
+
},
|
|
65
|
+
iconContainer: {
|
|
66
|
+
width: 36,
|
|
67
|
+
height: 36,
|
|
68
|
+
borderRadius: 18,
|
|
69
|
+
alignItems: "center",
|
|
70
|
+
justifyContent: "center",
|
|
71
|
+
marginBottom: 8,
|
|
72
|
+
},
|
|
73
|
+
valueRow: {
|
|
74
|
+
flexDirection: "row",
|
|
75
|
+
alignItems: "baseline",
|
|
76
|
+
gap: 4,
|
|
77
|
+
},
|
|
78
|
+
value: {
|
|
79
|
+
fontSize: 24,
|
|
80
|
+
fontWeight: "bold",
|
|
81
|
+
},
|
|
82
|
+
suffix: {
|
|
83
|
+
fontSize: 12,
|
|
84
|
+
},
|
|
85
|
+
label: {
|
|
86
|
+
fontSize: 12,
|
|
87
|
+
marginTop: 2,
|
|
88
|
+
},
|
|
89
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StreakDisplay Component
|
|
3
|
+
* Displays streak information - all text via props
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, Text, StyleSheet, type ViewStyle, type TextStyle } from "react-native";
|
|
8
|
+
|
|
9
|
+
export interface StreakDisplayProps {
|
|
10
|
+
current: number;
|
|
11
|
+
longest: number;
|
|
12
|
+
currentLabel: string; // e.g., "Current Streak"
|
|
13
|
+
bestLabel: string; // e.g., "Best"
|
|
14
|
+
daysLabel: string; // e.g., "days"
|
|
15
|
+
icon?: React.ReactNode;
|
|
16
|
+
// Customization
|
|
17
|
+
containerStyle?: ViewStyle;
|
|
18
|
+
numberStyle?: TextStyle;
|
|
19
|
+
labelStyle?: TextStyle;
|
|
20
|
+
// Colors
|
|
21
|
+
primaryColor?: string;
|
|
22
|
+
backgroundColor?: string;
|
|
23
|
+
textColor?: string;
|
|
24
|
+
subtextColor?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const StreakDisplay: React.FC<StreakDisplayProps> = ({
|
|
28
|
+
current,
|
|
29
|
+
longest,
|
|
30
|
+
currentLabel,
|
|
31
|
+
bestLabel,
|
|
32
|
+
daysLabel,
|
|
33
|
+
icon,
|
|
34
|
+
containerStyle,
|
|
35
|
+
numberStyle,
|
|
36
|
+
labelStyle,
|
|
37
|
+
primaryColor = "#FF6B35",
|
|
38
|
+
backgroundColor = "#1A1A1A",
|
|
39
|
+
textColor = "#FFFFFF",
|
|
40
|
+
subtextColor = "#888888",
|
|
41
|
+
}) => {
|
|
42
|
+
return (
|
|
43
|
+
<View style={[styles.container, { backgroundColor }, containerStyle]}>
|
|
44
|
+
<View style={styles.mainStreak}>
|
|
45
|
+
{icon && <View style={styles.iconContainer}>{icon}</View>}
|
|
46
|
+
<View style={styles.streakInfo}>
|
|
47
|
+
<Text style={[styles.number, { color: primaryColor }, numberStyle]}>
|
|
48
|
+
{current}
|
|
49
|
+
</Text>
|
|
50
|
+
<Text style={[styles.label, { color: subtextColor }, labelStyle]}>
|
|
51
|
+
{daysLabel}
|
|
52
|
+
</Text>
|
|
53
|
+
</View>
|
|
54
|
+
<Text style={[styles.currentLabel, { color: textColor }]}>
|
|
55
|
+
{currentLabel}
|
|
56
|
+
</Text>
|
|
57
|
+
</View>
|
|
58
|
+
|
|
59
|
+
<View style={[styles.bestStreak, { backgroundColor: `${primaryColor}20` }]}>
|
|
60
|
+
<Text style={[styles.bestLabel, { color: subtextColor }]}>
|
|
61
|
+
{bestLabel}
|
|
62
|
+
</Text>
|
|
63
|
+
<Text style={[styles.bestNumber, { color: primaryColor }]}>
|
|
64
|
+
{longest}
|
|
65
|
+
</Text>
|
|
66
|
+
</View>
|
|
67
|
+
</View>
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const styles = StyleSheet.create({
|
|
72
|
+
container: {
|
|
73
|
+
borderRadius: 16,
|
|
74
|
+
padding: 16,
|
|
75
|
+
flexDirection: "row",
|
|
76
|
+
alignItems: "center",
|
|
77
|
+
justifyContent: "space-between",
|
|
78
|
+
},
|
|
79
|
+
mainStreak: {
|
|
80
|
+
flexDirection: "row",
|
|
81
|
+
alignItems: "center",
|
|
82
|
+
gap: 12,
|
|
83
|
+
},
|
|
84
|
+
iconContainer: {
|
|
85
|
+
width: 40,
|
|
86
|
+
height: 40,
|
|
87
|
+
justifyContent: "center",
|
|
88
|
+
alignItems: "center",
|
|
89
|
+
},
|
|
90
|
+
streakInfo: {
|
|
91
|
+
alignItems: "center",
|
|
92
|
+
},
|
|
93
|
+
number: {
|
|
94
|
+
fontSize: 32,
|
|
95
|
+
fontWeight: "bold",
|
|
96
|
+
},
|
|
97
|
+
label: {
|
|
98
|
+
fontSize: 12,
|
|
99
|
+
textTransform: "uppercase",
|
|
100
|
+
},
|
|
101
|
+
currentLabel: {
|
|
102
|
+
fontSize: 14,
|
|
103
|
+
fontWeight: "500",
|
|
104
|
+
},
|
|
105
|
+
bestStreak: {
|
|
106
|
+
paddingHorizontal: 12,
|
|
107
|
+
paddingVertical: 8,
|
|
108
|
+
borderRadius: 12,
|
|
109
|
+
alignItems: "center",
|
|
110
|
+
},
|
|
111
|
+
bestLabel: {
|
|
112
|
+
fontSize: 11,
|
|
113
|
+
textTransform: "uppercase",
|
|
114
|
+
},
|
|
115
|
+
bestNumber: {
|
|
116
|
+
fontSize: 18,
|
|
117
|
+
fontWeight: "bold",
|
|
118
|
+
},
|
|
119
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gamification Components
|
|
3
|
+
* All text via props - NO hardcoded strings
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { LevelProgress, type LevelProgressProps } from "./LevelProgress";
|
|
7
|
+
export { PointsBadge, type PointsBadgeProps } from "./PointsBadge";
|
|
8
|
+
export { AchievementCard, type AchievementCardProps } from "./AchievementCard";
|
|
9
|
+
export { AchievementToast, type AchievementToastProps } from "./AchievementToast";
|
|
10
|
+
export { StreakDisplay, type StreakDisplayProps } from "./StreakDisplay";
|
|
11
|
+
export { StatsCard, type StatsCardProps } from "./StatsCard";
|
|
12
|
+
export { AchievementItem, type AchievementItemProps } from "./AchievementItem";
|
|
13
|
+
export { GamificationScreen, type GamificationScreenProps } from "./GamificationScreen";
|