redesign take 1

This commit is contained in:
dimitar committed 2026-03-11 08:22:48 +01:00
1 parent 1143f8ca02
commit e3a3c3fccf
19 files changed
+2783 -1493

No files matched your search

+371 -289
View File
@@ -11,11 +11,13 @@ import {
import { useUser, useClerk, useAuth } from "@clerk/clerk-expo";
import { useRouter } from "expo-router";
import { Ionicons } from "@expo/vector-icons";
import { LinearGradient } from "expo-linear-gradient";
import { theme } from "../../styles/theme";
import { AnimatedButton } from "../../components/AnimatedButton";
import { GradientBackground } from "../../components/GradientBackground";
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 { API_BASE_URL, API_ENDPOINTS } from "../../config/api";
import log from "../../utils/logger";
@@ -23,24 +25,9 @@ export default function ProfileScreen() {
const { user } = useUser();
const { signOut } = useClerk();
const router = useRouter();
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 },
]);
};
// Gym selection state and handlers
const { colors, typography, theme: activeTheme, setTheme } = useTheme();
const { getToken } = useAuth();
const [gyms, setGyms] = useState<
Array<{ id: string; name: string; location?: string }>
>([]);
@@ -60,7 +47,6 @@ export default function ProfileScreen() {
}
}, [user?.publicMetadata, gyms]);
// Auto-load gyms on mount
useEffect(() => {
loadGyms();
}, []);
@@ -80,9 +66,7 @@ export default function ProfileScreen() {
log.error(
"Failed to fetch gyms - non-OK response",
new Error(text.slice(0, 200)),
{
status: res.status,
},
{ status: res.status },
);
setGyms([]);
return;
@@ -92,9 +76,7 @@ export default function ProfileScreen() {
log.error(
"Failed to fetch gyms - expected JSON",
new Error(text.slice(0, 200)),
{
contentType,
},
{ contentType },
);
setGyms([]);
return;
@@ -149,9 +131,7 @@ export default function ProfileScreen() {
log.error(
"Failed to update gym selection - non-OK response",
new Error(text.slice(0, 200)),
{
status: res.status,
},
{ status: res.status },
);
Alert.alert("Error", "Failed to update gym selection");
return;
@@ -167,14 +147,12 @@ export default function ProfileScreen() {
});
}
}
// Update current gym state for immediate UI reflection
setCurrentGymId(selectedGymId);
setCurrentGymName(
selectedGymId
? (gyms.find((g) => g.id === selectedGymId)?.name ?? null)
: null,
);
// Attempt to reload Clerk user metadata so current gym reflects server state
try {
await (user as any)?.reload?.();
} catch (e) {
@@ -190,372 +168,476 @@ export default function ProfileScreen() {
}
};
return (
<View style={styles.container}>
<GradientBackground variant="primary" style={styles.header}>
<View style={styles.profileCard}>
<View style={styles.avatarContainer}>
{user?.imageUrl ? (
<Image source={{ uri: user.imageUrl }} style={styles.avatar} />
) : (
<View style={styles.placeholderAvatar}>
<Ionicons name="person" size={40} color="#fff" />
</View>
)}
<View style={styles.editBadge}>
<Ionicons name="pencil" size={12} color={theme.colors.primary} />
</View>
</View>
<Text style={styles.name}>{user?.fullName || "User"}</Text>
<Text style={styles.email}>
{user?.primaryEmailAddress?.emailAddress}
</Text>
<View style={styles.memberBadge}>
<Text style={styles.memberText}>Premium Member</Text>
</View>
</View>
</GradientBackground>
const handleSignOut = async () => {
try {
await signOut();
} catch (err) {
log.error("Failed to sign out", err);
}
};
<View style={styles.content}>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Account</Text>
<View style={[styles.infoCard, theme.shadows.subtle]}>
<TouchableOpacity
style={styles.infoRow}
onPress={() => router.push("/personal-details")}
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}>
<View style={styles.avatarContainer}>
{user?.imageUrl ? (
<Image source={{ uri: user.imageUrl }} style={styles.avatar} />
) : (
<View
style={[
styles.placeholderAvatar,
{ backgroundColor: colors.primary },
]}
>
<LinearGradient
colors={["rgba(59, 130, 246, 0.1)", "rgba(59, 130, 246, 0.05)"]}
style={styles.iconContainer}
<Ionicons name="person" size={40} color={colors.white} />
</View>
)}
</View>
<Text
style={[typography.h2, { color: colors.textPrimary, marginTop: 16 }]}
>
{user?.fullName || "User"}
</Text>
<Text
style={[
typography.body,
{ color: colors.textSecondary, marginTop: 4 },
]}
>
{user?.primaryEmailAddress?.emailAddress}
</Text>
<Badge
label="Premium Member"
variant="primary"
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={theme.colors.primary}
color={colors.primary}
/>
</LinearGradient>
<Text style={styles.infoLabel}>Personal Details</Text>
</IconContainer>
}
rightElement={
<Ionicons
name="chevron-forward"
size={20}
color={theme.colors.gray400}
color={colors.textTertiary}
/>
</TouchableOpacity>
<View style={styles.divider} />
<TouchableOpacity
style={styles.infoRow}
onPress={() => router.push("/fitness-profile")}
>
<LinearGradient
colors={["rgba(16, 185, 129, 0.1)", "rgba(16, 185, 129, 0.05)"]}
style={styles.iconContainer}
}
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={theme.colors.success}
color={colors.success}
/>
</LinearGradient>
<Text style={styles.infoLabel}>Fitness Profile</Text>
</IconContainer>
}
rightElement={
<Ionicons
name="chevron-forward"
size={20}
color={theme.colors.gray400}
color={colors.textTertiary}
/>
</TouchableOpacity>
<View style={styles.divider} />
<TouchableOpacity style={styles.infoRow}>
<LinearGradient
colors={["rgba(245, 158, 11, 0.1)", "rgba(245, 158, 11, 0.05)"]}
style={styles.iconContainer}
}
onPress={() => router.push("/fitness-profile")}
/>
<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={theme.colors.warning}
color={colors.warning}
/>
</LinearGradient>
<Text style={styles.infoLabel}>Notifications</Text>
</IconContainer>
}
rightElement={
<Ionicons
name="chevron-forward"
size={20}
color={theme.colors.gray400}
color={colors.textTertiary}
/>
</TouchableOpacity>
</View>
}
/>
</MinimalCard>
</View>
{/* Gym Selection */}
<View
style={[styles.infoCard, theme.shadows.subtle, { marginTop: 12 }]}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
padding: 12,
}}
{/* 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" },
]}
>
<Text style={styles.infoLabel}>
Gym{" "}
{currentGymName
? `(Current: ${currentGymName})`
: currentGymId
? `(Current: ${currentGymId})`
: "(Current: None)"}
Refresh
</Text>
</TouchableOpacity>
</View>
<MinimalCard variant="default">
{currentGymName && (
<View style={styles.currentGym}>
<Text
style={[typography.caption, { color: colors.textTertiary }]}
>
Current Gym
</Text>
<Text
style={[
typography.h3,
{ color: colors.textPrimary, marginTop: 4 },
]}
>
{currentGymName}
</Text>
<TouchableOpacity onPress={loadGyms}>
<Text style={{ color: theme.colors.primary }}>
Refresh Gyms
</Text>
</TouchableOpacity>
</View>
)}
{gymsLoading ? (
<ActivityIndicator style={{ padding: 12 }} />
) : (
{gymsLoading ? (
<View style={styles.loadingContainer}>
<ActivityIndicator color={colors.primary} />
</View>
) : (
<>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
style={{ paddingHorizontal: 12, paddingBottom: 12 }}
style={styles.gymScroll}
contentContainerStyle={styles.gymScrollContent}
>
<View style={{ flexDirection: "row" }}>
<TouchableOpacity
<TouchableOpacity
style={[
styles.gymChip,
{
backgroundColor:
selectedGymId === null
? `${colors.primary}20`
: colors.surfaceElevated,
borderColor:
selectedGymId === null ? colors.primary : colors.border,
},
]}
onPress={() => setSelectedGymId(null)}
>
<Text
style={[
styles.infoRow,
selectedGymId === null && {
backgroundColor: "rgba(59, 130, 246, 0.1)",
typography.body,
{
color:
selectedGymId === null
? colors.primary
: colors.textSecondary,
fontWeight: selectedGymId === null ? "600" : "400",
},
]}
onPress={() => setSelectedGymId(null)}
>
<Text style={styles.infoLabel}>Proceed without gym</Text>
</TouchableOpacity>
{gyms.map((gym) => (
<TouchableOpacity
key={gym.id}
No Gym
</Text>
</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,
},
]}
onPress={() => setSelectedGymId(gym.id)}
>
<Text
style={[
styles.infoRow,
selectedGymId === gym.id && {
backgroundColor: "rgba(59, 130, 246, 0.1)",
typography.body,
{
color:
selectedGymId === gym.id
? colors.primary
: colors.textSecondary,
fontWeight: selectedGymId === gym.id ? "600" : "400",
},
]}
onPress={() => setSelectedGymId(gym.id)}
>
<Text style={styles.infoLabel}>{gym.name}</Text>
</TouchableOpacity>
))}
</View>
{gym.name}
</Text>
</TouchableOpacity>
))}
</ScrollView>
)}
<View style={{ padding: 12 }}>
<AnimatedButton
title="Apply Gym Selection"
<MinimalButton
title="Apply Selection"
onPress={handleApplyGym}
variant="primary"
style={{ marginTop: 4 }}
icon={
<Ionicons
name="checkmark-outline"
size={20}
color={theme.colors.white}
/>
}
style={{ marginTop: 16 }}
/>
</View>
</View>
</View>
</>
)}
</MinimalCard>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Support</Text>
<View style={[styles.infoCard, theme.shadows.subtle]}>
<TouchableOpacity style={styles.infoRow}>
<LinearGradient
colors={["rgba(139, 92, 246, 0.1)", "rgba(139, 92, 246, 0.05)"]}
style={styles.iconContainer}
{/* 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={theme.colors.secondary}
color={colors.info}
/>
</LinearGradient>
<Text style={styles.infoLabel}>Help Center</Text>
</IconContainer>
}
rightElement={
<Ionicons
name="chevron-forward"
size={20}
color={theme.colors.gray400}
color={colors.textTertiary}
/>
</TouchableOpacity>
<View style={styles.divider} />
<TouchableOpacity style={styles.infoRow}>
<LinearGradient
colors={[
"rgba(107, 114, 128, 0.1)",
"rgba(107, 114, 128, 0.05)",
]}
style={styles.iconContainer}
>
}
/>
<View style={[styles.divider, { backgroundColor: colors.border }]} />
<ListItem
title="Privacy & Security"
leftIcon={
<IconContainer variant="subtle">
<Ionicons
name="shield-checkmark-outline"
size={20}
color={theme.colors.gray600}
color={colors.textSecondary}
/>
</LinearGradient>
<Text style={styles.infoLabel}>Privacy & Security</Text>
</IconContainer>
}
rightElement={
<Ionicons
name="chevron-forward"
size={20}
color={theme.colors.gray400}
color={colors.textTertiary}
/>
</TouchableOpacity>
</View>
</View>
}
/>
</MinimalCard>
</View>
<AnimatedButton
{/* Sign Out */}
<View style={styles.section}>
<MinimalButton
title="Sign Out"
onPress={confirmSignOut}
variant="danger"
style={styles.signOutButton}
icon={<Ionicons name="log-out-outline" size={20} color="#fff" />}
size="lg"
/>
<Text style={styles.version}>Version 1.0.0</Text>
</View>
</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,
backgroundColor: theme.colors.background,
},
header: {
content: {
padding: 24,
paddingTop: 60,
paddingBottom: 30,
borderBottomLeftRadius: theme.borderRadius.xl,
borderBottomRightRadius: theme.borderRadius.xl,
alignItems: "center",
},
profileCard: {
alignItems: "center",
paddingVertical: 32,
marginBottom: 24,
},
avatarContainer: {
position: "relative",
marginBottom: 16,
},
avatar: {
width: 100,
height: 100,
borderRadius: 50,
borderWidth: 4,
borderColor: "rgba(255, 255, 255, 0.3)",
},
placeholderAvatar: {
width: 100,
height: 100,
borderRadius: 50,
backgroundColor: "rgba(255, 255, 255, 0.2)",
justifyContent: "center",
alignItems: "center",
borderWidth: 4,
borderColor: "rgba(255, 255, 255, 0.3)",
},
editBadge: {
position: "absolute",
bottom: 0,
right: 0,
backgroundColor: theme.colors.white,
width: 32,
height: 32,
borderRadius: 16,
justifyContent: "center",
alignItems: "center",
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
name: {
fontSize: theme.typography.fontSize["2xl"],
fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.white,
marginBottom: 4,
},
email: {
fontSize: theme.typography.fontSize.sm,
color: "rgba(255, 255, 255, 0.8)",
marginBottom: 12,
},
memberBadge: {
backgroundColor: "rgba(255, 255, 255, 0.2)",
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: theme.borderRadius.full,
borderWidth: 1,
borderColor: "rgba(255, 255, 255, 0.3)",
},
memberText: {
color: theme.colors.white,
fontSize: theme.typography.fontSize.xs,
fontWeight: theme.typography.fontWeight.bold,
letterSpacing: 0.5,
},
content: {
padding: 20,
marginTop: -20,
},
section: {
marginBottom: 24,
},
sectionTitle: {
fontSize: theme.typography.fontSize.lg,
fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.gray900,
marginBottom: 12,
marginLeft: 4,
},
infoCard: {
backgroundColor: theme.colors.white,
borderRadius: theme.borderRadius.xl,
padding: 8,
borderWidth: 1,
borderColor: theme.colors.gray100,
},
infoRow: {
sectionHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
padding: 12,
},
iconContainer: {
width: 36,
height: 36,
borderRadius: 10,
justifyContent: "center",
alignItems: "center",
marginRight: 12,
},
infoLabel: {
flex: 1,
fontSize: theme.typography.fontSize.base,
color: theme.colors.gray900,
fontWeight: theme.typography.fontWeight.medium,
marginBottom: 12,
},
divider: {
height: 1,
backgroundColor: theme.colors.gray100,
marginLeft: 60,
marginLeft: 52,
},
signOutButton: {
marginTop: 8,
currentGym: {
marginBottom: 16,
paddingBottom: 16,
borderBottomWidth: 1,
borderBottomColor: "rgba(0, 0, 0, 0.05)",
},
version: {
textAlign: "center",
marginTop: 24,
color: theme.colors.gray400,
fontSize: theme.typography.fontSize.xs,
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,
},
});