refinenments

This commit is contained in:
echo 2026-03-12 20:19:48 +01:00
parent 5d6166df1b
commit 064dafad57
6 changed files with 635 additions and 245 deletions

Binary file not shown.

View File

@ -258,58 +258,95 @@ export default function GoalsScreen() {
)} )}
{/* Analytics Section */} {/* Analytics Section */}
{statistics && ( {statistics &&
(statistics.weeklyTrend.length > 0 ||
statistics.goals.goalsByType.length > 0) && (
<View style={styles.section}> <View style={styles.section}>
<MinimalCard variant="default">
<TouchableOpacity <TouchableOpacity
style={styles.analyticsHeader}
onPress={() => setShowAnalytics(!showAnalytics)} onPress={() => setShowAnalytics(!showAnalytics)}
activeOpacity={0.7} activeOpacity={0.85}
> >
<MinimalCard variant="elevated" style={styles.analyticsCard}>
<View style={styles.analyticsHeader}>
<View style={styles.analyticsHeaderLeft}> <View style={styles.analyticsHeaderLeft}>
<Ionicons <View
name="bar-chart-outline"
size={20}
color={colors.primary}
/>
<Text
style={[ style={[
typography.h3, styles.analyticsIcon,
{ color: colors.textPrimary, marginLeft: 8 }, { backgroundColor: `${colors.primary}15` },
]} ]}
> >
📈 Progress Analytics <Ionicons
name="bar-chart"
size={24}
color={colors.primary}
/>
</View>
<View>
<Text
style={[typography.h3, { color: colors.textPrimary }]}
>
Progress Analytics
</Text>
<Text
style={[
typography.caption,
{ color: colors.textTertiary, marginTop: 2 },
]}
>
{showAnalytics ? "Tap to collapse" : "Tap to expand"}
</Text> </Text>
</View> </View>
</View>
<View
style={[
styles.analyticsToggle,
{ backgroundColor: colors.surfaceElevated },
]}
>
<Ionicons <Ionicons
name={showAnalytics ? "chevron-up" : "chevron-down"} name={showAnalytics ? "chevron-up" : "chevron-down"}
size={20} size={20}
color={colors.textTertiary} color={colors.textSecondary}
/> />
</TouchableOpacity> </View>
</View>
{showAnalytics && ( {showAnalytics && (
<View <View style={styles.analyticsContent}>
{statistics.weeklyTrend.length > 0 && (
<View style={styles.chartSection}>
<Text
style={[ style={[
styles.analyticsContent, typography.h4,
{ borderTopColor: colors.border }, { color: colors.textPrimary, marginBottom: 16 },
]} ]}
> >
{statistics.weeklyTrend.length > 0 && ( Weekly Trend
</Text>
<WeeklyProgressChart <WeeklyProgressChart
weeklyData={statistics.weeklyTrend} weeklyData={statistics.weeklyTrend}
title="8-Week Trend"
/> />
</View>
)} )}
{statistics.goals.goalsByType.length > 0 && ( {statistics.goals.goalsByType.length > 0 && (
<View style={styles.chartSection}>
<Text
style={[
typography.h4,
{ color: colors.textPrimary, marginBottom: 16 },
]}
>
Goals by Type
</Text>
<GoalTypeBreakdownChart <GoalTypeBreakdownChart
data={statistics.goals.goalsByType} data={statistics.goals.goalsByType}
title="Goals by Type"
/> />
</View>
)} )}
</View> </View>
)} )}
</MinimalCard> </MinimalCard>
</TouchableOpacity>
</View> </View>
)} )}
@ -439,6 +476,9 @@ const styles = StyleSheet.create({
paddingHorizontal: 12, paddingHorizontal: 12,
borderRadius: 20, borderRadius: 20,
}, },
analyticsCard: {
padding: 20,
},
analyticsHeader: { analyticsHeader: {
flexDirection: "row", flexDirection: "row",
justifyContent: "space-between", justifyContent: "space-between",
@ -448,13 +488,32 @@ const styles = StyleSheet.create({
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
}, },
analyticsIcon: {
width: 48,
height: 48,
borderRadius: 14,
justifyContent: "center",
alignItems: "center",
marginRight: 14,
},
analyticsToggle: {
width: 36,
height: 36,
borderRadius: 10,
justifyContent: "center",
alignItems: "center",
},
analyticsContent: { analyticsContent: {
paddingTop: 16, paddingTop: 24,
marginTop: 16, marginTop: 20,
borderTopWidth: 1, borderTopWidth: 1,
borderTopColor: "rgba(0,0,0,0.05)",
},
chartSection: {
marginBottom: 20,
}, },
goalsList: { goalsList: {
gap: 12, gap: 16,
}, },
emptyState: { emptyState: {
alignItems: "center", alignItems: "center",

View File

@ -19,6 +19,7 @@ import { MinimalButton } from "../../components/MinimalButton";
import { Badge } from "../../components/Badge"; import { Badge } from "../../components/Badge";
import { IconContainer } from "../../components/IconContainer"; import { IconContainer } from "../../components/IconContainer";
import { API_BASE_URL, API_ENDPOINTS } from "../../config/api"; import { API_BASE_URL, API_ENDPOINTS } from "../../config/api";
import { fitnessProfileApi, FitnessProfile } from "../../api/fitnessProfile";
import log from "../../utils/logger"; import log from "../../utils/logger";
export default function ProfileScreen() { export default function ProfileScreen() {
@ -35,6 +36,11 @@ export default function ProfileScreen() {
const [selectedGymId, setSelectedGymId] = useState<string | null>(null); const [selectedGymId, setSelectedGymId] = useState<string | null>(null);
const [currentGymId, setCurrentGymId] = useState<string | null>(null); const [currentGymId, setCurrentGymId] = useState<string | null>(null);
const [currentGymName, setCurrentGymName] = useState<string | null>(null); const [currentGymName, setCurrentGymName] = useState<string | null>(null);
const [showGymDropdown, setShowGymDropdown] = useState(false);
const [fitnessProfile, setFitnessProfile] = useState<FitnessProfile | null>(
null,
);
const [profileLoading, setProfileLoading] = useState(false);
useEffect(() => { useEffect(() => {
const gid = const gid =
@ -49,8 +55,23 @@ export default function ProfileScreen() {
useEffect(() => { useEffect(() => {
loadGyms(); loadGyms();
loadFitnessProfile();
}, []); }, []);
const loadFitnessProfile = async () => {
try {
setProfileLoading(true);
const token = await getToken();
if (!token) return;
const profile = await fitnessProfileApi.getFitnessProfile(token);
setFitnessProfile(profile);
} catch (error) {
log.error("Failed to load fitness profile", error);
} finally {
setProfileLoading(false);
}
};
const loadGyms = async () => { const loadGyms = async () => {
try { try {
setGymsLoading(true); setGymsLoading(true);
@ -115,8 +136,12 @@ export default function ProfileScreen() {
const handleApplyGym = async () => { const handleApplyGym = async () => {
try { try {
const token = await getToken(); const token = await getToken();
const url = `${API_BASE_URL}${API_ENDPOINTS.USERS}/gym`; const url = `${API_BASE_URL}${API_ENDPOINTS.USERS.GYM}`;
log.debug("Updating gym selection", { url, gymId: selectedGymId }); log.debug("Updating gym selection", {
url,
gymId: selectedGymId,
token: token ? "present" : "missing",
});
const res = await fetch(url, { const res = await fetch(url, {
method: "PATCH", method: "PATCH",
headers: { headers: {
@ -331,29 +356,145 @@ export default function ProfileScreen() {
onPress={() => router.push("/personal-details")} onPress={() => router.push("/personal-details")}
/> />
<View style={[styles.divider, { backgroundColor: colors.border }]} /> <View style={[styles.divider, { backgroundColor: colors.border }]} />
<ListItem
title="Fitness Profile" {/* Fitness Profile Card */}
leftIcon={ <TouchableOpacity
<IconContainer onPress={() => router.push("/fitness-profile")}
variant="colored" activeOpacity={0.85}
backgroundColor={`${colors.success}20`}
> >
<Ionicons <MinimalCard variant="elevated" style={styles.fitnessProfileCard}>
name="fitness-outline" <View style={styles.fitnessProfileHeader}>
size={20} <View
color={colors.success} style={[
styles.fitnessProfileIcon,
{ backgroundColor: colors.success },
]}
>
<Ionicons name="fitness" size={24} color={colors.white} />
</View>
<View style={{ flex: 1, marginLeft: 14 }}>
<Text style={[typography.h4, { color: colors.textPrimary }]}>
Fitness Profile
</Text>
<Text
style={[
typography.caption,
{ color: colors.textTertiary, marginTop: 2 },
]}
>
{fitnessProfile ? "Tap to edit" : "Set up your profile"}
</Text>
</View>
<View
style={[
styles.editButton,
{ backgroundColor: colors.primary },
]}
>
<Ionicons name="pencil" size={16} color={colors.white} />
</View>
</View>
{profileLoading ? (
<View style={styles.profileLoading}>
<ActivityIndicator color={colors.primary} />
</View>
) : fitnessProfile ? (
<View
style={[
styles.fitnessProfileStats,
{ borderTopColor: colors.border },
]}
>
<View style={styles.profileStat}>
<Text
style={[
typography.statLarge,
{ color: colors.primary, fontSize: 28 },
]}
>
{fitnessProfile.height || "-"}
</Text>
<Text
style={[
typography.caption,
{ color: colors.textTertiary },
]}
>
Height (cm)
</Text>
</View>
<View
style={[
styles.profileStatDivider,
{ backgroundColor: colors.border },
]}
/> />
</IconContainer> <View style={styles.profileStat}>
} <Text
rightElement={ style={[
<Ionicons typography.statLarge,
name="chevron-forward" { color: colors.success, fontSize: 28 },
size={20} ]}
color={colors.textTertiary} >
{fitnessProfile.weight || "-"}
</Text>
<Text
style={[
typography.caption,
{ color: colors.textTertiary },
]}
>
Weight (kg)
</Text>
</View>
<View
style={[
styles.profileStatDivider,
{ backgroundColor: colors.border },
]}
/> />
} <View style={styles.profileStat}>
<Text
style={[
typography.statLarge,
{ color: colors.warning, fontSize: 28 },
]}
>
{fitnessProfile.age || "-"}
</Text>
<Text
style={[
typography.caption,
{ color: colors.textTertiary },
]}
>
Age
</Text>
</View>
</View>
) : (
<View
style={[styles.noProfile, { borderTopColor: colors.border }]}
>
<Text
style={[typography.body, { color: colors.textSecondary }]}
>
Complete your fitness profile to get personalized
recommendations
</Text>
<MinimalButton
title="Set Up Profile"
variant="primary"
size="sm"
style={{ marginTop: 12 }}
onPress={() => router.push("/fitness-profile")} onPress={() => router.push("/fitness-profile")}
/> />
</View>
)}
</MinimalCard>
</TouchableOpacity>
<View style={[styles.divider, { backgroundColor: colors.border }]} /> <View style={[styles.divider, { backgroundColor: colors.border }]} />
<ListItem <ListItem
title="Notifications" title="Notifications"
@ -382,24 +523,22 @@ export default function ProfileScreen() {
{/* Gym Selection */} {/* Gym Selection */}
<View style={styles.section}> <View style={styles.section}>
<View style={styles.sectionHeader}>
<Text style={[typography.h3, { color: colors.textPrimary }]}>
Gym Selection
</Text>
<TouchableOpacity onPress={loadGyms}>
<Text <Text
style={[ style={[
typography.body, typography.h3,
{ color: colors.primary, fontWeight: "600" }, { color: colors.textPrimary, marginBottom: 12 },
]} ]}
> >
Refresh Gym Selection
</Text> </Text>
</TouchableOpacity>
</View> <TouchableOpacity
<MinimalCard variant="default"> onPress={() => setShowGymDropdown(!showGymDropdown)}
{currentGymName && ( activeOpacity={0.85}
<View style={styles.currentGym}> >
<MinimalCard variant="bordered" style={styles.dropdownCard}>
<View style={styles.dropdownHeader}>
<View>
<Text <Text
style={[typography.caption, { color: colors.textTertiary }]} style={[typography.caption, { color: colors.textTertiary }]}
> >
@ -407,40 +546,48 @@ export default function ProfileScreen() {
</Text> </Text>
<Text <Text
style={[ style={[
typography.h3, typography.bodyEmphasis,
{ color: colors.textPrimary, marginTop: 4 }, { color: colors.textPrimary, marginTop: 2 },
]} ]}
> >
{currentGymName} {currentGymName || "No gym selected"}
</Text> </Text>
</View> </View>
)} <View
style={[
styles.dropdownIcon,
{ backgroundColor: colors.surfaceElevated },
]}
>
<Ionicons
name={showGymDropdown ? "chevron-up" : "chevron-down"}
size={20}
color={colors.textSecondary}
/>
</View>
</View>
</MinimalCard>
</TouchableOpacity>
{showGymDropdown && (
<MinimalCard variant="elevated" style={styles.dropdownOptions}>
{gymsLoading ? ( {gymsLoading ? (
<View style={styles.loadingContainer}> <View style={styles.loadingContainer}>
<ActivityIndicator color={colors.primary} /> <ActivityIndicator color={colors.primary} />
</View> </View>
) : ( ) : (
<> <>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
style={styles.gymScroll}
contentContainerStyle={styles.gymScrollContent}
>
<TouchableOpacity <TouchableOpacity
style={[ style={[
styles.gymChip, styles.dropdownOption,
{ selectedGymId === null && {
backgroundColor: backgroundColor: `${colors.primary}15`,
selectedGymId === null
? `${colors.primary}20`
: colors.surfaceElevated,
borderColor:
selectedGymId === null ? colors.primary : colors.border,
}, },
]} ]}
onPress={() => setSelectedGymId(null)} onPress={() => {
setSelectedGymId(null);
setShowGymDropdown(false);
}}
> >
<Text <Text
style={[ style={[
@ -456,25 +603,29 @@ export default function ProfileScreen() {
> >
No Gym No Gym
</Text> </Text>
{selectedGymId === null && (
<Ionicons
name="checkmark"
size={20}
color={colors.primary}
/>
)}
</TouchableOpacity> </TouchableOpacity>
{gyms.map((gym) => ( {gyms.map((gym) => (
<TouchableOpacity <TouchableOpacity
key={gym.id} key={gym.id}
style={[ style={[
styles.gymChip, styles.dropdownOption,
{ selectedGymId === gym.id && {
backgroundColor: backgroundColor: `${colors.primary}15`,
selectedGymId === gym.id
? `${colors.primary}20`
: colors.surfaceElevated,
borderColor:
selectedGymId === gym.id
? colors.primary
: colors.border,
}, },
]} ]}
onPress={() => setSelectedGymId(gym.id)} onPress={() => {
setSelectedGymId(gym.id);
setShowGymDropdown(false);
}}
> >
<View style={{ flex: 1 }}>
<Text <Text
style={[ style={[
typography.body, typography.body,
@ -483,24 +634,47 @@ export default function ProfileScreen() {
selectedGymId === gym.id selectedGymId === gym.id
? colors.primary ? colors.primary
: colors.textSecondary, : colors.textSecondary,
fontWeight: selectedGymId === gym.id ? "600" : "400", fontWeight:
selectedGymId === gym.id ? "600" : "400",
}, },
]} ]}
> >
{gym.name} {gym.name}
</Text> </Text>
{gym.location && (
<Text
style={[
typography.caption,
{ color: colors.textTertiary, marginTop: 2 },
]}
>
{gym.location}
</Text>
)}
</View>
{selectedGymId === gym.id && (
<Ionicons
name="checkmark"
size={20}
color={colors.primary}
/>
)}
</TouchableOpacity> </TouchableOpacity>
))} ))}
</ScrollView> {selectedGymId !== currentGymId && (
<MinimalButton <MinimalButton
title="Apply Selection" title="Apply Selection"
onPress={handleApplyGym} onPress={handleApplyGym}
variant="primary" variant="primary"
style={{ marginTop: 16 }} size="lg"
fullWidth
style={{ marginTop: 12 }}
/> />
)}
</> </>
)} )}
</MinimalCard> </MinimalCard>
)}
</View> </View>
{/* Support */} {/* Support */}
@ -651,4 +825,78 @@ const styles = StyleSheet.create({
borderWidth: 1.5, borderWidth: 1.5,
marginRight: 8, marginRight: 8,
}, },
dropdownCard: {
padding: 16,
},
dropdownHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
dropdownIcon: {
width: 36,
height: 36,
borderRadius: 10,
justifyContent: "center",
alignItems: "center",
},
dropdownOptions: {
marginTop: 8,
padding: 8,
},
dropdownOption: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingVertical: 14,
paddingHorizontal: 16,
borderRadius: 12,
marginBottom: 4,
},
fitnessProfileCard: {
padding: 16,
},
fitnessProfileHeader: {
flexDirection: "row",
alignItems: "center",
},
fitnessProfileIcon: {
width: 48,
height: 48,
borderRadius: 14,
justifyContent: "center",
alignItems: "center",
},
editButton: {
width: 32,
height: 32,
borderRadius: 8,
justifyContent: "center",
alignItems: "center",
},
profileLoading: {
paddingVertical: 20,
alignItems: "center",
},
fitnessProfileStats: {
flexDirection: "row",
justifyContent: "space-around",
marginTop: 20,
paddingTop: 16,
borderTopWidth: 1,
},
profileStat: {
alignItems: "center",
flex: 1,
},
profileStatDivider: {
width: 1,
height: 40,
},
noProfile: {
marginTop: 16,
paddingTop: 16,
borderTopWidth: 1,
alignItems: "center",
},
}); });

View File

@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { View, Text, StyleSheet, Dimensions } from "react-native"; import { View, Text, StyleSheet, Dimensions } from "react-native";
import { PieChart } from "react-native-chart-kit"; import { PieChart } from "react-native-chart-kit";
import { theme } from "../styles/theme"; import { useTheme } from "../contexts/ThemeContext";
interface GoalTypeData { interface GoalTypeData {
goalType: string; goalType: string;
@ -17,37 +17,40 @@ export function GoalTypeBreakdownChart({
data, data,
title = "Goals by Type", title = "Goals by Type",
}: GoalTypeBreakdownChartProps) { }: GoalTypeBreakdownChartProps) {
const { colors, typography } = useTheme();
const screenWidth = Dimensions.get("window").width; const screenWidth = Dimensions.get("window").width;
// Color palette for different goal types const chartColors = [
const colors = [ colors.primary,
"#3b82f6", // Blue colors.success,
"#10b981", // Green colors.warning,
"#f59e0b", // Orange colors.accent,
"#8b5cf6", // Purple colors.secondary,
"#ec4899", // Pink colors.info,
"#06b6d4", // Cyan
]; ];
// Prepare chart data
const chartData = data.map((item, index) => ({ const chartData = data.map((item, index) => ({
name: item.goalType, name: item.goalType
.replace(/_/g, " ")
.replace(/\b\w/g, (l) => l.toUpperCase()),
count: item.count, count: item.count,
color: colors[index % colors.length], color: chartColors[index % chartColors.length],
legendFontColor: theme.colors.gray600, legendFontColor: colors.textSecondary,
legendFontSize: 12, legendFontSize: 12,
})); }));
const chartConfig = { const totalGoals = data.reduce((sum, item) => sum + item.count, 0);
color: (opacity = 1) => `rgba(59, 130, 246, ${opacity})`,
};
if (data.length === 0) { if (data.length === 0) {
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Text style={styles.title}>{title}</Text> <Text style={[typography.h4, { color: colors.textPrimary }]}>
{title}
</Text>
<View style={styles.emptyState}> <View style={styles.emptyState}>
<Text style={styles.emptyText}>No goals yet</Text> <Text style={[typography.body, { color: colors.textTertiary }]}>
No goals yet
</Text>
</View> </View>
</View> </View>
); );
@ -55,46 +58,123 @@ export function GoalTypeBreakdownChart({
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Text style={styles.title}>{title}</Text> <Text
style={[typography.h4, { color: colors.textPrimary, marginBottom: 16 }]}
>
{title}
</Text>
{/* Summary Stats */}
<View style={styles.summaryRow}>
<View style={styles.summaryItem}>
<Text
style={[
typography.statLarge,
{ color: colors.primary, fontSize: 32 },
]}
>
{totalGoals}
</Text>
<Text style={[typography.caption, { color: colors.textTertiary }]}>
Total Goals
</Text>
</View>
<View style={styles.summaryItem}>
<Text
style={[
typography.statLarge,
{ color: colors.success, fontSize: 32 },
]}
>
{data.length}
</Text>
<Text style={[typography.caption, { color: colors.textTertiary }]}>
Types
</Text>
</View>
</View>
<View style={styles.chartContainer}> <View style={styles.chartContainer}>
<PieChart <PieChart
data={chartData} data={chartData}
width={screenWidth - 60} width={screenWidth - 80}
height={200} height={160}
chartConfig={chartConfig} chartConfig={{
color: (opacity = 1) => `rgba(0, 0, 0, ${opacity})`,
}}
accessor="count" accessor="count"
backgroundColor="transparent" backgroundColor="transparent"
paddingLeft="15" paddingLeft="15"
absolute absolute
/> />
</View> </View>
{/* Legend */}
<View style={styles.legendGrid}>
{data.map((item, index) => (
<View key={item.goalType} style={styles.legendItem}>
<View
style={[
styles.legendDot,
{ backgroundColor: chartColors[index % chartColors.length] },
]}
/>
<Text
style={[
typography.caption,
{ color: colors.textSecondary, flex: 1 },
]}
numberOfLines={1}
>
{item.goalType
.replace(/_/g, " ")
.replace(/\b\w/g, (l) => l.toUpperCase())}
</Text>
<Text
style={[typography.bodyEmphasis, { color: colors.textPrimary }]}
>
{item.count}
</Text>
</View>
))}
</View>
</View> </View>
); );
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
backgroundColor: theme.colors.white, borderRadius: 16,
borderRadius: theme.borderRadius.xl, padding: 4,
padding: 16,
marginBottom: 16,
...theme.shadows.medium,
}, },
title: { summaryRow: {
fontSize: theme.typography.fontSize.lg, flexDirection: "row",
fontWeight: theme.typography.fontWeight.bold, justifyContent: "space-around",
color: theme.colors.gray700, marginBottom: 16,
marginBottom: 12, paddingVertical: 12,
},
summaryItem: {
alignItems: "center",
}, },
chartContainer: { chartContainer: {
alignItems: "center", alignItems: "center",
marginBottom: 16,
},
legendGrid: {
gap: 10,
},
legendItem: {
flexDirection: "row",
alignItems: "center",
gap: 10,
},
legendDot: {
width: 12,
height: 12,
borderRadius: 6,
}, },
emptyState: { emptyState: {
paddingVertical: 40, paddingVertical: 32,
alignItems: "center", alignItems: "center",
}, },
emptyText: {
fontSize: theme.typography.fontSize.sm,
color: theme.colors.gray400,
},
}); });

View File

@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { View, Text, StyleSheet, Dimensions } from "react-native"; import { View, Text, StyleSheet, Dimensions } from "react-native";
import { LineChart } from "react-native-chart-kit"; import { LineChart } from "react-native-chart-kit";
import { theme } from "../styles/theme"; import { useTheme } from "../contexts/ThemeContext";
import type { WeeklyTrendData } from "../api/types"; import type { WeeklyTrendData } from "../api/types";
interface WeeklyProgressChartProps { interface WeeklyProgressChartProps {
@ -13,28 +13,26 @@ export function WeeklyProgressChart({
weeklyData, weeklyData,
title = "Weekly Progress", title = "Weekly Progress",
}: WeeklyProgressChartProps) { }: WeeklyProgressChartProps) {
const { colors, typography } = useTheme();
const screenWidth = Dimensions.get("window").width; const screenWidth = Dimensions.get("window").width;
// Prepare chart data const labels = weeklyData.map((week: WeeklyTrendData) => week.weekLabel);
const labels = weeklyData.map((week) => week.weekLabel); const checkInsData = weeklyData.map((week: WeeklyTrendData) => week.checkIns);
const checkInsData = weeklyData.map((week) => week.checkIns); const goalsCompletedData = weeklyData.map(
const goalsCompletedData = weeklyData.map((week) => week.goalsCompleted); (week: WeeklyTrendData) => week.goalsCompleted,
const avgProgressData = weeklyData.map((week) => week.averageProgress); );
const chartConfig = { const chartConfig = {
backgroundColor: theme.colors.white, backgroundColor: colors.surface,
backgroundGradientFrom: theme.colors.white, backgroundGradientFrom: colors.surface,
backgroundGradientTo: theme.colors.white, backgroundGradientTo: colors.surface,
decimalPlaces: 0, decimalPlaces: 0,
color: (opacity = 1) => `rgba(59, 130, 246, ${opacity})`, color: (opacity = 1) => `rgba(0, 102, 255, ${opacity})`,
labelColor: (opacity = 1) => `rgba(107, 114, 128, ${opacity})`, labelColor: (opacity = 1) => colors.textTertiary,
style: {
borderRadius: theme.borderRadius.lg,
},
propsForDots: { propsForDots: {
r: "4", r: "5",
strokeWidth: "2", strokeWidth: "2",
stroke: theme.colors.primary, stroke: colors.primary,
}, },
}; };
@ -43,31 +41,40 @@ export function WeeklyProgressChart({
datasets: [ datasets: [
{ {
data: checkInsData, data: checkInsData,
color: (opacity = 1) => `rgba(59, 130, 246, ${opacity})`, // Blue for check-ins color: () => colors.primary,
strokeWidth: 2, strokeWidth: 3,
}, },
{ {
data: goalsCompletedData, data: goalsCompletedData,
color: (opacity = 1) => `rgba(16, 185, 129, ${opacity})`, // Green for goals color: () => colors.success,
strokeWidth: 2, strokeWidth: 3,
}, },
], ],
legend: ["Check-ins", "Goals Completed"], legend: ["Check-ins", "Goals"],
}; };
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Text style={styles.title}>{title}</Text> {title && (
<Text
style={[
typography.h4,
{ color: colors.textPrimary, marginBottom: 16 },
]}
>
{title}
</Text>
)}
<View style={styles.chartContainer}> <View style={styles.chartContainer}>
<LineChart <LineChart
data={data} data={data}
width={screenWidth - 60} width={screenWidth - 80}
height={220} height={180}
chartConfig={chartConfig} chartConfig={chartConfig}
bezier bezier
style={styles.chart} style={styles.chart}
withInnerLines={true} withInnerLines={true}
withOuterLines={true} withOuterLines={false}
withVerticalLabels={true} withVerticalLabels={true}
withHorizontalLabels={true} withHorizontalLabels={true}
fromZero={true} fromZero={true}
@ -75,12 +82,20 @@ export function WeeklyProgressChart({
</View> </View>
<View style={styles.legend}> <View style={styles.legend}>
<View style={styles.legendItem}> <View style={styles.legendItem}>
<View style={[styles.legendDot, { backgroundColor: "#3b82f6" }]} /> <View
<Text style={styles.legendText}>Check-ins</Text> style={[styles.legendDot, { backgroundColor: colors.primary }]}
/>
<Text style={[typography.caption, { color: colors.textSecondary }]}>
Check-ins
</Text>
</View> </View>
<View style={styles.legendItem}> <View style={styles.legendItem}>
<View style={[styles.legendDot, { backgroundColor: "#10b981" }]} /> <View
<Text style={styles.legendText}>Goals Completed</Text> style={[styles.legendDot, { backgroundColor: colors.success }]}
/>
<Text style={[typography.caption, { color: colors.textSecondary }]}>
Goals
</Text>
</View> </View>
</View> </View>
</View> </View>
@ -89,17 +104,8 @@ export function WeeklyProgressChart({
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
backgroundColor: theme.colors.white, borderRadius: 16,
borderRadius: theme.borderRadius.xl, padding: 4,
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: { chartContainer: {
alignItems: "center", alignItems: "center",
@ -107,26 +113,22 @@ const styles = StyleSheet.create({
}, },
chart: { chart: {
marginVertical: 8, marginVertical: 8,
borderRadius: theme.borderRadius.lg, borderRadius: 12,
}, },
legend: { legend: {
flexDirection: "row", flexDirection: "row",
justifyContent: "center", justifyContent: "center",
gap: 20, gap: 24,
paddingTop: 8, paddingTop: 8,
}, },
legendItem: { legendItem: {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
gap: 6, gap: 8,
}, },
legendDot: { legendDot: {
width: 10, width: 10,
height: 10, height: 10,
borderRadius: 5, borderRadius: 5,
}, },
legendText: {
fontSize: theme.typography.fontSize.sm,
color: theme.colors.gray500,
},
}); });

View File

@ -35,6 +35,7 @@ export const API_ENDPOINTS = {
USERS: { USERS: {
LIST: "/api/users", LIST: "/api/users",
STATISTICS: "/api/users/statistics", STATISTICS: "/api/users/statistics",
GYM: "/api/users/gym",
}, },
GYMS: "/api/gyms", GYMS: "/api/gyms",
ATTENDANCE: { ATTENDANCE: {