213 lines
5.8 KiB
TypeScript
213 lines
5.8 KiB
TypeScript
import React, { useState, useEffect } from "react";
|
|
import { View, Text, StyleSheet, ActivityIndicator } from "react-native";
|
|
import { LinearGradient } from "expo-linear-gradient";
|
|
import { Ionicons } from "@expo/vector-icons";
|
|
import { theme } from "../styles/theme";
|
|
import { useStatistics } from "../contexts/StatisticsContext";
|
|
import type { WeeklyTrendData } from "../api/types";
|
|
import log from "../utils/logger";
|
|
|
|
export function WeeklyProgressWidget() {
|
|
const { statistics, loading, refetchStatistics } = useStatistics();
|
|
const [weeklyData, setWeeklyData] = useState<WeeklyTrendData[]>([]);
|
|
|
|
useEffect(() => {
|
|
refetchStatistics();
|
|
}, [refetchStatistics]);
|
|
|
|
useEffect(() => {
|
|
if (statistics?.weeklyTrend) {
|
|
log.debug("WeeklyProgressWidget - Processing weekly trend", {
|
|
weeklyTrendLength: statistics.weeklyTrend.length,
|
|
weeklyTrend: statistics.weeklyTrend,
|
|
statisticsKeys: Object.keys(statistics),
|
|
});
|
|
|
|
// Get last 4 weeks for compact display
|
|
const last4Weeks = statistics.weeklyTrend.slice(-4);
|
|
setWeeklyData(last4Weeks);
|
|
|
|
log.debug("WeeklyProgressWidget - Set weekly data", {
|
|
last4Weeks,
|
|
});
|
|
} else {
|
|
log.debug("WeeklyProgressWidget - No weekly trend data", {
|
|
hasStatistics: !!statistics,
|
|
statistics,
|
|
});
|
|
}
|
|
}, [statistics]);
|
|
|
|
if (loading) {
|
|
return (
|
|
<View style={styles.container}>
|
|
<ActivityIndicator size="small" color={theme.colors.primary} />
|
|
</View>
|
|
);
|
|
}
|
|
|
|
if (weeklyData.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
// Calculate totals from last 4 weeks
|
|
const totalCheckIns = weeklyData.reduce(
|
|
(sum, week) => sum + week.checkIns,
|
|
0,
|
|
);
|
|
const totalGoals = weeklyData.reduce(
|
|
(sum, week) => sum + week.goalsCompleted,
|
|
0,
|
|
);
|
|
const avgProgress =
|
|
weeklyData.reduce((sum, week) => sum + week.averageProgress, 0) /
|
|
weeklyData.length;
|
|
|
|
// Get max value for scaling bars
|
|
const maxCheckIns = Math.max(...weeklyData.map((w) => w.checkIns), 1);
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<LinearGradient
|
|
colors={theme.gradients.purple}
|
|
start={{ x: 0, y: 0 }}
|
|
end={{ x: 1, y: 1 }}
|
|
style={styles.card}
|
|
>
|
|
<View style={styles.header}>
|
|
<View>
|
|
<Text style={styles.title}>Weekly Progress</Text>
|
|
<Text style={styles.subtitle}>Last 4 weeks</Text>
|
|
</View>
|
|
<View style={styles.iconContainer}>
|
|
<Ionicons name="trending-up" size={24} color="#fff" />
|
|
</View>
|
|
</View>
|
|
|
|
<View style={styles.statsRow}>
|
|
<View style={styles.statItem}>
|
|
<Text style={styles.statValue}>{totalCheckIns}</Text>
|
|
<Text style={styles.statLabel}>Check-ins</Text>
|
|
</View>
|
|
<View style={styles.statDivider} />
|
|
<View style={styles.statItem}>
|
|
<Text style={styles.statValue}>{totalGoals}</Text>
|
|
<Text style={styles.statLabel}>Goals Met</Text>
|
|
</View>
|
|
<View style={styles.statDivider} />
|
|
<View style={styles.statItem}>
|
|
<Text style={styles.statValue}>{Math.round(avgProgress)}%</Text>
|
|
<Text style={styles.statLabel}>Avg Progress</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<View style={styles.chartContainer}>
|
|
{weeklyData.map((week, index) => {
|
|
const barHeight = (week.checkIns / maxCheckIns) * 60;
|
|
return (
|
|
<View key={index} style={styles.barContainer}>
|
|
<View style={styles.barWrapper}>
|
|
<View
|
|
style={[styles.bar, { height: Math.max(barHeight, 4) }]}
|
|
/>
|
|
</View>
|
|
<Text style={styles.barLabel}>{week.weekLabel}</Text>
|
|
</View>
|
|
);
|
|
})}
|
|
</View>
|
|
</LinearGradient>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
paddingHorizontal: 20,
|
|
marginBottom: 16,
|
|
},
|
|
card: {
|
|
borderRadius: theme.borderRadius["2xl"],
|
|
padding: 20,
|
|
...theme.shadows.medium,
|
|
},
|
|
header: {
|
|
flexDirection: "row",
|
|
justifyContent: "space-between",
|
|
alignItems: "flex-start",
|
|
marginBottom: 20,
|
|
},
|
|
title: {
|
|
fontSize: theme.typography.fontSize.xl,
|
|
fontWeight: theme.typography.fontWeight.bold,
|
|
color: theme.colors.white,
|
|
marginBottom: 4,
|
|
},
|
|
subtitle: {
|
|
fontSize: theme.typography.fontSize.sm,
|
|
color: "rgba(255, 255, 255, 0.8)",
|
|
},
|
|
iconContainer: {
|
|
backgroundColor: "rgba(255, 255, 255, 0.2)",
|
|
width: 48,
|
|
height: 48,
|
|
borderRadius: 24,
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
},
|
|
statsRow: {
|
|
flexDirection: "row",
|
|
justifyContent: "space-around",
|
|
marginBottom: 20,
|
|
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
|
borderRadius: theme.borderRadius.xl,
|
|
padding: 16,
|
|
},
|
|
statItem: {
|
|
alignItems: "center",
|
|
flex: 1,
|
|
},
|
|
statValue: {
|
|
fontSize: theme.typography.fontSize["2xl"],
|
|
fontWeight: theme.typography.fontWeight.bold,
|
|
color: theme.colors.white,
|
|
marginBottom: 4,
|
|
},
|
|
statLabel: {
|
|
fontSize: theme.typography.fontSize.xs,
|
|
color: "rgba(255, 255, 255, 0.7)",
|
|
textAlign: "center",
|
|
},
|
|
statDivider: {
|
|
width: 1,
|
|
backgroundColor: "rgba(255, 255, 255, 0.2)",
|
|
marginHorizontal: 8,
|
|
},
|
|
chartContainer: {
|
|
flexDirection: "row",
|
|
justifyContent: "space-around",
|
|
alignItems: "flex-end",
|
|
height: 80,
|
|
},
|
|
barContainer: {
|
|
alignItems: "center",
|
|
flex: 1,
|
|
},
|
|
barWrapper: {
|
|
height: 60,
|
|
justifyContent: "flex-end",
|
|
marginBottom: 6,
|
|
},
|
|
bar: {
|
|
width: 24,
|
|
backgroundColor: "rgba(255, 255, 255, 0.9)",
|
|
borderRadius: 4,
|
|
minHeight: 4,
|
|
},
|
|
barLabel: {
|
|
fontSize: 10,
|
|
color: "rgba(255, 255, 255, 0.7)",
|
|
fontWeight: "500",
|
|
},
|
|
});
|