fitaiProto/apps/mobile/src/app/(tabs)/profile.tsx

831 lines
24 KiB
TypeScript

import {
View,
Text,
StyleSheet,
TouchableOpacity,
Image,
Alert,
ScrollView,
ActivityIndicator,
} from "react-native";
import { useUser, useClerk, useAuth } from "@clerk/clerk-expo";
import { useRouter } from "expo-router";
import { Ionicons } from "@expo/vector-icons";
import { useState, useEffect } from "react";
import { useTheme } from "../../contexts/ThemeContext";
import { MinimalCard } from "../../components/MinimalCard";
import { ListItem } from "../../components/ListItem";
import { MinimalButton } from "../../components/MinimalButton";
import { Badge } from "../../components/Badge";
import { IconContainer } from "../../components/IconContainer";
import { fitnessProfileApi, FitnessProfile } from "../../api/fitnessProfile";
import { gymsApi, type Gym } from "../../api/gyms";
import { useMembership } from "../../hooks/useMembership";
import log from "../../utils/logger";
export default function ProfileScreen() {
const { user } = useUser();
const { signOut } = useClerk();
const router = useRouter();
const { colors, typography, theme: activeTheme, setTheme } = useTheme();
const { getToken } = useAuth();
const { membershipType } = useMembership();
const [gyms, setGyms] = useState<Gym[]>([]);
const [gymsLoading, setGymsLoading] = useState(false);
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 =
((user?.publicMetadata as any)?.gymId as string | undefined) ?? null;
setCurrentGymId(gid ?? null);
if (gid && gyms.length > 0) {
const g = gyms.find((x) => x.id === gid);
setCurrentGymName(g?.name ?? null);
if (selectedGymId === null) setSelectedGymId(gid);
}
}, [user?.publicMetadata, gyms]);
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);
const token = await getToken();
const list = await gymsApi.getGyms(token);
setGyms(list);
const gid =
currentGymId ??
((user?.publicMetadata as any)?.gymId as string | undefined) ??
null;
if (gid) {
const g = list.find((x) => x.id === gid);
setCurrentGymId(gid);
setCurrentGymName(g?.name ?? null);
if (selectedGymId === null) setSelectedGymId(gid);
}
} catch (err) {
log.error("Failed to fetch gyms", err);
setGyms([]);
} finally {
setGymsLoading(false);
}
};
const handleApplyGym = async () => {
try {
const token = await getToken();
await gymsApi.updateUserGym(selectedGymId, token);
setCurrentGymId(selectedGymId);
setCurrentGymName(
selectedGymId
? (gyms.find((g) => g.id === selectedGymId)?.name ?? null)
: null,
);
try {
await (user as any)?.reload?.();
} catch (e) {
log.debug("Failed to reload user after gym update", { error: e });
}
Alert.alert(
"Success",
selectedGymId ? "Gym selected successfully" : "Proceeding without gym",
);
} catch (err) {
log.error("Failed to update gym selection", err);
Alert.alert("Error", "Failed to update gym selection");
}
};
const handleSignOut = async () => {
try {
await signOut();
} catch (err) {
log.error("Failed to sign out", err);
}
};
const confirmSignOut = () => {
Alert.alert("Sign Out", "Are you sure you want to sign out?", [
{ text: "Cancel", style: "cancel" },
{ text: "Sign Out", style: "destructive", onPress: handleSignOut },
]);
};
const handleThemeChange = () => {
Alert.alert("Choose Theme", "Select your preferred theme", [
{
text: "Light",
onPress: () => setTheme("light"),
},
{
text: "Dark",
onPress: () => setTheme("dark"),
},
{
text: "System",
onPress: () => setTheme("system"),
},
{ text: "Cancel", style: "cancel" },
]);
};
const getThemeLabel = () => {
if (activeTheme === "light") return "Light";
if (activeTheme === "dark") return "Dark";
return "System";
};
return (
<ScrollView
style={[styles.container, { backgroundColor: colors.background }]}
contentContainerStyle={styles.content}
>
{/* Header Card */}
<MinimalCard
variant="elevated"
style={[styles.profileCard, { backgroundColor: colors.primary }]}
>
<View style={styles.avatarContainer}>
{user?.imageUrl ? (
<Image source={{ uri: user.imageUrl }} style={styles.avatar} />
) : (
<View
style={[
styles.placeholderAvatar,
{
backgroundColor: colors.white,
borderWidth: 3,
borderColor: colors.white,
},
]}
>
<Ionicons name="person" size={40} color={colors.primary} />
</View>
)}
</View>
<Text
style={[
typography.h1,
{ color: colors.white, marginTop: 16, fontSize: 28 },
]}
>
{user?.fullName || "User"}
</Text>
<Text
style={[
typography.body,
{ color: "rgba(255,255,255,0.8)", marginTop: 4 },
]}
>
{user?.primaryEmailAddress?.emailAddress}
</Text>
<Badge
label={`${membershipType.toUpperCase()} Member`}
variant={membershipType === "basic" ? "neutral" : "success"}
style={{ marginTop: 12 }}
/>
</MinimalCard>
{/* Theme Settings */}
<View style={styles.section}>
<Text
style={[
typography.h3,
{ color: colors.textPrimary, marginBottom: 12 },
]}
>
Appearance
</Text>
<MinimalCard variant="default">
<ListItem
title="Theme"
subtitle={`Current: ${getThemeLabel()}`}
leftIcon={
<IconContainer
variant="colored"
backgroundColor={`${colors.accent}20`}
>
<Ionicons
name="color-palette"
size={20}
color={colors.accent}
/>
</IconContainer>
}
rightElement={
<Ionicons
name="chevron-forward"
size={20}
color={colors.textTertiary}
/>
}
onPress={handleThemeChange}
/>
</MinimalCard>
</View>
{/* Account Settings */}
<View style={styles.section}>
<Text
style={[
typography.h3,
{ color: colors.textPrimary, marginBottom: 12 },
]}
>
Account
</Text>
<MinimalCard variant="default">
<ListItem
title="Personal Details"
leftIcon={
<IconContainer
variant="colored"
backgroundColor={`${colors.primary}20`}
>
<Ionicons
name="person-outline"
size={20}
color={colors.primary}
/>
</IconContainer>
}
rightElement={
<Ionicons
name="chevron-forward"
size={20}
color={colors.textTertiary}
/>
}
onPress={() => router.push("/personal-details")}
/>
<View style={[styles.divider, { backgroundColor: colors.border }]} />
{/* 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"
leftIcon={
<IconContainer
variant="colored"
backgroundColor={`${colors.warning}20`}
>
<Ionicons
name="notifications-outline"
size={20}
color={colors.warning}
/>
</IconContainer>
}
rightElement={
<Ionicons
name="chevron-forward"
size={20}
color={colors.textTertiary}
/>
}
/>
</MinimalCard>
</View>
{/* Gym Selection */}
<View style={styles.section}>
<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={[
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 ? (
<View style={styles.loadingContainer}>
<ActivityIndicator color={colors.primary} />
</View>
) : (
<>
<TouchableOpacity
style={[
styles.dropdownOption,
selectedGymId === null && {
backgroundColor: `${colors.primary}15`,
},
]}
onPress={() => {
setSelectedGymId(null);
setShowGymDropdown(false);
}}
>
<Text
style={[
typography.body,
{
color:
selectedGymId === null
? colors.primary
: colors.textSecondary,
fontWeight: selectedGymId === null ? "600" : "400",
},
]}
>
No Gym
</Text>
{selectedGymId === null && (
<Ionicons
name="checkmark"
size={20}
color={colors.primary}
/>
)}
</TouchableOpacity>
{gyms.map((gym) => (
<TouchableOpacity
key={gym.id}
style={[
styles.dropdownOption,
selectedGymId === gym.id && {
backgroundColor: `${colors.primary}15`,
},
]}
onPress={() => {
setSelectedGymId(gym.id);
setShowGymDropdown(false);
}}
>
<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>
))}
{selectedGymId !== currentGymId && (
<MinimalButton
title="Apply Selection"
onPress={handleApplyGym}
variant="primary"
size="lg"
fullWidth
style={{ marginTop: 12 }}
/>
)}
</>
)}
</MinimalCard>
)}
</View>
{/* Support */}
<View style={styles.section}>
<Text
style={[
typography.h3,
{ color: colors.textPrimary, marginBottom: 12 },
]}
>
Support
</Text>
<MinimalCard variant="default">
<ListItem
title="Help Center"
leftIcon={
<IconContainer
variant="colored"
backgroundColor={`${colors.info}20`}
>
<Ionicons
name="help-circle-outline"
size={20}
color={colors.info}
/>
</IconContainer>
}
rightElement={
<Ionicons
name="chevron-forward"
size={20}
color={colors.textTertiary}
/>
}
/>
<View style={[styles.divider, { backgroundColor: colors.border }]} />
<ListItem
title="Privacy & Security"
leftIcon={
<IconContainer variant="subtle">
<Ionicons
name="shield-checkmark-outline"
size={20}
color={colors.textSecondary}
/>
</IconContainer>
}
rightElement={
<Ionicons
name="chevron-forward"
size={20}
color={colors.textTertiary}
/>
}
/>
</MinimalCard>
</View>
{/* Sign Out */}
<View style={styles.section}>
<MinimalButton
title="Sign Out"
onPress={confirmSignOut}
variant="danger"
size="lg"
/>
</View>
<Text
style={[
typography.caption,
{
color: colors.textTertiary,
textAlign: "center",
marginBottom: 100,
},
]}
>
Version 1.0.0
</Text>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
content: {
padding: 20,
paddingTop: 60,
paddingBottom: 100,
},
profileCard: {
alignItems: "center",
paddingVertical: 32,
marginBottom: 24,
},
avatarContainer: {
position: "relative",
},
avatar: {
width: 100,
height: 100,
borderRadius: 50,
},
placeholderAvatar: {
width: 100,
height: 100,
borderRadius: 50,
justifyContent: "center",
alignItems: "center",
},
section: {
marginBottom: 24,
},
sectionHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 12,
},
divider: {
height: 1,
marginLeft: 52,
},
currentGym: {
marginBottom: 16,
paddingBottom: 16,
borderBottomWidth: 1,
borderBottomColor: "rgba(0, 0, 0, 0.05)",
},
loadingContainer: {
paddingVertical: 20,
alignItems: "center",
},
gymScroll: {
marginHorizontal: -16,
},
gymScrollContent: {
paddingHorizontal: 16,
gap: 8,
},
gymChip: {
paddingHorizontal: 16,
paddingVertical: 10,
borderRadius: 20,
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",
},
});