refinenments
This commit is contained in:
parent
5d6166df1b
commit
064dafad57
Binary file not shown.
@ -258,60 +258,97 @@ export default function GoalsScreen() {
|
||||
)}
|
||||
|
||||
{/* Analytics Section */}
|
||||
{statistics && (
|
||||
<View style={styles.section}>
|
||||
<MinimalCard variant="default">
|
||||
{statistics &&
|
||||
(statistics.weeklyTrend.length > 0 ||
|
||||
statistics.goals.goalsByType.length > 0) && (
|
||||
<View style={styles.section}>
|
||||
<TouchableOpacity
|
||||
style={styles.analyticsHeader}
|
||||
onPress={() => setShowAnalytics(!showAnalytics)}
|
||||
activeOpacity={0.7}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
<View style={styles.analyticsHeaderLeft}>
|
||||
<Ionicons
|
||||
name="bar-chart-outline"
|
||||
size={20}
|
||||
color={colors.primary}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
typography.h3,
|
||||
{ color: colors.textPrimary, marginLeft: 8 },
|
||||
]}
|
||||
>
|
||||
📈 Progress Analytics
|
||||
</Text>
|
||||
</View>
|
||||
<Ionicons
|
||||
name={showAnalytics ? "chevron-up" : "chevron-down"}
|
||||
size={20}
|
||||
color={colors.textTertiary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<MinimalCard variant="elevated" style={styles.analyticsCard}>
|
||||
<View style={styles.analyticsHeader}>
|
||||
<View style={styles.analyticsHeaderLeft}>
|
||||
<View
|
||||
style={[
|
||||
styles.analyticsIcon,
|
||||
{ backgroundColor: `${colors.primary}15` },
|
||||
]}
|
||||
>
|
||||
<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>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
styles.analyticsToggle,
|
||||
{ backgroundColor: colors.surfaceElevated },
|
||||
]}
|
||||
>
|
||||
<Ionicons
|
||||
name={showAnalytics ? "chevron-up" : "chevron-down"}
|
||||
size={20}
|
||||
color={colors.textSecondary}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{showAnalytics && (
|
||||
<View
|
||||
style={[
|
||||
styles.analyticsContent,
|
||||
{ borderTopColor: colors.border },
|
||||
]}
|
||||
>
|
||||
{statistics.weeklyTrend.length > 0 && (
|
||||
<WeeklyProgressChart
|
||||
weeklyData={statistics.weeklyTrend}
|
||||
title="8-Week Trend"
|
||||
/>
|
||||
{showAnalytics && (
|
||||
<View style={styles.analyticsContent}>
|
||||
{statistics.weeklyTrend.length > 0 && (
|
||||
<View style={styles.chartSection}>
|
||||
<Text
|
||||
style={[
|
||||
typography.h4,
|
||||
{ color: colors.textPrimary, marginBottom: 16 },
|
||||
]}
|
||||
>
|
||||
Weekly Trend
|
||||
</Text>
|
||||
<WeeklyProgressChart
|
||||
weeklyData={statistics.weeklyTrend}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
{statistics.goals.goalsByType.length > 0 && (
|
||||
<View style={styles.chartSection}>
|
||||
<Text
|
||||
style={[
|
||||
typography.h4,
|
||||
{ color: colors.textPrimary, marginBottom: 16 },
|
||||
]}
|
||||
>
|
||||
Goals by Type
|
||||
</Text>
|
||||
<GoalTypeBreakdownChart
|
||||
data={statistics.goals.goalsByType}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
{statistics.goals.goalsByType.length > 0 && (
|
||||
<GoalTypeBreakdownChart
|
||||
data={statistics.goals.goalsByType}
|
||||
title="Goals by Type"
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</MinimalCard>
|
||||
</View>
|
||||
)}
|
||||
</MinimalCard>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Active Goals */}
|
||||
<View style={styles.section}>
|
||||
@ -439,6 +476,9 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 20,
|
||||
},
|
||||
analyticsCard: {
|
||||
padding: 20,
|
||||
},
|
||||
analyticsHeader: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
@ -448,13 +488,32 @@ const styles = StyleSheet.create({
|
||||
flexDirection: "row",
|
||||
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: {
|
||||
paddingTop: 16,
|
||||
marginTop: 16,
|
||||
paddingTop: 24,
|
||||
marginTop: 20,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: "rgba(0,0,0,0.05)",
|
||||
},
|
||||
chartSection: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
goalsList: {
|
||||
gap: 12,
|
||||
gap: 16,
|
||||
},
|
||||
emptyState: {
|
||||
alignItems: "center",
|
||||
|
||||
@ -19,6 +19,7 @@ import { MinimalButton } from "../../components/MinimalButton";
|
||||
import { Badge } from "../../components/Badge";
|
||||
import { IconContainer } from "../../components/IconContainer";
|
||||
import { API_BASE_URL, API_ENDPOINTS } from "../../config/api";
|
||||
import { fitnessProfileApi, FitnessProfile } from "../../api/fitnessProfile";
|
||||
import log from "../../utils/logger";
|
||||
|
||||
export default function ProfileScreen() {
|
||||
@ -35,6 +36,11 @@ export default function ProfileScreen() {
|
||||
const [selectedGymId, setSelectedGymId] = useState<string | null>(null);
|
||||
const [currentGymId, setCurrentGymId] = 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(() => {
|
||||
const gid =
|
||||
@ -49,8 +55,23 @@ export default function ProfileScreen() {
|
||||
|
||||
useEffect(() => {
|
||||
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 () => {
|
||||
try {
|
||||
setGymsLoading(true);
|
||||
@ -115,8 +136,12 @@ export default function ProfileScreen() {
|
||||
const handleApplyGym = async () => {
|
||||
try {
|
||||
const token = await getToken();
|
||||
const url = `${API_BASE_URL}${API_ENDPOINTS.USERS}/gym`;
|
||||
log.debug("Updating gym selection", { url, gymId: selectedGymId });
|
||||
const url = `${API_BASE_URL}${API_ENDPOINTS.USERS.GYM}`;
|
||||
log.debug("Updating gym selection", {
|
||||
url,
|
||||
gymId: selectedGymId,
|
||||
token: token ? "present" : "missing",
|
||||
});
|
||||
const res = await fetch(url, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
@ -331,29 +356,145 @@ export default function ProfileScreen() {
|
||||
onPress={() => router.push("/personal-details")}
|
||||
/>
|
||||
<View style={[styles.divider, { backgroundColor: colors.border }]} />
|
||||
<ListItem
|
||||
title="Fitness Profile"
|
||||
leftIcon={
|
||||
<IconContainer
|
||||
variant="colored"
|
||||
backgroundColor={`${colors.success}20`}
|
||||
>
|
||||
<Ionicons
|
||||
name="fitness-outline"
|
||||
size={20}
|
||||
color={colors.success}
|
||||
/>
|
||||
</IconContainer>
|
||||
}
|
||||
rightElement={
|
||||
<Ionicons
|
||||
name="chevron-forward"
|
||||
size={20}
|
||||
color={colors.textTertiary}
|
||||
/>
|
||||
}
|
||||
|
||||
{/* Fitness Profile Card */}
|
||||
<TouchableOpacity
|
||||
onPress={() => router.push("/fitness-profile")}
|
||||
/>
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
<MinimalCard variant="elevated" style={styles.fitnessProfileCard}>
|
||||
<View style={styles.fitnessProfileHeader}>
|
||||
<View
|
||||
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 },
|
||||
]}
|
||||
/>
|
||||
<View style={styles.profileStat}>
|
||||
<Text
|
||||
style={[
|
||||
typography.statLarge,
|
||||
{ color: colors.success, fontSize: 28 },
|
||||
]}
|
||||
>
|
||||
{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")}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</MinimalCard>
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={[styles.divider, { backgroundColor: colors.border }]} />
|
||||
<ListItem
|
||||
title="Notifications"
|
||||
@ -382,65 +523,71 @@ export default function ProfileScreen() {
|
||||
|
||||
{/* Gym Selection */}
|
||||
<View style={styles.section}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<Text style={[typography.h3, { color: colors.textPrimary }]}>
|
||||
Gym Selection
|
||||
</Text>
|
||||
<TouchableOpacity onPress={loadGyms}>
|
||||
<Text
|
||||
style={[
|
||||
typography.body,
|
||||
{ color: colors.primary, fontWeight: "600" },
|
||||
]}
|
||||
>
|
||||
Refresh
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<MinimalCard variant="default">
|
||||
{currentGymName && (
|
||||
<View style={styles.currentGym}>
|
||||
<Text
|
||||
style={[typography.caption, { color: colors.textTertiary }]}
|
||||
>
|
||||
Current Gym
|
||||
</Text>
|
||||
<Text
|
||||
<Text
|
||||
style={[
|
||||
typography.h3,
|
||||
{ color: colors.textPrimary, marginBottom: 12 },
|
||||
]}
|
||||
>
|
||||
Gym Selection
|
||||
</Text>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => setShowGymDropdown(!showGymDropdown)}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
<MinimalCard variant="bordered" style={styles.dropdownCard}>
|
||||
<View style={styles.dropdownHeader}>
|
||||
<View>
|
||||
<Text
|
||||
style={[typography.caption, { color: colors.textTertiary }]}
|
||||
>
|
||||
Current Gym
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
typography.bodyEmphasis,
|
||||
{ color: colors.textPrimary, marginTop: 2 },
|
||||
]}
|
||||
>
|
||||
{currentGymName || "No gym selected"}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
typography.h3,
|
||||
{ color: colors.textPrimary, marginTop: 4 },
|
||||
styles.dropdownIcon,
|
||||
{ backgroundColor: colors.surfaceElevated },
|
||||
]}
|
||||
>
|
||||
{currentGymName}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name={showGymDropdown ? "chevron-up" : "chevron-down"}
|
||||
size={20}
|
||||
color={colors.textSecondary}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</MinimalCard>
|
||||
</TouchableOpacity>
|
||||
|
||||
{gymsLoading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator color={colors.primary} />
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
style={styles.gymScroll}
|
||||
contentContainerStyle={styles.gymScrollContent}
|
||||
>
|
||||
{showGymDropdown && (
|
||||
<MinimalCard variant="elevated" style={styles.dropdownOptions}>
|
||||
{gymsLoading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator color={colors.primary} />
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.gymChip,
|
||||
{
|
||||
backgroundColor:
|
||||
selectedGymId === null
|
||||
? `${colors.primary}20`
|
||||
: colors.surfaceElevated,
|
||||
borderColor:
|
||||
selectedGymId === null ? colors.primary : colors.border,
|
||||
styles.dropdownOption,
|
||||
selectedGymId === null && {
|
||||
backgroundColor: `${colors.primary}15`,
|
||||
},
|
||||
]}
|
||||
onPress={() => setSelectedGymId(null)}
|
||||
onPress={() => {
|
||||
setSelectedGymId(null);
|
||||
setShowGymDropdown(false);
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
@ -456,51 +603,78 @@ export default function ProfileScreen() {
|
||||
>
|
||||
No Gym
|
||||
</Text>
|
||||
{selectedGymId === null && (
|
||||
<Ionicons
|
||||
name="checkmark"
|
||||
size={20}
|
||||
color={colors.primary}
|
||||
/>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
{gyms.map((gym) => (
|
||||
<TouchableOpacity
|
||||
key={gym.id}
|
||||
style={[
|
||||
styles.gymChip,
|
||||
{
|
||||
backgroundColor:
|
||||
selectedGymId === gym.id
|
||||
? `${colors.primary}20`
|
||||
: colors.surfaceElevated,
|
||||
borderColor:
|
||||
selectedGymId === gym.id
|
||||
? colors.primary
|
||||
: colors.border,
|
||||
styles.dropdownOption,
|
||||
selectedGymId === gym.id && {
|
||||
backgroundColor: `${colors.primary}15`,
|
||||
},
|
||||
]}
|
||||
onPress={() => setSelectedGymId(gym.id)}
|
||||
onPress={() => {
|
||||
setSelectedGymId(gym.id);
|
||||
setShowGymDropdown(false);
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
typography.body,
|
||||
{
|
||||
color:
|
||||
selectedGymId === gym.id
|
||||
? colors.primary
|
||||
: colors.textSecondary,
|
||||
fontWeight: selectedGymId === gym.id ? "600" : "400",
|
||||
},
|
||||
]}
|
||||
>
|
||||
{gym.name}
|
||||
</Text>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text
|
||||
style={[
|
||||
typography.body,
|
||||
{
|
||||
color:
|
||||
selectedGymId === gym.id
|
||||
? colors.primary
|
||||
: colors.textSecondary,
|
||||
fontWeight:
|
||||
selectedGymId === gym.id ? "600" : "400",
|
||||
},
|
||||
]}
|
||||
>
|
||||
{gym.name}
|
||||
</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>
|
||||
))}
|
||||
</ScrollView>
|
||||
<MinimalButton
|
||||
title="Apply Selection"
|
||||
onPress={handleApplyGym}
|
||||
variant="primary"
|
||||
style={{ marginTop: 16 }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</MinimalCard>
|
||||
{selectedGymId !== currentGymId && (
|
||||
<MinimalButton
|
||||
title="Apply Selection"
|
||||
onPress={handleApplyGym}
|
||||
variant="primary"
|
||||
size="lg"
|
||||
fullWidth
|
||||
style={{ marginTop: 12 }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</MinimalCard>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Support */}
|
||||
@ -651,4 +825,78 @@ const styles = StyleSheet.create({
|
||||
borderWidth: 1.5,
|
||||
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",
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { View, Text, StyleSheet, Dimensions } from "react-native";
|
||||
import { PieChart } from "react-native-chart-kit";
|
||||
import { theme } from "../styles/theme";
|
||||
import { useTheme } from "../contexts/ThemeContext";
|
||||
|
||||
interface GoalTypeData {
|
||||
goalType: string;
|
||||
@ -17,37 +17,40 @@ export function GoalTypeBreakdownChart({
|
||||
data,
|
||||
title = "Goals by Type",
|
||||
}: GoalTypeBreakdownChartProps) {
|
||||
const { colors, typography } = useTheme();
|
||||
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
|
||||
const chartColors = [
|
||||
colors.primary,
|
||||
colors.success,
|
||||
colors.warning,
|
||||
colors.accent,
|
||||
colors.secondary,
|
||||
colors.info,
|
||||
];
|
||||
|
||||
// Prepare chart data
|
||||
const chartData = data.map((item, index) => ({
|
||||
name: item.goalType,
|
||||
name: item.goalType
|
||||
.replace(/_/g, " ")
|
||||
.replace(/\b\w/g, (l) => l.toUpperCase()),
|
||||
count: item.count,
|
||||
color: colors[index % colors.length],
|
||||
legendFontColor: theme.colors.gray600,
|
||||
color: chartColors[index % chartColors.length],
|
||||
legendFontColor: colors.textSecondary,
|
||||
legendFontSize: 12,
|
||||
}));
|
||||
|
||||
const chartConfig = {
|
||||
color: (opacity = 1) => `rgba(59, 130, 246, ${opacity})`,
|
||||
};
|
||||
const totalGoals = data.reduce((sum, item) => sum + item.count, 0);
|
||||
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>{title}</Text>
|
||||
<Text style={[typography.h4, { color: colors.textPrimary }]}>
|
||||
{title}
|
||||
</Text>
|
||||
<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>
|
||||
);
|
||||
@ -55,46 +58,123 @@ export function GoalTypeBreakdownChart({
|
||||
|
||||
return (
|
||||
<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}>
|
||||
<PieChart
|
||||
data={chartData}
|
||||
width={screenWidth - 60}
|
||||
height={200}
|
||||
chartConfig={chartConfig}
|
||||
width={screenWidth - 80}
|
||||
height={160}
|
||||
chartConfig={{
|
||||
color: (opacity = 1) => `rgba(0, 0, 0, ${opacity})`,
|
||||
}}
|
||||
accessor="count"
|
||||
backgroundColor="transparent"
|
||||
paddingLeft="15"
|
||||
absolute
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
backgroundColor: theme.colors.white,
|
||||
borderRadius: theme.borderRadius.xl,
|
||||
padding: 16,
|
||||
marginBottom: 16,
|
||||
...theme.shadows.medium,
|
||||
borderRadius: 16,
|
||||
padding: 4,
|
||||
},
|
||||
title: {
|
||||
fontSize: theme.typography.fontSize.lg,
|
||||
fontWeight: theme.typography.fontWeight.bold,
|
||||
color: theme.colors.gray700,
|
||||
marginBottom: 12,
|
||||
summaryRow: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-around",
|
||||
marginBottom: 16,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
summaryItem: {
|
||||
alignItems: "center",
|
||||
},
|
||||
chartContainer: {
|
||||
alignItems: "center",
|
||||
marginBottom: 16,
|
||||
},
|
||||
legendGrid: {
|
||||
gap: 10,
|
||||
},
|
||||
legendItem: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
},
|
||||
legendDot: {
|
||||
width: 12,
|
||||
height: 12,
|
||||
borderRadius: 6,
|
||||
},
|
||||
emptyState: {
|
||||
paddingVertical: 40,
|
||||
paddingVertical: 32,
|
||||
alignItems: "center",
|
||||
},
|
||||
emptyText: {
|
||||
fontSize: theme.typography.fontSize.sm,
|
||||
color: theme.colors.gray400,
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
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 { useTheme } from "../contexts/ThemeContext";
|
||||
import type { WeeklyTrendData } from "../api/types";
|
||||
|
||||
interface WeeklyProgressChartProps {
|
||||
@ -13,28 +13,26 @@ export function WeeklyProgressChart({
|
||||
weeklyData,
|
||||
title = "Weekly Progress",
|
||||
}: WeeklyProgressChartProps) {
|
||||
const { colors, typography } = useTheme();
|
||||
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 labels = weeklyData.map((week: WeeklyTrendData) => week.weekLabel);
|
||||
const checkInsData = weeklyData.map((week: WeeklyTrendData) => week.checkIns);
|
||||
const goalsCompletedData = weeklyData.map(
|
||||
(week: WeeklyTrendData) => week.goalsCompleted,
|
||||
);
|
||||
|
||||
const chartConfig = {
|
||||
backgroundColor: theme.colors.white,
|
||||
backgroundGradientFrom: theme.colors.white,
|
||||
backgroundGradientTo: theme.colors.white,
|
||||
backgroundColor: colors.surface,
|
||||
backgroundGradientFrom: colors.surface,
|
||||
backgroundGradientTo: colors.surface,
|
||||
decimalPlaces: 0,
|
||||
color: (opacity = 1) => `rgba(59, 130, 246, ${opacity})`,
|
||||
labelColor: (opacity = 1) => `rgba(107, 114, 128, ${opacity})`,
|
||||
style: {
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
},
|
||||
color: (opacity = 1) => `rgba(0, 102, 255, ${opacity})`,
|
||||
labelColor: (opacity = 1) => colors.textTertiary,
|
||||
propsForDots: {
|
||||
r: "4",
|
||||
r: "5",
|
||||
strokeWidth: "2",
|
||||
stroke: theme.colors.primary,
|
||||
stroke: colors.primary,
|
||||
},
|
||||
};
|
||||
|
||||
@ -43,31 +41,40 @@ export function WeeklyProgressChart({
|
||||
datasets: [
|
||||
{
|
||||
data: checkInsData,
|
||||
color: (opacity = 1) => `rgba(59, 130, 246, ${opacity})`, // Blue for check-ins
|
||||
strokeWidth: 2,
|
||||
color: () => colors.primary,
|
||||
strokeWidth: 3,
|
||||
},
|
||||
{
|
||||
data: goalsCompletedData,
|
||||
color: (opacity = 1) => `rgba(16, 185, 129, ${opacity})`, // Green for goals
|
||||
strokeWidth: 2,
|
||||
color: () => colors.success,
|
||||
strokeWidth: 3,
|
||||
},
|
||||
],
|
||||
legend: ["Check-ins", "Goals Completed"],
|
||||
legend: ["Check-ins", "Goals"],
|
||||
};
|
||||
|
||||
return (
|
||||
<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}>
|
||||
<LineChart
|
||||
data={data}
|
||||
width={screenWidth - 60}
|
||||
height={220}
|
||||
width={screenWidth - 80}
|
||||
height={180}
|
||||
chartConfig={chartConfig}
|
||||
bezier
|
||||
style={styles.chart}
|
||||
withInnerLines={true}
|
||||
withOuterLines={true}
|
||||
withOuterLines={false}
|
||||
withVerticalLabels={true}
|
||||
withHorizontalLabels={true}
|
||||
fromZero={true}
|
||||
@ -75,12 +82,20 @@ export function WeeklyProgressChart({
|
||||
</View>
|
||||
<View style={styles.legend}>
|
||||
<View style={styles.legendItem}>
|
||||
<View style={[styles.legendDot, { backgroundColor: "#3b82f6" }]} />
|
||||
<Text style={styles.legendText}>Check-ins</Text>
|
||||
<View
|
||||
style={[styles.legendDot, { backgroundColor: colors.primary }]}
|
||||
/>
|
||||
<Text style={[typography.caption, { color: colors.textSecondary }]}>
|
||||
Check-ins
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.legendItem}>
|
||||
<View style={[styles.legendDot, { backgroundColor: "#10b981" }]} />
|
||||
<Text style={styles.legendText}>Goals Completed</Text>
|
||||
<View
|
||||
style={[styles.legendDot, { backgroundColor: colors.success }]}
|
||||
/>
|
||||
<Text style={[typography.caption, { color: colors.textSecondary }]}>
|
||||
Goals
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@ -89,17 +104,8 @@ export function WeeklyProgressChart({
|
||||
|
||||
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,
|
||||
borderRadius: 16,
|
||||
padding: 4,
|
||||
},
|
||||
chartContainer: {
|
||||
alignItems: "center",
|
||||
@ -107,26 +113,22 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
chart: {
|
||||
marginVertical: 8,
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
borderRadius: 12,
|
||||
},
|
||||
legend: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
gap: 20,
|
||||
gap: 24,
|
||||
paddingTop: 8,
|
||||
},
|
||||
legendItem: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
gap: 8,
|
||||
},
|
||||
legendDot: {
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: 5,
|
||||
},
|
||||
legendText: {
|
||||
fontSize: theme.typography.fontSize.sm,
|
||||
color: theme.colors.gray500,
|
||||
},
|
||||
});
|
||||
|
||||
@ -35,6 +35,7 @@ export const API_ENDPOINTS = {
|
||||
USERS: {
|
||||
LIST: "/api/users",
|
||||
STATISTICS: "/api/users/statistics",
|
||||
GYM: "/api/users/gym",
|
||||
},
|
||||
GYMS: "/api/gyms",
|
||||
ATTENDANCE: {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user