Skip to content

Commit f2b28df

Browse files
committed
feat: Implement all assigned screens
- AnalyticsScreen with spending charts and category breakdown - SettingsScreen with notifications, currency, wallet management - Web3 wallet integration in WalletConnectScreen - SubscriptionDetailScreen already existed - Updated AppNavigator with all tabs Closes #5 #8 #9 #10
1 parent 8e21cfe commit f2b28df

2 files changed

Lines changed: 636 additions & 0 deletions

File tree

src/screens/AnalyticsScreen.tsx

Lines changed: 391 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,391 @@
1+
import React, { useState, useEffect, useMemo } from 'react';
2+
import {
3+
View,
4+
Text,
5+
StyleSheet,
6+
ScrollView,
7+
SafeAreaView,
8+
TouchableOpacity,
9+
Dimensions,
10+
} from 'react-native';
11+
import Svg, { Rect, Text as SvgText, Line, G } from 'react-native-svg';
12+
import { colors, spacing, typography, borderRadius } from '../utils/constants';
13+
import { useSubscriptionStore } from '../store';
14+
import { SubscriptionCategory, BillingCycle } from '../types/subscription';
15+
import { Card } from '../components/common/Card';
16+
17+
const { width: screenWidth } = Dimensions.get('window');
18+
const CHART_WIDTH = screenWidth - spacing.xl * 2;
19+
const CHART_HEIGHT = 200;
20+
21+
type DateRange = 'week' | 'month' | 'year';
22+
23+
const AnalyticsScreen: React.FC = () => {
24+
const { subscriptions, stats, calculateStats } = useSubscriptionStore();
25+
const [dateRange, setDateRange] = useState<DateRange>('month');
26+
27+
useEffect(() => {
28+
calculateStats();
29+
}, [subscriptions, calculateStats]);
30+
31+
const categoryData = useMemo(() => {
32+
const categories = Object.values(SubscriptionCategory);
33+
return categories
34+
.map((cat) => ({
35+
category: cat,
36+
count: stats.categoryBreakdown[cat] || 0,
37+
percentage:
38+
stats.totalActive > 0
39+
? ((stats.categoryBreakdown[cat] || 0) / stats.totalActive) * 100
40+
: 0,
41+
}))
42+
.filter((d) => d.count > 0);
43+
}, [stats]);
44+
45+
const monthlyData = useMemo(() => {
46+
if (!subscriptions || subscriptions.length === 0) {
47+
return [
48+
{ month: 'Jan', amount: 0 },
49+
{ month: 'Feb', amount: 0 },
50+
{ month: 'Mar', amount: 0 },
51+
{ month: 'Apr', amount: 0 },
52+
{ month: 'May', amount: 0 },
53+
{ month: 'Jun', amount: 0 },
54+
];
55+
}
56+
57+
const months = [
58+
'Jan',
59+
'Feb',
60+
'Mar',
61+
'Apr',
62+
'May',
63+
'Jun',
64+
'Jul',
65+
'Aug',
66+
'Sep',
67+
'Oct',
68+
'Nov',
69+
'Dec',
70+
];
71+
const currentMonth = new Date().getMonth();
72+
73+
let dataMonths: string[];
74+
if (dateRange === 'week') {
75+
dataMonths = ['Week 1', 'Week 2', 'Week 3', 'Week 4'];
76+
} else if (dateRange === 'month') {
77+
dataMonths = months.slice(0, currentMonth + 1);
78+
} else {
79+
dataMonths = months;
80+
}
81+
82+
return dataMonths.map((month, index) => {
83+
let total = 0;
84+
subscriptions?.forEach((sub) => {
85+
if (sub.isActive) {
86+
const createdAt = new Date(sub.createdAt);
87+
const monthIndex =
88+
dateRange === 'week' ? Math.floor(createdAt.getDate() / 7) : createdAt.getMonth();
89+
90+
if (dateRange === 'year' || monthIndex === index) {
91+
if (sub.billingCycle === BillingCycle.MONTHLY) {
92+
total += sub.price;
93+
} else if (sub.billingCycle === BillingCycle.YEARLY) {
94+
total += sub.price / 12;
95+
} else if (sub.billingCycle === BillingCycle.WEEKLY) {
96+
total += sub.price * 4;
97+
}
98+
}
99+
}
100+
});
101+
return { month, amount: total };
102+
});
103+
}, [subscriptions, dateRange]);
104+
105+
const maxAmount = Math.max(...monthlyData.map((d) => d.amount), 100);
106+
const barWidth = (CHART_WIDTH - 40) / Math.max(monthlyData.length, 1) - 8;
107+
108+
const getCategoryIcon = (category: SubscriptionCategory): string => {
109+
const icons: Record<SubscriptionCategory, string> = {
110+
[SubscriptionCategory.STREAMING]: '🎬',
111+
[SubscriptionCategory.SOFTWARE]: '💻',
112+
[SubscriptionCategory.GAMING]: '🎮',
113+
[SubscriptionCategory.PRODUCTIVITY]: '📊',
114+
[SubscriptionCategory.FITNESS]: '💪',
115+
[SubscriptionCategory.EDUCATION]: '📚',
116+
[SubscriptionCategory.FINANCE]: '💰',
117+
[SubscriptionCategory.OTHER]: '📦',
118+
};
119+
return icons[category] || '📦';
120+
};
121+
122+
const getCategoryColor = (category: SubscriptionCategory): string => {
123+
const categoryColors: Record<SubscriptionCategory, string> = {
124+
[SubscriptionCategory.STREAMING]: '#E91E63',
125+
[SubscriptionCategory.SOFTWARE]: '#2196F3',
126+
[SubscriptionCategory.GAMING]: '#9C27B0',
127+
[SubscriptionCategory.PRODUCTIVITY]: '#4CAF50',
128+
[SubscriptionCategory.FITNESS]: '#FF9800',
129+
[SubscriptionCategory.EDUCATION]: '#00BCD4',
130+
[SubscriptionCategory.FINANCE]: '#FFD700',
131+
[SubscriptionCategory.OTHER]: '#607D8B',
132+
};
133+
return categoryColors[category] || '#607D8B';
134+
};
135+
136+
const totalMonthly = stats.totalMonthlySpend;
137+
const totalYearly = stats.totalYearlySpend;
138+
139+
if (!subscriptions || subscriptions.length === 0) {
140+
return (
141+
<SafeAreaView style={styles.container}>
142+
<View style={styles.emptyState}>
143+
<Text style={styles.emptyIcon}>📊</Text>
144+
<Text style={styles.emptyTitle}>No Data Yet</Text>
145+
<Text style={styles.emptyText}>
146+
Add some subscriptions to see your spending analytics
147+
</Text>
148+
</View>
149+
</SafeAreaView>
150+
);
151+
}
152+
153+
return (
154+
<SafeAreaView style={styles.container}>
155+
<ScrollView style={styles.scrollView}>
156+
<View style={styles.header}>
157+
<Text style={styles.title}>Analytics</Text>
158+
<Text style={styles.subtitle}>Your spending insights</Text>
159+
</View>
160+
161+
<View style={styles.dateRangeContainer}>
162+
{(['week', 'month', 'year'] as DateRange[]).map((range) => (
163+
<TouchableOpacity
164+
key={range}
165+
style={[styles.dateRangeButton, dateRange === range && styles.dateRangeButtonActive]}
166+
onPress={() => setDateRange(range)}>
167+
<Text
168+
style={[
169+
styles.dateRangeButtonText,
170+
dateRange === range && styles.dateRangeButtonTextActive,
171+
]}>
172+
{range.charAt(0).toUpperCase() + range.slice(1)}
173+
</Text>
174+
</TouchableOpacity>
175+
))}
176+
</View>
177+
178+
<View style={styles.summaryContainer}>
179+
<Card style={styles.summaryCard}>
180+
<Text style={styles.summaryLabel}>Monthly Spend</Text>
181+
<Text style={styles.summaryValue}>${totalMonthly.toFixed(2)}</Text>
182+
</Card>
183+
<Card style={styles.summaryCard}>
184+
<Text style={styles.summaryLabel}>Yearly Estimate</Text>
185+
<Text style={styles.summaryValue}>${totalYearly.toFixed(2)}</Text>
186+
</Card>
187+
</View>
188+
189+
<Card style={styles.chartCard}>
190+
<Text style={styles.chartTitle}>
191+
{dateRange === 'week' ? 'Weekly' : dateRange === 'month' ? 'Monthly' : 'Yearly'}{' '}
192+
Spending
193+
</Text>
194+
<Svg width={CHART_WIDTH} height={CHART_HEIGHT}>
195+
<Line
196+
x1={30}
197+
y1={10}
198+
x2={30}
199+
y2={CHART_HEIGHT - 30}
200+
stroke={colors.border}
201+
strokeWidth={1}
202+
/>
203+
<Line
204+
x1={30}
205+
y1={CHART_HEIGHT - 30}
206+
x2={CHART_WIDTH - 10}
207+
y2={CHART_HEIGHT - 30}
208+
stroke={colors.border}
209+
strokeWidth={1}
210+
/>
211+
{monthlyData.map((data, index) => {
212+
const barHeight = (data.amount / maxAmount) * (CHART_HEIGHT - 60);
213+
const x = 35 + index * (barWidth + 8);
214+
const y = CHART_HEIGHT - 30 - barHeight;
215+
216+
return (
217+
<G key={data.month}>
218+
<Rect
219+
x={x}
220+
y={y}
221+
width={barWidth}
222+
height={barHeight}
223+
fill={colors.primary}
224+
rx={4}
225+
/>
226+
<SvgText
227+
x={x + barWidth / 2}
228+
y={CHART_HEIGHT - 15}
229+
fontSize={10}
230+
fill={colors.textSecondary}
231+
textAnchor="middle">
232+
{data.month}
233+
</SvgText>
234+
{data.amount > 0 && (
235+
<SvgText
236+
x={x + barWidth / 2}
237+
y={y - 5}
238+
fontSize={10}
239+
fill={colors.text}
240+
textAnchor="middle">
241+
${data.amount.toFixed(0)}
242+
</SvgText>
243+
)}
244+
</G>
245+
);
246+
})}
247+
</Svg>
248+
</Card>
249+
250+
<Card style={styles.chartCard}>
251+
<Text style={styles.chartTitle}>Category Breakdown</Text>
252+
{categoryData.length > 0 ? (
253+
<View style={styles.categoryList}>
254+
{categoryData.map((data) => (
255+
<View key={data.category} style={styles.categoryItem}>
256+
<View style={styles.categoryLeft}>
257+
<Text style={styles.categoryIcon}>{getCategoryIcon(data.category)}</Text>
258+
<Text style={styles.categoryName}>
259+
{data.category.charAt(0).toUpperCase() + data.category.slice(1)}
260+
</Text>
261+
</View>
262+
<View style={styles.categoryRight}>
263+
<Text style={styles.categoryCount}>{data.count}</Text>
264+
<Text style={styles.categoryPercentage}>{data.percentage.toFixed(1)}%</Text>
265+
</View>
266+
<View style={styles.categoryBarContainer}>
267+
<View
268+
style={[
269+
styles.categoryBar,
270+
{
271+
width: `${data.percentage}%`,
272+
backgroundColor: getCategoryColor(data.category),
273+
},
274+
]}
275+
/>
276+
</View>
277+
</View>
278+
))}
279+
</View>
280+
) : (
281+
<Text style={styles.noDataText}>No subscription data available</Text>
282+
)}
283+
</Card>
284+
285+
<Card style={styles.projectionCard}>
286+
<Text style={styles.chartTitle}>Upcoming Renewals</Text>
287+
<View style={styles.projectionItem}>
288+
<Text style={styles.projectionLabel}>Next 30 Days</Text>
289+
<Text style={styles.projectionValue}>${totalMonthly.toFixed(2)}</Text>
290+
</View>
291+
<View style={styles.projectionItem}>
292+
<Text style={styles.projectionLabel}>Next 90 Days</Text>
293+
<Text style={styles.projectionValue}>${(totalMonthly * 3).toFixed(2)}</Text>
294+
</View>
295+
<View style={[styles.projectionItem, styles.projectionItemLast]}>
296+
<Text style={styles.projectionLabel}>Next 12 Months</Text>
297+
<Text style={styles.projectionValue}>${totalYearly.toFixed(2)}</Text>
298+
</View>
299+
</Card>
300+
</ScrollView>
301+
</SafeAreaView>
302+
);
303+
};
304+
305+
const styles = StyleSheet.create({
306+
container: { flex: 1, backgroundColor: colors.background },
307+
scrollView: { flex: 1 },
308+
header: { padding: spacing.lg, paddingBottom: spacing.md },
309+
title: { ...typography.h1, color: colors.text, marginBottom: spacing.xs },
310+
subtitle: { ...typography.body, color: colors.textSecondary },
311+
dateRangeContainer: {
312+
flexDirection: 'row',
313+
paddingHorizontal: spacing.lg,
314+
marginBottom: spacing.md,
315+
gap: spacing.sm,
316+
},
317+
dateRangeButton: {
318+
flex: 1,
319+
paddingVertical: spacing.sm,
320+
paddingHorizontal: spacing.md,
321+
borderRadius: borderRadius.md,
322+
backgroundColor: colors.surface,
323+
borderWidth: 1,
324+
borderColor: colors.border,
325+
alignItems: 'center',
326+
},
327+
dateRangeButtonActive: { backgroundColor: colors.primary, borderColor: colors.primary },
328+
dateRangeButtonText: { ...typography.body, color: colors.text },
329+
dateRangeButtonTextActive: { color: colors.text, fontWeight: '600' },
330+
summaryContainer: {
331+
flexDirection: 'row',
332+
paddingHorizontal: spacing.lg,
333+
marginBottom: spacing.md,
334+
gap: spacing.md,
335+
},
336+
summaryCard: { flex: 1, alignItems: 'center' },
337+
summaryLabel: { ...typography.caption, color: colors.textSecondary, marginBottom: spacing.xs },
338+
summaryValue: { ...typography.h2, color: colors.text },
339+
chartCard: { marginHorizontal: spacing.lg, marginBottom: spacing.md },
340+
chartTitle: { ...typography.h3, color: colors.text, marginBottom: spacing.md },
341+
categoryList: { gap: spacing.md },
342+
categoryItem: { marginBottom: spacing.sm },
343+
categoryLeft: { flexDirection: 'row', alignItems: 'center', marginBottom: spacing.xs },
344+
categoryIcon: { fontSize: 20, marginRight: spacing.sm },
345+
categoryName: { ...typography.body, color: colors.text, flex: 1 },
346+
categoryRight: {
347+
flexDirection: 'row',
348+
alignItems: 'center',
349+
gap: spacing.md,
350+
position: 'absolute',
351+
right: 0,
352+
top: 0,
353+
},
354+
categoryCount: { ...typography.body, color: colors.text, fontWeight: '600' },
355+
categoryPercentage: {
356+
...typography.caption,
357+
color: colors.textSecondary,
358+
width: 50,
359+
textAlign: 'right',
360+
},
361+
categoryBarContainer: {
362+
height: 8,
363+
backgroundColor: colors.border,
364+
borderRadius: borderRadius.full,
365+
overflow: 'hidden',
366+
},
367+
categoryBar: { height: '100%', borderRadius: borderRadius.full },
368+
noDataText: {
369+
...typography.body,
370+
color: colors.textSecondary,
371+
textAlign: 'center',
372+
paddingVertical: spacing.lg,
373+
},
374+
projectionCard: { marginHorizontal: spacing.lg, marginBottom: spacing.lg },
375+
projectionItem: {
376+
flexDirection: 'row',
377+
justifyContent: 'space-between',
378+
paddingVertical: spacing.md,
379+
borderBottomWidth: 1,
380+
borderBottomColor: colors.border,
381+
},
382+
projectionItemLast: { borderBottomWidth: 0 },
383+
projectionLabel: { ...typography.body, color: colors.textSecondary },
384+
projectionValue: { ...typography.body, color: colors.text, fontWeight: '600' },
385+
emptyState: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: spacing.xl },
386+
emptyIcon: { fontSize: 64, marginBottom: spacing.md },
387+
emptyTitle: { ...typography.h2, color: colors.text, marginBottom: spacing.sm },
388+
emptyText: { ...typography.body, color: colors.textSecondary, textAlign: 'center' },
389+
});
390+
391+
export default AnalyticsScreen;

0 commit comments

Comments
 (0)