From 064dafad57c015cb0d9a2a2456f596394cd0a0bc Mon Sep 17 00:00:00 2001 From: echo Date: Thu, 12 Mar 2026 20:19:48 +0100 Subject: [PATCH] refinenments --- apps/admin/data/fitai.db | Bin 172032 -> 172032 bytes apps/mobile/src/app/(tabs)/goals.tsx | 163 ++++-- apps/mobile/src/app/(tabs)/profile.tsx | 466 ++++++++++++++---- .../src/components/GoalTypeBreakdownChart.tsx | 154 ++++-- .../src/components/WeeklyProgressChart.tsx | 96 ++-- apps/mobile/src/config/api.ts | 1 + 6 files changed, 635 insertions(+), 245 deletions(-) diff --git a/apps/admin/data/fitai.db b/apps/admin/data/fitai.db index 62294b2b057da644ea4c6b091d91bb0ec162c533..17ac7ec5bdc37716ca4895a85abbfcb5d9a70775 100644 GIT binary patch delta 1993 zcma)7U5Fc16rR~6ZL@85wzOK7t?encw2>su-zMFNVBJ~~j4dsTC|h@LX6_`nnVFlt zb7#{PF}t8eOQG5bd9YXnkqSa}LtXHp_Q7`%`cSMC{CNdlx z@0{=Z&Yh(j!%H`Y-yPTI-`hE*&wnxZsiC|5;nA^?jn#wWyAz2$-OqPV8A4ACenw+y zP3wMl8M>eC%5DF-c>Y7TmJf=aJ82eQBYCIh^ncv-_(vlfjLn*H*0^e%GHw~0`=dA3 zM$!i&)4E->%ae!fVtS%9SxXnoxpI2as#)o5!7P~6E)|O;yFmr^wp<$8+nxFL%9d#2 zre@5K{5mq1xY^$W-y}-Q@z}O%RZH*CbbVVis*PS!XZ+&ax_oOteT?t#;o3ntlYo5 zI`gW2MIVkBXBFAEjH}qz86&Cq`#-fJy{lZG#`KFCPPJ-W#Pm0f-fBnR8;gW(E3n%g zJ&7^*G~yrM9u`?Qp>k;)f6p6&+!7^+qh<2z%t85@7iU3--gJ( zXf>{@rdPketUvL`rlHu1ws|eK(%&=sNvwSS$cEAB>h8B@--*3V9z!XWjQ{PBkc8ux zhT>1Y%!B<46kK9aAmB^Ey*hpnA)&SiCU;v7l?=~hn?s!w#|J@8>Pcs@w{&Uf(N44e z)6)C0LF9fPY~o4cOJ;(F@|#35?q%Sm26HGZ@<4zY2!XkVdSd{r4kPQkgDr{;>uzBEFAuLD^wSYMm1TEFX3sf~| zHi+j@2Xlx7m%G?Y-~vK%9M$ugFYzP=K6ZoPq=pbJ!RvzhK5%cnlhE-X0uP91!6`cs z*nxmHW%ULYB_64x`#qUC%FQ55BTyrb+8GPlTr_zgRd;^SYH>v~myLBMDv(RU^PZ$) zfjDp?@G8CEs`_}RCYaxZHgTFEHwcH=4u*L|QpkhLyg*Wfvf(tZBZqas@dyrRQK3Rw z#H4V5Fc%3DP^uKN>2f8TRfz|iZW7uITgVOSs4@erFg)Lx_y<99Nt_fOI}UH-IA{?9 z6Y2E^kV5XHE#CIR{0!uia8jb)b*w(cgc&edf#WC@7cLaQr!wq0mq$yre4xZ^DDWD$ z76)wl^8u0j{1mhTzmX0w=s?q>6mSYu2&EQQG8O93>b0u+*mJ4%SxQ-~j*42!t)a7I zn<8rpG6RJq91brjIDtXRSrs^nUh^9RCbGQ;iuaIhTdAlhMg1LG_d_(3qi0b=!Ys88 zVU6$)v_ICC;^1lN=bv}-DJ~A$$g^rPYy&(dQdS(FblhOirVo;haaFbX(11CLVHvQhr zF?~E1=5+bjJGyIM?R=#G4nxO>hH0ax1z3bRix_07yF&@2yp5n}eZ?B!b4r~KHqOr= YNdkqk@R>A=ue*cK+10fVhBik31~qA--2eap delta 307 zcmYj~ze@sP7{{OI-ZS+s-$#drFw`r93MvBsfWS3F0-@omTbdjy#Q7YewYnQ_NU*3u zX}IFW-qrsg+CoEU3mR+_A)*&r8o!_KuSU;k^o)x%Rxjs@SbcOlgsw1>vpjmRp3Wu` zmj93~W^46hG)sR#kF3^fht59@{!1(8H!~Z0e|M`R-ZKj~JVFJaGYV;sUa26g+&t+^ ztb)OGQ_K7IbK79t0XpFy+)?fh7tZhw4MrZ2q&M - + {statistics && + (statistics.weeklyTrend.length > 0 || + statistics.goals.goalsByType.length > 0) && ( + setShowAnalytics(!showAnalytics)} - activeOpacity={0.7} + activeOpacity={0.85} > - - - - 📈 Progress Analytics - - - - + + + + + + + + + Progress Analytics + + + {showAnalytics ? "Tap to collapse" : "Tap to expand"} + + + + + + + - {showAnalytics && ( - - {statistics.weeklyTrend.length > 0 && ( - + {showAnalytics && ( + + {statistics.weeklyTrend.length > 0 && ( + + + Weekly Trend + + + + )} + {statistics.goals.goalsByType.length > 0 && ( + + + Goals by Type + + + + )} + )} - {statistics.goals.goalsByType.length > 0 && ( - - )} - - )} - - - )} + + + + )} {/* Active Goals */} @@ -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", diff --git a/apps/mobile/src/app/(tabs)/profile.tsx b/apps/mobile/src/app/(tabs)/profile.tsx index 2945f93..d6808a3 100644 --- a/apps/mobile/src/app/(tabs)/profile.tsx +++ b/apps/mobile/src/app/(tabs)/profile.tsx @@ -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(null); const [currentGymId, setCurrentGymId] = useState(null); const [currentGymName, setCurrentGymName] = useState(null); + const [showGymDropdown, setShowGymDropdown] = useState(false); + const [fitnessProfile, setFitnessProfile] = useState( + 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")} /> - - - - } - rightElement={ - - } + + {/* Fitness Profile Card */} + router.push("/fitness-profile")} - /> + activeOpacity={0.85} + > + + + + + + + + Fitness Profile + + + {fitnessProfile ? "Tap to edit" : "Set up your profile"} + + + + + + + + {profileLoading ? ( + + + + ) : fitnessProfile ? ( + + + + {fitnessProfile.height || "-"} + + + Height (cm) + + + + + + {fitnessProfile.weight || "-"} + + + Weight (kg) + + + + + + {fitnessProfile.age || "-"} + + + Age + + + + ) : ( + + + Complete your fitness profile to get personalized + recommendations + + router.push("/fitness-profile")} + /> + + )} + + + - - - Gym Selection - - - - Refresh - - - - - {currentGymName && ( - - - Current Gym - - + Gym Selection + + + setShowGymDropdown(!showGymDropdown)} + activeOpacity={0.85} + > + + + + + Current Gym + + + {currentGymName || "No gym selected"} + + + - {currentGymName} - + + - )} + + - {gymsLoading ? ( - - - - ) : ( - <> - + {showGymDropdown && ( + + {gymsLoading ? ( + + + + ) : ( + <> setSelectedGymId(null)} + onPress={() => { + setSelectedGymId(null); + setShowGymDropdown(false); + }} > No Gym + {selectedGymId === null && ( + + )} {gyms.map((gym) => ( setSelectedGymId(gym.id)} + onPress={() => { + setSelectedGymId(gym.id); + setShowGymDropdown(false); + }} > - - {gym.name} - + + + {gym.name} + + {gym.location && ( + + {gym.location} + + )} + + {selectedGymId === gym.id && ( + + )} ))} - - - - )} - + {selectedGymId !== currentGymId && ( + + )} + + )} + + )} {/* 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", + }, }); diff --git a/apps/mobile/src/components/GoalTypeBreakdownChart.tsx b/apps/mobile/src/components/GoalTypeBreakdownChart.tsx index 22757d6..6d62a19 100644 --- a/apps/mobile/src/components/GoalTypeBreakdownChart.tsx +++ b/apps/mobile/src/components/GoalTypeBreakdownChart.tsx @@ -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 ( - {title} + + {title} + - No goals yet + + No goals yet + ); @@ -55,46 +58,123 @@ export function GoalTypeBreakdownChart({ return ( - {title} + + {title} + + + {/* Summary Stats */} + + + + {totalGoals} + + + Total Goals + + + + + {data.length} + + + Types + + + + `rgba(0, 0, 0, ${opacity})`, + }} accessor="count" backgroundColor="transparent" paddingLeft="15" absolute /> + + {/* Legend */} + + {data.map((item, index) => ( + + + + {item.goalType + .replace(/_/g, " ") + .replace(/\b\w/g, (l) => l.toUpperCase())} + + + {item.count} + + + ))} + ); } 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, - }, }); diff --git a/apps/mobile/src/components/WeeklyProgressChart.tsx b/apps/mobile/src/components/WeeklyProgressChart.tsx index 26e43c5..9d6a1a4 100644 --- a/apps/mobile/src/components/WeeklyProgressChart.tsx +++ b/apps/mobile/src/components/WeeklyProgressChart.tsx @@ -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 ( - {title} + {title && ( + + {title} + + )} - - Check-ins + + + Check-ins + - - Goals Completed + + + Goals + @@ -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, - }, }); diff --git a/apps/mobile/src/config/api.ts b/apps/mobile/src/config/api.ts index 08c3d3a..563dbba 100644 --- a/apps/mobile/src/config/api.ts +++ b/apps/mobile/src/config/api.ts @@ -35,6 +35,7 @@ export const API_ENDPOINTS = { USERS: { LIST: "/api/users", STATISTICS: "/api/users/statistics", + GYM: "/api/users/gym", }, GYMS: "/api/gyms", ATTENDANCE: {