3 and 4 completed

This commit is contained in:
dimitar committed 2026-03-11 02:38:43 +01:00
1 parent 3573709d99
commit 97436c6823
21 files changed
+2168 -558

No files matched your search

+227 -141
View File
@@ -1,157 +1,243 @@
import React from 'react';
import { View, Text, StyleSheet, Dimensions } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { Ionicons } from '@expo/vector-icons';
import { theme } from '../styles/theme';
import React, { useState, useEffect } from "react";
import {
View,
Text,
StyleSheet,
Dimensions,
ActivityIndicator,
} from "react-native";
import { LinearGradient } from "expo-linear-gradient";
import { Ionicons } from "@expo/vector-icons";
import { useUser } from "@clerk/clerk-expo";
import { theme } from "../styles/theme";
import { getUserStatistics } from "../api/statistics";
import type { UserStatisticsResponse } from "../api/types";
const { width } = Dimensions.get('window');
const { width } = Dimensions.get("window");
interface ActivityWidgetProps {
steps: number;
calories: number;
duration: number; // in minutes
steps?: number;
calories: number;
duration?: number; // in minutes
}
export function ActivityWidget({ steps, calories, duration }: ActivityWidgetProps) {
return (
<View style={styles.container}>
<LinearGradient
colors={theme.gradients.dark}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={[styles.card, theme.shadows.medium]}
>
<View style={styles.header}>
<Text style={styles.title}>Daily Activity</Text>
<Ionicons name="stats-chart" size={20} color={theme.colors.primary} />
</View>
export function ActivityWidget({
steps = 0,
calories,
duration = 0,
}: ActivityWidgetProps) {
const { user } = useUser();
const [statistics, setStatistics] = useState<UserStatisticsResponse | null>(
null,
);
const [loading, setLoading] = useState(true);
<View style={styles.statsRow}>
<View style={styles.statItem}>
<View style={[styles.iconContainer, { backgroundColor: 'rgba(59, 130, 246, 0.2)' }]}>
<Ionicons name="footsteps" size={20} color="#3b82f6" />
</View>
<Text style={styles.statValue}>{steps.toLocaleString()}</Text>
<Text style={styles.statLabel}>Steps</Text>
</View>
useEffect(() => {
const loadStatistics = async () => {
if (!user?.id) return;
<View style={styles.divider} />
try {
const stats = await getUserStatistics(user.id);
setStatistics(stats);
} catch (error) {
console.error("Failed to load statistics:", error);
} finally {
setLoading(false);
}
};
<View style={styles.statItem}>
<View style={[styles.iconContainer, { backgroundColor: 'rgba(239, 68, 68, 0.2)' }]}>
<Ionicons name="flame" size={20} color="#ef4444" />
</View>
<Text style={styles.statValue}>{calories}</Text>
<Text style={styles.statLabel}>Kcal</Text>
</View>
loadStatistics();
}, [user?.id]);
<View style={styles.divider} />
// Calculate weekly activity bars from weekly trend data
const getWeeklyBars = () => {
if (!statistics || statistics.weeklyTrend.length === 0) {
// Fallback mock data
return [0.4, 0.6, 0.3, 0.8, 0.5, 0.9, 0.7];
}
<View style={styles.statItem}>
<View style={[styles.iconContainer, { backgroundColor: 'rgba(16, 185, 129, 0.2)' }]}>
<Ionicons name="time" size={20} color="#10b981" />
</View>
<Text style={styles.statValue}>{duration}m</Text>
<Text style={styles.statLabel}>Active</Text>
</View>
</View>
// Get last 7 weeks and normalize to 0-1 scale
const last7Weeks = statistics.weeklyTrend.slice(-7);
const maxCheckIns = Math.max(...last7Weeks.map((w) => w.checkIns), 1);
{/* Simple Bar Chart Visualization */}
<View style={styles.chartContainer}>
{[0.4, 0.6, 0.3, 0.8, 0.5, 0.9, 0.7].map((height, index) => (
<View key={index} style={styles.barContainer}>
<LinearGradient
colors={theme.gradients.primaryVertical}
style={[styles.bar, { height: height * 60 }]}
/>
<Text style={styles.dayLabel}>
{['M', 'T', 'W', 'T', 'F', 'S', 'S'][index]}
</Text>
</View>
))}
</View>
</LinearGradient>
return last7Weeks.map((week) => {
// Normalize check-ins to 0.2-1.0 range for better visualization
const normalized = week.checkIns / maxCheckIns;
return Math.max(normalized * 0.8 + 0.2, 0.2);
});
};
const weeklyBars = getWeeklyBars();
const checkInsThisWeek = statistics?.attendance.checkInsThisWeek || 0;
const currentStreak = statistics?.attendance.currentStreak || 0;
return (
<View style={styles.container}>
<LinearGradient
colors={theme.gradients.dark}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={[styles.card, theme.shadows.medium]}
>
<View style={styles.header}>
<Text style={styles.title}>Daily Activity</Text>
<Ionicons name="stats-chart" size={20} color={theme.colors.primary} />
</View>
);
{loading ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="small" color={theme.colors.primary} />
</View>
) : (
<>
<View style={styles.statsRow}>
<View style={styles.statItem}>
<View
style={[
styles.iconContainer,
{ backgroundColor: "rgba(59, 130, 246, 0.2)" },
]}
>
<Ionicons name="checkmark-circle" size={20} color="#3b82f6" />
</View>
<Text style={styles.statValue}>{checkInsThisWeek}</Text>
<Text style={styles.statLabel}>This Week</Text>
</View>
<View style={styles.divider} />
<View style={styles.statItem}>
<View
style={[
styles.iconContainer,
{ backgroundColor: "rgba(239, 68, 68, 0.2)" },
]}
>
<Ionicons name="flame" size={20} color="#ef4444" />
</View>
<Text style={styles.statValue}>{calories}</Text>
<Text style={styles.statLabel}>Kcal</Text>
</View>
<View style={styles.divider} />
<View style={styles.statItem}>
<View
style={[
styles.iconContainer,
{ backgroundColor: "rgba(16, 185, 129, 0.2)" },
]}
>
<Ionicons name="trophy" size={20} color="#10b981" />
</View>
<Text style={styles.statValue}>{currentStreak}</Text>
<Text style={styles.statLabel}>Day Streak</Text>
</View>
</View>
{/* Weekly Bar Chart */}
<View style={styles.chartContainer}>
{weeklyBars.map((height, index) => (
<View key={index} style={styles.barContainer}>
<LinearGradient
colors={theme.gradients.primaryVertical}
style={[styles.bar, { height: height * 60 }]}
/>
<Text style={styles.dayLabel}>
{["M", "T", "W", "T", "F", "S", "S"][index % 7]}
</Text>
</View>
))}
</View>
</>
)}
</LinearGradient>
</View>
);
}
const styles = StyleSheet.create({
container: {
marginHorizontal: 20,
marginBottom: 20,
},
card: {
borderRadius: 24,
padding: 20,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.1)',
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 20,
},
title: {
fontSize: 18,
fontWeight: '700',
color: '#fff',
},
statsRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 24,
},
statItem: {
alignItems: 'center',
flex: 1,
},
iconContainer: {
width: 40,
height: 40,
borderRadius: 20,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 8,
},
statValue: {
fontSize: 20,
fontWeight: '700',
color: '#fff',
marginBottom: 2,
},
statLabel: {
fontSize: 12,
color: theme.colors.gray400,
fontWeight: '500',
},
divider: {
width: 1,
height: 40,
backgroundColor: 'rgba(255, 255, 255, 0.1)',
},
chartContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-end',
height: 80,
paddingTop: 10,
borderTopWidth: 1,
borderTopColor: 'rgba(255, 255, 255, 0.1)',
},
barContainer: {
alignItems: 'center',
gap: 8,
},
bar: {
width: 6,
borderRadius: 3,
opacity: 0.8,
},
dayLabel: {
fontSize: 10,
color: theme.colors.gray500,
fontWeight: '600',
},
container: {
marginHorizontal: 20,
marginBottom: 20,
},
card: {
borderRadius: 24,
padding: 20,
borderWidth: 1,
borderColor: "rgba(255, 255, 255, 0.1)",
},
header: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 20,
},
title: {
fontSize: 18,
fontWeight: "700",
color: "#fff",
},
loadingContainer: {
paddingVertical: 40,
alignItems: "center",
justifyContent: "center",
},
statsRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 24,
},
statItem: {
alignItems: "center",
flex: 1,
},
iconContainer: {
width: 40,
height: 40,
borderRadius: 20,
justifyContent: "center",
alignItems: "center",
marginBottom: 8,
},
statValue: {
fontSize: 20,
fontWeight: "700",
color: "#fff",
marginBottom: 2,
},
statLabel: {
fontSize: 12,
color: theme.colors.gray400,
fontWeight: "500",
},
divider: {
width: 1,
height: 40,
backgroundColor: "rgba(255, 255, 255, 0.1)",
},
chartContainer: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "flex-end",
height: 80,
paddingTop: 10,
borderTopWidth: 1,
borderTopColor: "rgba(255, 255, 255, 0.1)",
},
barContainer: {
alignItems: "center",
gap: 8,
},
bar: {
width: 6,
borderRadius: 3,
opacity: 0.8,
},
dayLabel: {
fontSize: 10,
color: theme.colors.gray500,
fontWeight: "600",
},
});
@@ -0,0 +1,303 @@
import React, { useState, useEffect } from "react";
import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
import { LinearGradient } from "expo-linear-gradient";
import { Ionicons } from "@expo/vector-icons";
import { theme } from "../styles/theme";
interface AttendanceRecord {
id: string;
checkInTime: string;
checkOutTime?: string | null;
duration?: number | null;
}
interface AttendanceCalendarProps {
attendanceRecords: AttendanceRecord[];
}
export function AttendanceCalendar({
attendanceRecords,
}: AttendanceCalendarProps) {
const [currentMonth, setCurrentMonth] = useState(new Date());
const [calendarDays, setCalendarDays] = useState<
Array<{ date: Date | null; hasAttendance: boolean; isToday: boolean }>
>([]);
useEffect(() => {
generateCalendar(currentMonth);
}, [currentMonth, attendanceRecords]);
const generateCalendar = (month: Date) => {
const year = month.getFullYear();
const monthIndex = month.getMonth();
// Get first day of month and number of days
const firstDay = new Date(year, monthIndex, 1);
const lastDay = new Date(year, monthIndex + 1, 0);
const daysInMonth = lastDay.getDate();
const startingDayOfWeek = firstDay.getDay();
// Create attendance lookup set
const attendanceDates = new Set(
attendanceRecords.map((record) =>
new Date(record.checkInTime).toDateString(),
),
);
const today = new Date().toDateString();
// Build calendar array
const days: Array<{
date: Date | null;
hasAttendance: boolean;
isToday: boolean;
}> = [];
// Add empty cells for days before month starts
for (let i = 0; i < startingDayOfWeek; i++) {
days.push({ date: null, hasAttendance: false, isToday: false });
}
// Add days of the month
for (let day = 1; day <= daysInMonth; day++) {
const date = new Date(year, monthIndex, day);
const dateString = date.toDateString();
days.push({
date,
hasAttendance: attendanceDates.has(dateString),
isToday: dateString === today,
});
}
setCalendarDays(days);
};
const goToPreviousMonth = () => {
setCurrentMonth(
new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1),
);
};
const goToNextMonth = () => {
setCurrentMonth(
new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1),
);
};
const monthName = currentMonth.toLocaleDateString("en-US", {
month: "long",
year: "numeric",
});
return (
<View style={styles.container}>
<LinearGradient
colors={[theme.colors.white, theme.colors.gray50]}
style={[styles.card, theme.shadows.medium]}
>
{/* Header */}
<View style={styles.header}>
<Text style={styles.title}>Attendance Calendar</Text>
</View>
{/* Month Navigation */}
<View style={styles.monthNav}>
<TouchableOpacity
onPress={goToPreviousMonth}
style={styles.navButton}
>
<Ionicons
name="chevron-back"
size={20}
color={theme.colors.primary}
/>
</TouchableOpacity>
<Text style={styles.monthText}>{monthName}</Text>
<TouchableOpacity onPress={goToNextMonth} style={styles.navButton}>
<Ionicons
name="chevron-forward"
size={20}
color={theme.colors.primary}
/>
</TouchableOpacity>
</View>
{/* Day Headers */}
<View style={styles.dayHeaders}>
{["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].map((day) => (
<Text key={day} style={styles.dayHeader}>
{day}
</Text>
))}
</View>
{/* Calendar Grid */}
<View style={styles.calendarGrid}>
{calendarDays.map((day, index) => (
<View key={index} style={styles.dayCell}>
{day.date ? (
<View
style={[
styles.dayContent,
day.isToday && styles.todayContent,
day.hasAttendance && styles.attendanceContent,
]}
>
<Text
style={[
styles.dayText,
day.isToday && styles.todayText,
day.hasAttendance && styles.attendanceText,
]}
>
{day.date.getDate()}
</Text>
{day.hasAttendance && <View style={styles.attendanceDot} />}
</View>
) : (
<View style={styles.emptyCell} />
)}
</View>
))}
</View>
{/* Legend */}
<View style={styles.legend}>
<View style={styles.legendItem}>
<View style={styles.legendDotAttendance} />
<Text style={styles.legendText}>Attended</Text>
</View>
<View style={styles.legendItem}>
<View style={styles.legendDotToday} />
<Text style={styles.legendText}>Today</Text>
</View>
</View>
</LinearGradient>
</View>
);
}
const styles = StyleSheet.create({
container: {
paddingHorizontal: 20,
marginBottom: 20,
},
card: {
borderRadius: theme.borderRadius["2xl"],
padding: 20,
},
header: {
marginBottom: 16,
},
title: {
fontSize: theme.typography.fontSize.xl,
fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.gray800,
},
monthNav: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 16,
paddingHorizontal: 8,
},
navButton: {
padding: 8,
},
monthText: {
fontSize: theme.typography.fontSize.lg,
fontWeight: theme.typography.fontWeight.semibold,
color: theme.colors.gray700,
},
dayHeaders: {
flexDirection: "row",
marginBottom: 8,
},
dayHeader: {
flex: 1,
textAlign: "center",
fontSize: theme.typography.fontSize.xs,
fontWeight: theme.typography.fontWeight.semibold,
color: theme.colors.gray500,
},
calendarGrid: {
flexDirection: "row",
flexWrap: "wrap",
},
dayCell: {
width: `${100 / 7}%`,
aspectRatio: 1,
padding: 2,
},
dayContent: {
flex: 1,
justifyContent: "center",
alignItems: "center",
borderRadius: theme.borderRadius.md,
position: "relative",
},
todayContent: {
backgroundColor: theme.colors.primaryLight,
borderWidth: 1,
borderColor: theme.colors.primary,
},
attendanceContent: {
backgroundColor: theme.colors.successLight,
},
emptyCell: {
flex: 1,
},
dayText: {
fontSize: theme.typography.fontSize.sm,
color: theme.colors.gray700,
fontWeight: theme.typography.fontWeight.medium,
},
todayText: {
color: theme.colors.white,
fontWeight: theme.typography.fontWeight.bold,
},
attendanceText: {
color: theme.colors.white,
fontWeight: theme.typography.fontWeight.bold,
},
attendanceDot: {
position: "absolute",
bottom: 2,
width: 4,
height: 4,
borderRadius: 2,
backgroundColor: theme.colors.white,
},
legend: {
flexDirection: "row",
justifyContent: "center",
gap: 20,
marginTop: 16,
paddingTop: 16,
borderTopWidth: 1,
borderTopColor: theme.colors.gray200,
},
legendItem: {
flexDirection: "row",
alignItems: "center",
gap: 6,
},
legendDotAttendance: {
width: 12,
height: 12,
borderRadius: 6,
backgroundColor: theme.colors.successLight,
},
legendDotToday: {
width: 12,
height: 12,
borderRadius: 6,
backgroundColor: theme.colors.primaryLight,
borderWidth: 1,
borderColor: theme.colors.primary,
},
legendText: {
fontSize: theme.typography.fontSize.xs,
color: theme.colors.gray600,
},
});
@@ -0,0 +1,100 @@
import React from "react";
import { View, Text, StyleSheet, Dimensions } from "react-native";
import { PieChart } from "react-native-chart-kit";
import { theme } from "../styles/theme";
interface GoalTypeData {
goalType: string;
count: number;
}
interface GoalTypeBreakdownChartProps {
data: GoalTypeData[];
title?: string;
}
export function GoalTypeBreakdownChart({
data,
title = "Goals by Type",
}: GoalTypeBreakdownChartProps) {
const screenWidth = Dimensions.get("window").width;
// Color palette for different goal types
const colors = [
"#3b82f6", // Blue
"#10b981", // Green
"#f59e0b", // Orange
"#8b5cf6", // Purple
"#ec4899", // Pink
"#06b6d4", // Cyan
];
// Prepare chart data
const chartData = data.map((item, index) => ({
name: item.goalType,
count: item.count,
color: colors[index % colors.length],
legendFontColor: theme.colors.gray600,
legendFontSize: 12,
}));
const chartConfig = {
color: (opacity = 1) => `rgba(59, 130, 246, ${opacity})`,
};
if (data.length === 0) {
return (
<View style={styles.container}>
<Text style={styles.title}>{title}</Text>
<View style={styles.emptyState}>
<Text style={styles.emptyText}>No goals yet</Text>
</View>
</View>
);
}
return (
<View style={styles.container}>
<Text style={styles.title}>{title}</Text>
<View style={styles.chartContainer}>
<PieChart
data={chartData}
width={screenWidth - 60}
height={200}
chartConfig={chartConfig}
accessor="count"
backgroundColor="transparent"
paddingLeft="15"
absolute
/>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
backgroundColor: theme.colors.white,
borderRadius: theme.borderRadius.xl,
padding: 16,
marginBottom: 16,
...theme.shadows.medium,
},
title: {
fontSize: theme.typography.fontSize.lg,
fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.gray700,
marginBottom: 12,
},
chartContainer: {
alignItems: "center",
},
emptyState: {
paddingVertical: 40,
alignItems: "center",
},
emptyText: {
fontSize: theme.typography.fontSize.sm,
color: theme.colors.gray400,
},
});
@@ -0,0 +1,132 @@
import React from "react";
import { View, Text, StyleSheet, Dimensions } from "react-native";
import { LineChart } from "react-native-chart-kit";
import { theme } from "../styles/theme";
import type { WeeklyTrendData } from "../api/types";
interface WeeklyProgressChartProps {
weeklyData: WeeklyTrendData[];
title?: string;
}
export function WeeklyProgressChart({
weeklyData,
title = "Weekly Progress",
}: WeeklyProgressChartProps) {
const screenWidth = Dimensions.get("window").width;
// Prepare chart data
const labels = weeklyData.map((week) => week.weekLabel);
const checkInsData = weeklyData.map((week) => week.checkIns);
const goalsCompletedData = weeklyData.map((week) => week.goalsCompleted);
const avgProgressData = weeklyData.map((week) => week.averageProgress);
const chartConfig = {
backgroundColor: theme.colors.white,
backgroundGradientFrom: theme.colors.white,
backgroundGradientTo: theme.colors.white,
decimalPlaces: 0,
color: (opacity = 1) => `rgba(59, 130, 246, ${opacity})`,
labelColor: (opacity = 1) => `rgba(107, 114, 128, ${opacity})`,
style: {
borderRadius: theme.borderRadius.lg,
},
propsForDots: {
r: "4",
strokeWidth: "2",
stroke: theme.colors.primary,
},
};
const data = {
labels,
datasets: [
{
data: checkInsData,
color: (opacity = 1) => `rgba(59, 130, 246, ${opacity})`, // Blue for check-ins
strokeWidth: 2,
},
{
data: goalsCompletedData,
color: (opacity = 1) => `rgba(16, 185, 129, ${opacity})`, // Green for goals
strokeWidth: 2,
},
],
legend: ["Check-ins", "Goals Completed"],
};
return (
<View style={styles.container}>
<Text style={styles.title}>{title}</Text>
<View style={styles.chartContainer}>
<LineChart
data={data}
width={screenWidth - 60}
height={220}
chartConfig={chartConfig}
bezier
style={styles.chart}
withInnerLines={true}
withOuterLines={true}
withVerticalLabels={true}
withHorizontalLabels={true}
fromZero={true}
/>
</View>
<View style={styles.legend}>
<View style={styles.legendItem}>
<View style={[styles.legendDot, { backgroundColor: "#3b82f6" }]} />
<Text style={styles.legendText}>Check-ins</Text>
</View>
<View style={styles.legendItem}>
<View style={[styles.legendDot, { backgroundColor: "#10b981" }]} />
<Text style={styles.legendText}>Goals Completed</Text>
</View>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
backgroundColor: theme.colors.white,
borderRadius: theme.borderRadius.xl,
padding: 16,
marginBottom: 16,
...theme.shadows.medium,
},
title: {
fontSize: theme.typography.fontSize.lg,
fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.gray700,
marginBottom: 12,
},
chartContainer: {
alignItems: "center",
marginBottom: 12,
},
chart: {
marginVertical: 8,
borderRadius: theme.borderRadius.lg,
},
legend: {
flexDirection: "row",
justifyContent: "center",
gap: 20,
paddingTop: 8,
},
legendItem: {
flexDirection: "row",
alignItems: "center",
gap: 6,
},
legendDot: {
width: 10,
height: 10,
borderRadius: 5,
},
legendText: {
fontSize: theme.typography.fontSize.sm,
color: theme.colors.gray500,
},
});
@@ -0,0 +1,205 @@
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 { useUser } from "@clerk/clerk-expo";
import { getUserStatistics } from "../api/statistics";
import type { WeeklyTrendData } from "../api/types";
export function WeeklyProgressWidget() {
const { user } = useUser();
const [weeklyData, setWeeklyData] = useState<WeeklyTrendData[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const loadWeeklyData = async () => {
if (!user?.id) return;
try {
const stats = await getUserStatistics(user.id);
// Get last 4 weeks for compact display
const last4Weeks = stats.weeklyTrend.slice(-4);
setWeeklyData(last4Weeks);
} catch (error) {
console.error("Failed to load weekly data:", error);
} finally {
setLoading(false);
}
};
loadWeeklyData();
}, [user?.id]);
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",
},
});