@playbasis-ai/qwikcard-sdk 2.3.4

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.
Files changed (106) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/LICENSE +21 -0
  3. package/README.md +267 -0
  4. package/SDK_HANDOVER_GUIDE.md +129 -0
  5. package/dist/PlaybasisProvider.d.ts +19 -0
  6. package/dist/PlaybasisProvider.d.ts.map +1 -0
  7. package/dist/PlaybasisProvider.js +24 -0
  8. package/dist/QwikCardApp.d.ts +9 -0
  9. package/dist/QwikCardApp.d.ts.map +1 -0
  10. package/dist/QwikCardApp.js +210 -0
  11. package/dist/api/client.d.ts +66 -0
  12. package/dist/api/client.d.ts.map +1 -0
  13. package/dist/api/client.js +196 -0
  14. package/dist/components/Badge.d.ts +8 -0
  15. package/dist/components/Badge.d.ts.map +1 -0
  16. package/dist/components/Badge.js +34 -0
  17. package/dist/components/BadgeIcon.d.ts +10 -0
  18. package/dist/components/BadgeIcon.d.ts.map +1 -0
  19. package/dist/components/BadgeIcon.js +51 -0
  20. package/dist/components/Button.d.ts +10 -0
  21. package/dist/components/Button.d.ts.map +1 -0
  22. package/dist/components/Button.js +40 -0
  23. package/dist/components/GradientCard.d.ts +9 -0
  24. package/dist/components/GradientCard.d.ts.map +1 -0
  25. package/dist/components/GradientCard.js +28 -0
  26. package/dist/components/PointsBalance.d.ts +8 -0
  27. package/dist/components/PointsBalance.d.ts.map +1 -0
  28. package/dist/components/PointsBalance.js +85 -0
  29. package/dist/components/ProgressBar.d.ts +8 -0
  30. package/dist/components/ProgressBar.d.ts.map +1 -0
  31. package/dist/components/ProgressBar.js +41 -0
  32. package/dist/components/QuestProgress.d.ts +10 -0
  33. package/dist/components/QuestProgress.d.ts.map +1 -0
  34. package/dist/components/QuestProgress.js +94 -0
  35. package/dist/components/RadialGauge.d.ts +8 -0
  36. package/dist/components/RadialGauge.d.ts.map +1 -0
  37. package/dist/components/RadialGauge.js +53 -0
  38. package/dist/components/RewardCard.d.ts +10 -0
  39. package/dist/components/RewardCard.d.ts.map +1 -0
  40. package/dist/components/RewardCard.js +64 -0
  41. package/dist/components/RulesModal.d.ts +7 -0
  42. package/dist/components/RulesModal.d.ts.map +1 -0
  43. package/dist/components/RulesModal.js +106 -0
  44. package/dist/hooks/useBadges.d.ts +12 -0
  45. package/dist/hooks/useBadges.d.ts.map +1 -0
  46. package/dist/hooks/useBadges.js +42 -0
  47. package/dist/hooks/usePoints.d.ts +13 -0
  48. package/dist/hooks/usePoints.d.ts.map +1 -0
  49. package/dist/hooks/usePoints.js +70 -0
  50. package/dist/hooks/useQuests.d.ts +12 -0
  51. package/dist/hooks/useQuests.d.ts.map +1 -0
  52. package/dist/hooks/useQuests.js +41 -0
  53. package/dist/hooks/useQwikApp.d.ts +16 -0
  54. package/dist/hooks/useQwikApp.d.ts.map +1 -0
  55. package/dist/hooks/useQwikApp.js +42 -0
  56. package/dist/hooks/useRewards.d.ts +12 -0
  57. package/dist/hooks/useRewards.d.ts.map +1 -0
  58. package/dist/hooks/useRewards.js +56 -0
  59. package/dist/index.d.ts +27 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +29 -0
  62. package/dist/theme/context.d.ts +8 -0
  63. package/dist/theme/context.d.ts.map +1 -0
  64. package/dist/theme/context.js +8 -0
  65. package/dist/theme/tokens.d.ts +35 -0
  66. package/dist/theme/tokens.d.ts.map +1 -0
  67. package/dist/theme/tokens.js +33 -0
  68. package/dist/types/index.d.ts +119 -0
  69. package/dist/types/index.d.ts.map +1 -0
  70. package/dist/types/index.js +4 -0
  71. package/dist/web/widgetAssets.d.ts +4 -0
  72. package/dist/web/widgetAssets.d.ts.map +1 -0
  73. package/dist/web/widgetAssets.js +5 -0
  74. package/dist/web/widgetHtml.d.ts +2 -0
  75. package/dist/web/widgetHtml.d.ts.map +1 -0
  76. package/dist/web/widgetHtml.js +299 -0
  77. package/dist/web/widgetTypes.d.ts +128 -0
  78. package/dist/web/widgetTypes.d.ts.map +1 -0
  79. package/dist/web/widgetTypes.js +1 -0
  80. package/package.json +86 -0
  81. package/src/PlaybasisProvider.tsx +72 -0
  82. package/src/QwikCardApp.tsx +302 -0
  83. package/src/api/client.ts +307 -0
  84. package/src/components/Badge.tsx +51 -0
  85. package/src/components/BadgeIcon.tsx +97 -0
  86. package/src/components/Button.tsx +70 -0
  87. package/src/components/GradientCard.tsx +49 -0
  88. package/src/components/PointsBalance.tsx +122 -0
  89. package/src/components/ProgressBar.tsx +65 -0
  90. package/src/components/QuestProgress.tsx +153 -0
  91. package/src/components/RadialGauge.tsx +101 -0
  92. package/src/components/RewardCard.tsx +123 -0
  93. package/src/components/RulesModal.tsx +171 -0
  94. package/src/hooks/useBadges.ts +59 -0
  95. package/src/hooks/usePoints.ts +91 -0
  96. package/src/hooks/useQuests.ts +60 -0
  97. package/src/hooks/useQwikApp.ts +49 -0
  98. package/src/hooks/useRewards.ts +74 -0
  99. package/src/index.ts +34 -0
  100. package/src/theme/context.tsx +17 -0
  101. package/src/theme/tokens.ts +68 -0
  102. package/src/types/index.ts +176 -0
  103. package/src/web/widgetAssets.d.ts +3 -0
  104. package/src/web/widgetAssets.ts +6 -0
  105. package/src/web/widgetHtml.ts +302 -0
  106. package/src/web/widgetTypes.ts +146 -0
@@ -0,0 +1,97 @@
1
+ import React from 'react';
2
+ import { View, Text, Image, StyleSheet } from 'react-native';
3
+
4
+ import type { Badge } from '../types';
5
+
6
+ // ─────────────────────────────────────────────────────────────────────────────
7
+ // Props
8
+ // ─────────────────────────────────────────────────────────────────────────────
9
+
10
+ interface BadgeIconProps {
11
+ badge: Badge;
12
+ size?: 'small' | 'medium' | 'large';
13
+ showName?: boolean;
14
+ style?: object;
15
+ }
16
+
17
+ // ─────────────────────────────────────────────────────────────────────────────
18
+ // Component
19
+ // ─────────────────────────────────────────────────────────────────────────────
20
+
21
+ export function BadgeIcon({ badge, size = 'medium', showName = true, style }: BadgeIconProps) {
22
+ const dimensions = {
23
+ small: 40,
24
+ medium: 60,
25
+ large: 80,
26
+ };
27
+
28
+ const iconSize = dimensions[size];
29
+ const isEarned = badge.isEarned;
30
+
31
+ return (
32
+ <View style={[styles.container, style]}>
33
+ <View
34
+ style={[
35
+ styles.iconContainer,
36
+ {
37
+ width: iconSize,
38
+ height: iconSize,
39
+ borderRadius: iconSize / 2,
40
+ opacity: isEarned ? 1 : 0.4,
41
+ },
42
+ ]}
43
+ >
44
+ {badge.imageUrl ? (
45
+ <Image
46
+ source={{ uri: badge.imageUrl }}
47
+ style={{ width: iconSize - 8, height: iconSize - 8, borderRadius: (iconSize - 8) / 2 }}
48
+ resizeMode="cover"
49
+ />
50
+ ) : (
51
+ <Text style={{ fontSize: iconSize / 2 }}>🏅</Text>
52
+ )}
53
+ </View>
54
+
55
+ {showName && (
56
+ <Text style={[styles.name, { color: isEarned ? '#1F2937' : '#9CA3AF' }]} numberOfLines={2}>
57
+ {badge.name}
58
+ </Text>
59
+ )}
60
+
61
+ {isEarned && badge.earnedAt && (
62
+ <Text style={styles.earnedDate}>{new Date(badge.earnedAt).toLocaleDateString()}</Text>
63
+ )}
64
+ </View>
65
+ );
66
+ }
67
+
68
+ // ─────────────────────────────────────────────────────────────────────────────
69
+ // Styles
70
+ // ─────────────────────────────────────────────────────────────────────────────
71
+
72
+ const styles = StyleSheet.create({
73
+ container: {
74
+ alignItems: 'center',
75
+ padding: 8,
76
+ width: 80,
77
+ },
78
+ iconContainer: {
79
+ backgroundColor: '#F3F4F6',
80
+ justifyContent: 'center',
81
+ alignItems: 'center',
82
+ marginBottom: 4,
83
+ },
84
+ name: {
85
+ fontSize: 11,
86
+ fontWeight: '500',
87
+ textAlign: 'center',
88
+ marginTop: 4,
89
+ },
90
+ earnedDate: {
91
+ fontSize: 9,
92
+ color: '#9CA3AF',
93
+ marginTop: 2,
94
+ },
95
+ });
96
+
97
+ export default BadgeIcon;
@@ -0,0 +1,70 @@
1
+ import React from 'react';
2
+ import { TouchableOpacity, Text, StyleSheet, ViewStyle } from 'react-native';
3
+ import LinearGradient from 'react-native-linear-gradient';
4
+
5
+ import { useTheme } from '../theme/context';
6
+
7
+ interface ButtonProps {
8
+ title: string;
9
+ onPress: () => void;
10
+ variant?: 'primary' | 'secondary';
11
+ style?: ViewStyle;
12
+ }
13
+
14
+ export function Button({ title, onPress, variant = 'primary', style }: ButtonProps) {
15
+ const theme = useTheme();
16
+
17
+ if (variant === 'secondary') {
18
+ return (
19
+ <TouchableOpacity
20
+ onPress={onPress}
21
+ style={[
22
+ styles.container,
23
+ {
24
+ backgroundColor: 'rgba(255, 255, 255, 0.1)',
25
+ borderColor: theme.colors.surface,
26
+ borderWidth: 1,
27
+ borderRadius: theme.borderRadius.m,
28
+ },
29
+ style,
30
+ ]}
31
+ >
32
+ <Text style={[styles.text, { color: theme.colors.text.primary }]}>{title}</Text>
33
+ </TouchableOpacity>
34
+ );
35
+ }
36
+
37
+ // Primary variant with gradient
38
+ return (
39
+ <TouchableOpacity onPress={onPress} style={[styles.touchable, style]}>
40
+ <LinearGradient
41
+ colors={theme.colors.gradients.button}
42
+ start={{ x: 0, y: 0 }}
43
+ end={{ x: 1, y: 0 }}
44
+ style={[styles.container, { borderRadius: theme.borderRadius.m }]}
45
+ >
46
+ <Text style={[styles.text, { color: theme.colors.text.onPrimary }]}>{title}</Text>
47
+ </LinearGradient>
48
+ </TouchableOpacity>
49
+ );
50
+ }
51
+
52
+ const styles = StyleSheet.create({
53
+ touchable: {
54
+ shadowColor: '#FF4500',
55
+ shadowOffset: { width: 0, height: 4 },
56
+ shadowOpacity: 0.3,
57
+ shadowRadius: 8,
58
+ elevation: 4,
59
+ },
60
+ container: {
61
+ paddingVertical: 12,
62
+ paddingHorizontal: 20,
63
+ alignItems: 'center',
64
+ justifyContent: 'center',
65
+ },
66
+ text: {
67
+ fontSize: 14,
68
+ fontWeight: '700',
69
+ },
70
+ });
@@ -0,0 +1,49 @@
1
+ import React, { ReactNode } from 'react';
2
+ import { View, StyleSheet, ViewStyle, StyleProp } from 'react-native';
3
+ import LinearGradient from 'react-native-linear-gradient';
4
+
5
+ import { useTheme } from '../theme/context';
6
+
7
+ interface GradientCardProps {
8
+ children: ReactNode;
9
+ style?: StyleProp<ViewStyle>;
10
+ }
11
+
12
+ export function GradientCard({ children, style }: GradientCardProps) {
13
+ const theme = useTheme();
14
+ const isGradientAvailable =
15
+ LinearGradient !== undefined &&
16
+ LinearGradient !== null &&
17
+ (typeof LinearGradient === 'function' || typeof LinearGradient === 'object');
18
+
19
+ const containerStyle = [
20
+ styles.container,
21
+ {
22
+ borderRadius: theme.borderRadius.l,
23
+ borderColor: theme.colors.surface,
24
+ },
25
+ style,
26
+ ];
27
+
28
+ if (!isGradientAvailable) {
29
+ return <View style={containerStyle}>{children}</View>;
30
+ }
31
+
32
+ return (
33
+ <LinearGradient
34
+ colors={['rgba(255, 255, 255, 0.12)', 'rgba(255, 255, 255, 0.04)']}
35
+ start={{ x: 0, y: 0 }}
36
+ end={{ x: 1, y: 1 }}
37
+ style={containerStyle}
38
+ >
39
+ {children}
40
+ </LinearGradient>
41
+ );
42
+ }
43
+
44
+ const styles = StyleSheet.create({
45
+ container: {
46
+ borderWidth: 1,
47
+ overflow: 'hidden',
48
+ },
49
+ });
@@ -0,0 +1,122 @@
1
+ import React from 'react';
2
+ import { View, Text, StyleSheet } from 'react-native';
3
+
4
+ import { GradientCard } from './GradientCard';
5
+ import { useTheme } from '../theme/context';
6
+ import type { PointBalance } from '../types';
7
+
8
+ interface PointsBalanceProps {
9
+ balances: PointBalance[];
10
+ style?: object;
11
+ }
12
+
13
+ export function PointsBalance({ balances, style }: PointsBalanceProps) {
14
+ const theme = useTheme();
15
+
16
+ // Find primary and secondary currencies
17
+ const primary = balances.find((b) => b.currency === 'qwik-coins') || balances[0];
18
+ const secondary = balances.find((b) => b.currency !== primary?.currency);
19
+
20
+ const formattedBalance = primary?.balance.toLocaleString() || '0';
21
+ const secondaryBalance = secondary
22
+ ? `${secondary.balance.toLocaleString()} ${secondary.currency}`
23
+ : null;
24
+
25
+ return (
26
+ <GradientCard style={[styles.card, style]}>
27
+ <View style={styles.header}>
28
+ <Text style={styles.label}>CURRENT BALANCE</Text>
29
+ <Text style={styles.brand}>QwikCard</Text>
30
+ </View>
31
+
32
+ <View style={styles.mainContent}>
33
+ <Text style={styles.currencySymbol}>$</Text>
34
+ <Text style={styles.amount}>{formattedBalance}</Text>
35
+ </View>
36
+
37
+ <View style={styles.footer}>
38
+ {secondaryBalance && (
39
+ <View style={styles.chip}>
40
+ <Text style={styles.chipText}>+ {secondaryBalance}</Text>
41
+ </View>
42
+ )}
43
+ <View style={styles.dots}>
44
+ <View style={[styles.dot, { backgroundColor: '#FFD700' }]} />
45
+ <View style={[styles.dot, { backgroundColor: '#FF69B4' }]} />
46
+ </View>
47
+ </View>
48
+ </GradientCard>
49
+ );
50
+ }
51
+
52
+ const styles = StyleSheet.create({
53
+ card: {
54
+ padding: 20,
55
+ minHeight: 150,
56
+ justifyContent: 'space-between',
57
+ },
58
+ header: {
59
+ flexDirection: 'row',
60
+ justifyContent: 'space-between',
61
+ alignItems: 'center',
62
+ marginBottom: 16,
63
+ },
64
+ label: {
65
+ color: 'rgba(255, 255, 255, 0.8)',
66
+ fontSize: 10,
67
+ fontWeight: '700',
68
+ letterSpacing: 1.2,
69
+ },
70
+ brand: {
71
+ color: '#FFFFFF',
72
+ fontSize: 12,
73
+ fontWeight: '800',
74
+ fontStyle: 'italic',
75
+ },
76
+ mainContent: {
77
+ flexDirection: 'row',
78
+ alignItems: 'flex-start',
79
+ },
80
+ currencySymbol: {
81
+ color: 'rgba(255, 255, 255, 0.9)',
82
+ fontSize: 20,
83
+ fontWeight: '600',
84
+ marginTop: 2,
85
+ marginRight: 4,
86
+ },
87
+ amount: {
88
+ color: '#FFFFFF',
89
+ fontSize: 32,
90
+ fontWeight: '800',
91
+ letterSpacing: -0.5,
92
+ },
93
+ footer: {
94
+ flexDirection: 'row',
95
+ justifyContent: 'space-between',
96
+ alignItems: 'center',
97
+ marginTop: 16,
98
+ },
99
+ chip: {
100
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
101
+ paddingHorizontal: 10,
102
+ paddingVertical: 4,
103
+ borderRadius: 16,
104
+ },
105
+ chipText: {
106
+ color: '#FFFFFF',
107
+ fontSize: 11,
108
+ fontWeight: '600',
109
+ },
110
+ dots: {
111
+ flexDirection: 'row',
112
+ gap: 5,
113
+ },
114
+ dot: {
115
+ width: 20,
116
+ height: 20,
117
+ borderRadius: 10,
118
+ opacity: 0.8,
119
+ },
120
+ });
121
+
122
+ export default PointsBalance;
@@ -0,0 +1,65 @@
1
+ import React from 'react';
2
+ import { View, Text, StyleSheet } from 'react-native';
3
+
4
+ import { useTheme } from '../theme/context';
5
+
6
+ interface ProgressBarProps {
7
+ label?: string;
8
+ value: number; // 0-100
9
+ color?: string;
10
+ }
11
+
12
+ export function ProgressBar({ label, value, color }: ProgressBarProps) {
13
+ const theme = useTheme();
14
+ const barColor = color || theme.colors.primary;
15
+
16
+ return (
17
+ <View style={styles.container}>
18
+ {label && (
19
+ <View style={styles.header}>
20
+ <Text style={[styles.label, { color: theme.colors.text.primary }]}>{label}</Text>
21
+ <Text style={[styles.value, { color: theme.colors.text.secondary }]}>{value}%</Text>
22
+ </View>
23
+ )}
24
+ <View style={[styles.track, { backgroundColor: 'rgba(148, 163, 184, 0.2)' }]}>
25
+ <View
26
+ style={[
27
+ styles.fill,
28
+ {
29
+ width: `${value}%`,
30
+ backgroundColor: barColor,
31
+ },
32
+ ]}
33
+ />
34
+ </View>
35
+ </View>
36
+ );
37
+ }
38
+
39
+ const styles = StyleSheet.create({
40
+ container: {
41
+ width: '100%',
42
+ marginVertical: 4,
43
+ },
44
+ header: {
45
+ flexDirection: 'row',
46
+ justifyContent: 'space-between',
47
+ marginBottom: 6,
48
+ },
49
+ label: {
50
+ fontSize: 12,
51
+ fontWeight: '700',
52
+ },
53
+ value: {
54
+ fontSize: 12,
55
+ },
56
+ track: {
57
+ height: 8,
58
+ borderRadius: 999,
59
+ overflow: 'hidden',
60
+ },
61
+ fill: {
62
+ height: '100%',
63
+ borderRadius: 999,
64
+ },
65
+ });
@@ -0,0 +1,153 @@
1
+ import React from 'react';
2
+ import { View, Text, StyleSheet } from 'react-native';
3
+
4
+ import type { Quest } from '../types';
5
+
6
+ // ─────────────────────────────────────────────────────────────────────────────
7
+ // Props
8
+ // ─────────────────────────────────────────────────────────────────────────────
9
+
10
+ interface QuestProgressProps {
11
+ quest: Quest;
12
+ primaryColor?: string;
13
+ backgroundColor?: string;
14
+ style?: object;
15
+ }
16
+
17
+ // ─────────────────────────────────────────────────────────────────────────────
18
+ // Component
19
+ // ─────────────────────────────────────────────────────────────────────────────
20
+
21
+ export function QuestProgress({
22
+ quest,
23
+ primaryColor = '#6366F1',
24
+ backgroundColor = '#E5E7EB',
25
+ style,
26
+ }: QuestProgressProps) {
27
+ const progressPercent = quest.target > 0 ? (quest.progress / quest.target) * 100 : 0;
28
+ const isCompleted = quest.status === 'completed';
29
+
30
+ return (
31
+ <View style={[styles.container, style]}>
32
+ <View style={styles.header}>
33
+ <Text style={styles.name}>{quest.name}</Text>
34
+ {isCompleted && <Text style={styles.completedBadge}>✓</Text>}
35
+ </View>
36
+
37
+ <Text style={styles.description}>{quest.description}</Text>
38
+
39
+ <View style={[styles.progressBar, { backgroundColor }]}>
40
+ <View
41
+ style={[
42
+ styles.progressFill,
43
+ {
44
+ backgroundColor: isCompleted ? '#10B981' : primaryColor,
45
+ width: `${Math.min(progressPercent, 100)}%`,
46
+ },
47
+ ]}
48
+ />
49
+ </View>
50
+
51
+ <View style={styles.footer}>
52
+ <Text style={styles.progressText}>
53
+ {quest.progress} / {quest.target}
54
+ </Text>
55
+ <Text style={styles.percentText}>{Math.round(progressPercent)}%</Text>
56
+ </View>
57
+
58
+ {quest.rewards && quest.rewards.length > 0 && (
59
+ <View style={styles.rewards}>
60
+ <Text style={styles.rewardLabel}>Rewards:</Text>
61
+ {quest.rewards.map((reward, idx) => (
62
+ <Text key={idx} style={styles.rewardText}>
63
+ {reward.type === 'points' && `${reward.amount} ${reward.currency}`}
64
+ {reward.type === 'badge' && `🏅 Badge`}
65
+ {reward.type === 'reward' && `🎁 Reward`}
66
+ </Text>
67
+ ))}
68
+ </View>
69
+ )}
70
+ </View>
71
+ );
72
+ }
73
+
74
+ // ─────────────────────────────────────────────────────────────────────────────
75
+ // Styles
76
+ // ─────────────────────────────────────────────────────────────────────────────
77
+
78
+ const styles = StyleSheet.create({
79
+ container: {
80
+ backgroundColor: '#FFFFFF',
81
+ borderRadius: 12,
82
+ padding: 16,
83
+ marginVertical: 8,
84
+ shadowColor: '#000',
85
+ shadowOffset: { width: 0, height: 2 },
86
+ shadowOpacity: 0.1,
87
+ shadowRadius: 4,
88
+ elevation: 3,
89
+ },
90
+ header: {
91
+ flexDirection: 'row',
92
+ justifyContent: 'space-between',
93
+ alignItems: 'center',
94
+ marginBottom: 4,
95
+ },
96
+ name: {
97
+ fontSize: 16,
98
+ fontWeight: '600',
99
+ color: '#1F2937',
100
+ },
101
+ completedBadge: {
102
+ fontSize: 18,
103
+ color: '#10B981',
104
+ },
105
+ description: {
106
+ fontSize: 14,
107
+ color: '#6B7280',
108
+ marginBottom: 12,
109
+ },
110
+ progressBar: {
111
+ height: 8,
112
+ borderRadius: 4,
113
+ overflow: 'hidden',
114
+ },
115
+ progressFill: {
116
+ height: '100%',
117
+ borderRadius: 4,
118
+ },
119
+ footer: {
120
+ flexDirection: 'row',
121
+ justifyContent: 'space-between',
122
+ marginTop: 8,
123
+ },
124
+ progressText: {
125
+ fontSize: 12,
126
+ color: '#6B7280',
127
+ },
128
+ percentText: {
129
+ fontSize: 12,
130
+ fontWeight: '600',
131
+ color: '#1F2937',
132
+ },
133
+ rewards: {
134
+ flexDirection: 'row',
135
+ alignItems: 'center',
136
+ marginTop: 12,
137
+ paddingTop: 12,
138
+ borderTopWidth: 1,
139
+ borderTopColor: '#E5E7EB',
140
+ },
141
+ rewardLabel: {
142
+ fontSize: 12,
143
+ color: '#6B7280',
144
+ marginRight: 8,
145
+ },
146
+ rewardText: {
147
+ fontSize: 12,
148
+ color: '#1F2937',
149
+ marginRight: 8,
150
+ },
151
+ });
152
+
153
+ export default QuestProgress;
@@ -0,0 +1,101 @@
1
+ import React from 'react';
2
+ import { View, StyleSheet, Text } from 'react-native';
3
+ import Svg, { Circle } from 'react-native-svg';
4
+
5
+ import { useTheme } from '../theme/context';
6
+ import { GradientCard } from './GradientCard';
7
+
8
+ interface RadialGaugeProps {
9
+ value: number;
10
+ max: number;
11
+ label: string;
12
+ }
13
+
14
+ export function RadialGauge({ value, max, label }: RadialGaugeProps) {
15
+ const theme = useTheme();
16
+ const size = 80;
17
+ const stroke = 6;
18
+ const radius = size / 2 - stroke;
19
+ const circumference = radius * 2 * Math.PI;
20
+ const strokeDashoffset = circumference - (value / max) * circumference;
21
+
22
+ const percentage = Math.round((value / max) * 100);
23
+
24
+ return (
25
+ <GradientCard style={styles.container}>
26
+ <View style={styles.content}>
27
+ <View style={styles.gaugeContainer}>
28
+ <Svg height={size} width={size} style={{ transform: [{ rotate: '-90deg' }] }}>
29
+ <Circle
30
+ stroke="rgba(255,255,255,0.1)"
31
+ strokeWidth={stroke}
32
+ fill="transparent"
33
+ r={radius}
34
+ cx={size / 2}
35
+ cy={size / 2}
36
+ />
37
+ <Circle
38
+ stroke="#FFD700"
39
+ strokeWidth={stroke}
40
+ strokeDasharray={`${circumference} ${circumference}`}
41
+ strokeDashoffset={strokeDashoffset}
42
+ strokeLinecap="round"
43
+ fill="transparent"
44
+ r={radius}
45
+ cx={size / 2}
46
+ cy={size / 2}
47
+ />
48
+ </Svg>
49
+ <View style={styles.centerText}>
50
+ <Text style={styles.percentText}>{percentage}%</Text>
51
+ </View>
52
+ </View>
53
+ <View style={styles.infoContainer}>
54
+ <Text style={[styles.label, { color: theme.colors.text.primary }]}>{label}</Text>
55
+ <Text style={[styles.subText, { color: theme.colors.text.secondary }]}>
56
+ ${value} of ${max} saved.
57
+ </Text>
58
+ </View>
59
+ </View>
60
+ </GradientCard>
61
+ );
62
+ }
63
+
64
+ const styles = StyleSheet.create({
65
+ container: {
66
+ padding: 16,
67
+ marginBottom: 16,
68
+ },
69
+ content: {
70
+ flexDirection: 'row',
71
+ alignItems: 'center',
72
+ },
73
+ gaugeContainer: {
74
+ position: 'relative',
75
+ width: 80,
76
+ height: 80,
77
+ },
78
+ centerText: {
79
+ ...StyleSheet.absoluteFillObject,
80
+ alignItems: 'center',
81
+ justifyContent: 'center',
82
+ },
83
+ percentText: {
84
+ fontSize: 16,
85
+ fontWeight: '800',
86
+ color: 'white',
87
+ },
88
+ infoContainer: {
89
+ flex: 1,
90
+ marginLeft: 16,
91
+ },
92
+ label: {
93
+ fontSize: 16,
94
+ fontWeight: '700',
95
+ marginBottom: 2,
96
+ },
97
+ subText: {
98
+ fontSize: 12,
99
+ lineHeight: 16,
100
+ },
101
+ });