refinenments

This commit is contained in:
dimitar committed 2026-03-12 20:19:48 +01:00
1 parent 5d6166df1b
commit 064dafad57
6 files changed
+635 -245

No files matched your search

+111 -52
View File
@@ -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",
+357 -109
View File
@@ -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",
},
});