@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.
- package/CHANGELOG.md +142 -0
- package/LICENSE +21 -0
- package/README.md +267 -0
- package/SDK_HANDOVER_GUIDE.md +129 -0
- package/dist/PlaybasisProvider.d.ts +19 -0
- package/dist/PlaybasisProvider.d.ts.map +1 -0
- package/dist/PlaybasisProvider.js +24 -0
- package/dist/QwikCardApp.d.ts +9 -0
- package/dist/QwikCardApp.d.ts.map +1 -0
- package/dist/QwikCardApp.js +210 -0
- package/dist/api/client.d.ts +66 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +196 -0
- package/dist/components/Badge.d.ts +8 -0
- package/dist/components/Badge.d.ts.map +1 -0
- package/dist/components/Badge.js +34 -0
- package/dist/components/BadgeIcon.d.ts +10 -0
- package/dist/components/BadgeIcon.d.ts.map +1 -0
- package/dist/components/BadgeIcon.js +51 -0
- package/dist/components/Button.d.ts +10 -0
- package/dist/components/Button.d.ts.map +1 -0
- package/dist/components/Button.js +40 -0
- package/dist/components/GradientCard.d.ts +9 -0
- package/dist/components/GradientCard.d.ts.map +1 -0
- package/dist/components/GradientCard.js +28 -0
- package/dist/components/PointsBalance.d.ts +8 -0
- package/dist/components/PointsBalance.d.ts.map +1 -0
- package/dist/components/PointsBalance.js +85 -0
- package/dist/components/ProgressBar.d.ts +8 -0
- package/dist/components/ProgressBar.d.ts.map +1 -0
- package/dist/components/ProgressBar.js +41 -0
- package/dist/components/QuestProgress.d.ts +10 -0
- package/dist/components/QuestProgress.d.ts.map +1 -0
- package/dist/components/QuestProgress.js +94 -0
- package/dist/components/RadialGauge.d.ts +8 -0
- package/dist/components/RadialGauge.d.ts.map +1 -0
- package/dist/components/RadialGauge.js +53 -0
- package/dist/components/RewardCard.d.ts +10 -0
- package/dist/components/RewardCard.d.ts.map +1 -0
- package/dist/components/RewardCard.js +64 -0
- package/dist/components/RulesModal.d.ts +7 -0
- package/dist/components/RulesModal.d.ts.map +1 -0
- package/dist/components/RulesModal.js +106 -0
- package/dist/hooks/useBadges.d.ts +12 -0
- package/dist/hooks/useBadges.d.ts.map +1 -0
- package/dist/hooks/useBadges.js +42 -0
- package/dist/hooks/usePoints.d.ts +13 -0
- package/dist/hooks/usePoints.d.ts.map +1 -0
- package/dist/hooks/usePoints.js +70 -0
- package/dist/hooks/useQuests.d.ts +12 -0
- package/dist/hooks/useQuests.d.ts.map +1 -0
- package/dist/hooks/useQuests.js +41 -0
- package/dist/hooks/useQwikApp.d.ts +16 -0
- package/dist/hooks/useQwikApp.d.ts.map +1 -0
- package/dist/hooks/useQwikApp.js +42 -0
- package/dist/hooks/useRewards.d.ts +12 -0
- package/dist/hooks/useRewards.d.ts.map +1 -0
- package/dist/hooks/useRewards.js +56 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/theme/context.d.ts +8 -0
- package/dist/theme/context.d.ts.map +1 -0
- package/dist/theme/context.js +8 -0
- package/dist/theme/tokens.d.ts +35 -0
- package/dist/theme/tokens.d.ts.map +1 -0
- package/dist/theme/tokens.js +33 -0
- package/dist/types/index.d.ts +119 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +4 -0
- package/dist/web/widgetAssets.d.ts +4 -0
- package/dist/web/widgetAssets.d.ts.map +1 -0
- package/dist/web/widgetAssets.js +5 -0
- package/dist/web/widgetHtml.d.ts +2 -0
- package/dist/web/widgetHtml.d.ts.map +1 -0
- package/dist/web/widgetHtml.js +299 -0
- package/dist/web/widgetTypes.d.ts +128 -0
- package/dist/web/widgetTypes.d.ts.map +1 -0
- package/dist/web/widgetTypes.js +1 -0
- package/package.json +86 -0
- package/src/PlaybasisProvider.tsx +72 -0
- package/src/QwikCardApp.tsx +302 -0
- package/src/api/client.ts +307 -0
- package/src/components/Badge.tsx +51 -0
- package/src/components/BadgeIcon.tsx +97 -0
- package/src/components/Button.tsx +70 -0
- package/src/components/GradientCard.tsx +49 -0
- package/src/components/PointsBalance.tsx +122 -0
- package/src/components/ProgressBar.tsx +65 -0
- package/src/components/QuestProgress.tsx +153 -0
- package/src/components/RadialGauge.tsx +101 -0
- package/src/components/RewardCard.tsx +123 -0
- package/src/components/RulesModal.tsx +171 -0
- package/src/hooks/useBadges.ts +59 -0
- package/src/hooks/usePoints.ts +91 -0
- package/src/hooks/useQuests.ts +60 -0
- package/src/hooks/useQwikApp.ts +49 -0
- package/src/hooks/useRewards.ts +74 -0
- package/src/index.ts +34 -0
- package/src/theme/context.tsx +17 -0
- package/src/theme/tokens.ts +68 -0
- package/src/types/index.ts +176 -0
- package/src/web/widgetAssets.d.ts +3 -0
- package/src/web/widgetAssets.ts +6 -0
- package/src/web/widgetHtml.ts +302 -0
- 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
|
+
});
|