Skip to content

Commit 6f9e016

Browse files
committed
feat: Enhance user profile management with image upload and display in account and friends screens
1 parent 1b142c1 commit 6f9e016

File tree

4 files changed

+175
-59
lines changed

4 files changed

+175
-59
lines changed

frontend/api/auth.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export const signup = (name, email, password) => {
88
return apiClient.post("/auth/signup/email", { name, email, password });
99
};
1010

11-
export const updateUser = (userData) => apiClient.patch("/user/", userData);
11+
export const updateUser = (userData) => apiClient.patch("/users/me", userData);
1212

1313
export const refresh = (refresh_token) => {
1414
return apiClient.post("/auth/refresh", { refresh_token });

frontend/screens/AccountScreen.js

Lines changed: 51 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
1-
import React, { useContext } from 'react';
2-
import { View, StyleSheet, Alert } from 'react-native';
3-
import { Text, Appbar, Avatar, List, Divider } from 'react-native-paper';
4-
import { AuthContext } from '../context/AuthContext';
1+
import { useContext } from "react";
2+
import { Alert, StyleSheet, View } from "react-native";
3+
import { Appbar, Avatar, Divider, List, Text } from "react-native-paper";
4+
import { AuthContext } from "../context/AuthContext";
55

66
const AccountScreen = ({ navigation }) => {
7-
const { user, logout } = useContext(AuthContext);
7+
const { user, logout } = useContext(AuthContext);
88

9-
const handleLogout = () => {
10-
logout();
11-
};
9+
const handleLogout = () => {
10+
logout();
11+
};
1212

13-
const handleComingSoon = () => {
14-
Alert.alert('Coming Soon', 'This feature is not yet implemented.');
15-
};
13+
const handleComingSoon = () => {
14+
Alert.alert("Coming Soon", "This feature is not yet implemented.");
15+
};
1616

1717
return (
1818
<View style={styles.container}>
@@ -21,35 +21,43 @@ const AccountScreen = ({ navigation }) => {
2121
</Appbar.Header>
2222
<View style={styles.content}>
2323
<View style={styles.profileSection}>
24-
<Avatar.Text size={80} label={user?.name?.charAt(0) || 'A'} />
25-
<Text variant="headlineSmall" style={styles.name}>{user?.name}</Text>
26-
<Text variant="bodyLarge" style={styles.email}>{user?.email}</Text>
24+
{user?.imageUrl && /^(https?:|data:image)/.test(user.imageUrl) ? (
25+
<Avatar.Image size={80} source={{ uri: user.imageUrl }} />
26+
) : (
27+
<Avatar.Text size={80} label={user?.name?.charAt(0) || "A"} />
28+
)}
29+
<Text variant="headlineSmall" style={styles.name}>
30+
{user?.name}
31+
</Text>
32+
<Text variant="bodyLarge" style={styles.email}>
33+
{user?.email}
34+
</Text>
2735
</View>
2836

2937
<List.Section>
30-
<List.Item
31-
title="Edit Profile"
32-
left={() => <List.Icon icon="account-edit" />}
33-
onPress={() => navigation.navigate('EditProfile')}
34-
/>
35-
<Divider />
36-
<List.Item
37-
title="Email Settings"
38-
left={() => <List.Icon icon="email-settings" />}
39-
onPress={handleComingSoon}
40-
/>
41-
<Divider />
42-
<List.Item
43-
title="Send Feedback"
44-
left={() => <List.Icon icon="feedback" />}
45-
onPress={handleComingSoon}
46-
/>
47-
<Divider />
48-
<List.Item
49-
title="Logout"
50-
left={() => <List.Icon icon="logout" />}
51-
onPress={handleLogout}
52-
/>
38+
<List.Item
39+
title="Edit Profile"
40+
left={() => <List.Icon icon="account-edit" />}
41+
onPress={() => navigation.navigate("EditProfile")}
42+
/>
43+
<Divider />
44+
<List.Item
45+
title="Email Settings"
46+
left={() => <List.Icon icon="email-settings" />}
47+
onPress={handleComingSoon}
48+
/>
49+
<Divider />
50+
<List.Item
51+
title="Send Feedback"
52+
left={() => <List.Icon icon="feedback" />}
53+
onPress={handleComingSoon}
54+
/>
55+
<Divider />
56+
<List.Item
57+
title="Logout"
58+
left={() => <List.Icon icon="logout" />}
59+
onPress={handleLogout}
60+
/>
5361
</List.Section>
5462
</View>
5563
</View>
@@ -64,16 +72,16 @@ const styles = StyleSheet.create({
6472
padding: 16,
6573
},
6674
profileSection: {
67-
alignItems: 'center',
75+
alignItems: "center",
6876
marginBottom: 24,
6977
},
7078
name: {
71-
marginTop: 16,
79+
marginTop: 16,
80+
},
81+
email: {
82+
marginTop: 4,
83+
color: "gray",
7284
},
73-
email: {
74-
marginTop: 4,
75-
color: 'gray',
76-
}
7785
});
7886

7987
export default AccountScreen;

frontend/screens/EditProfileScreen.js

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1+
import * as ImagePicker from "expo-image-picker";
12
import { useContext, useState } from "react";
23
import { Alert, StyleSheet, View } from "react-native";
3-
import { Appbar, Button, TextInput, Title } from "react-native-paper";
4+
import { Appbar, Avatar, Button, TextInput, Title } from "react-native-paper";
45
import { updateUser } from "../api/auth";
56
import { AuthContext } from "../context/AuthContext";
67

78
const EditProfileScreen = ({ navigation }) => {
89
const { user, token, updateUserInContext } = useContext(AuthContext);
910
const [name, setName] = useState(user?.name || "");
11+
const [pickedImage, setPickedImage] = useState(null); // { uri, base64 }
1012
const [isSubmitting, setIsSubmitting] = useState(false);
1113

1214
const handleUpdateProfile = async () => {
@@ -16,7 +18,14 @@ const EditProfileScreen = ({ navigation }) => {
1618
}
1719
setIsSubmitting(true);
1820
try {
19-
const response = await updateUser({ name });
21+
const updates = { name };
22+
23+
// Add image if picked
24+
if (pickedImage?.base64) {
25+
updates.imageUrl = `data:image/jpeg;base64,${pickedImage.base64}`;
26+
}
27+
28+
const response = await updateUser(updates);
2029
updateUserInContext(response.data);
2130
Alert.alert("Success", "Profile updated successfully.");
2231
navigation.goBack();
@@ -28,6 +37,29 @@ const EditProfileScreen = ({ navigation }) => {
2837
}
2938
};
3039

40+
const pickImage = async () => {
41+
// Ask permissions
42+
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
43+
if (status !== "granted") {
44+
Alert.alert(
45+
"Permission required",
46+
"We need media library permission to select an image."
47+
);
48+
return;
49+
}
50+
const result = await ImagePicker.launchImageLibraryAsync({
51+
mediaTypes: ImagePicker.MediaTypeOptions.Images,
52+
base64: true,
53+
allowsEditing: true,
54+
aspect: [1, 1],
55+
quality: 0.8,
56+
});
57+
if (!result.canceled && result.assets && result.assets.length > 0) {
58+
const asset = result.assets[0];
59+
setPickedImage({ uri: asset.uri, base64: asset.base64 });
60+
}
61+
};
62+
3163
return (
3264
<View style={styles.container}>
3365
<Appbar.Header>
@@ -36,6 +68,26 @@ const EditProfileScreen = ({ navigation }) => {
3668
</Appbar.Header>
3769
<View style={styles.content}>
3870
<Title>Edit Your Details</Title>
71+
72+
{/* Profile Picture Section */}
73+
<View style={styles.profilePictureSection}>
74+
{pickedImage?.uri ? (
75+
<Avatar.Image size={100} source={{ uri: pickedImage.uri }} />
76+
) : user?.imageUrl && /^(https?:|data:image)/.test(user.imageUrl) ? (
77+
<Avatar.Image size={100} source={{ uri: user.imageUrl }} />
78+
) : (
79+
<Avatar.Text size={100} label={(user?.name || "?").charAt(0)} />
80+
)}
81+
<Button
82+
mode="outlined"
83+
onPress={pickImage}
84+
icon="camera"
85+
style={styles.imageButton}
86+
>
87+
{pickedImage ? "Change Photo" : "Add Photo"}
88+
</Button>
89+
</View>
90+
3991
<TextInput
4092
label="Name"
4193
value={name}
@@ -63,6 +115,13 @@ const styles = StyleSheet.create({
63115
content: {
64116
padding: 16,
65117
},
118+
profilePictureSection: {
119+
alignItems: "center",
120+
marginBottom: 24,
121+
},
122+
imageButton: {
123+
marginTop: 12,
124+
},
66125
input: {
67126
marginBottom: 16,
68127
},

frontend/screens/FriendsScreen.js

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import { Alert, FlatList, StyleSheet, View } from "react-native";
44
import {
55
ActivityIndicator,
66
Appbar,
7+
Avatar,
78
Divider,
89
IconButton,
910
List,
1011
Text,
1112
} from "react-native-paper";
12-
import { getFriendsBalance } from "../api/groups";
13+
import { getFriendsBalance, getGroupMembers, getGroups } from "../api/groups";
1314
import { AuthContext } from "../context/AuthContext";
1415

1516
const FriendsScreen = () => {
@@ -23,20 +24,51 @@ const FriendsScreen = () => {
2324
const fetchData = async () => {
2425
setIsLoading(true);
2526
try {
27+
// Fetch friends balance data
2628
const friendsResponse = await getFriendsBalance();
2729
const friendsData = friendsResponse.data.friendsBalance || [];
2830

29-
// Transform the backend data to match the expected frontend format
30-
const transformedFriends = friendsData.map((friend) => ({
31-
id: friend.userId,
32-
name: friend.userName,
33-
netBalance: friend.netBalance,
34-
groups: friend.breakdown.map((group) => ({
35-
id: group.groupId,
36-
name: group.groupName,
37-
balance: group.balance,
38-
})),
39-
}));
31+
// Fetch all groups to get member details with user images
32+
const groupsResponse = await getGroups();
33+
const groups = groupsResponse.data.groups || [];
34+
35+
// Create a map of userId to user details by fetching all group members
36+
const userDetailsMap = new Map();
37+
38+
for (const group of groups) {
39+
try {
40+
const membersResponse = await getGroupMembers(group.id);
41+
const members = membersResponse.data || [];
42+
43+
members.forEach((member) => {
44+
if (member.user && member.userId) {
45+
userDetailsMap.set(member.userId, member.user);
46+
}
47+
});
48+
} catch (error) {
49+
console.warn(
50+
`Failed to fetch members for group ${group.id}:`,
51+
error
52+
);
53+
}
54+
}
55+
56+
// Transform the backend data and enrich with user details
57+
const transformedFriends = friendsData.map((friend) => {
58+
const userDetails = userDetailsMap.get(friend.userId);
59+
60+
return {
61+
id: friend.userId,
62+
name: friend.userName,
63+
imageUrl: userDetails?.imageUrl || null,
64+
netBalance: friend.netBalance,
65+
groups: friend.breakdown.map((group) => ({
66+
id: group.groupId,
67+
name: group.groupName,
68+
balance: group.balance,
69+
})),
70+
};
71+
});
4072

4173
setFriends(transformedFriends);
4274
} catch (error) {
@@ -59,14 +91,31 @@ const FriendsScreen = () => {
5991
? `You owe $${Math.abs(item.netBalance).toFixed(2)}`
6092
: `Owes you $${item.netBalance.toFixed(2)}`;
6193

94+
const isImage =
95+
item.imageUrl && /^(https?:|data:image)/.test(item.imageUrl);
96+
6297
return (
6398
<List.Accordion
6499
title={item.name}
65100
description={item.netBalance !== 0 ? balanceText : "Settled up"}
66101
descriptionStyle={{
67102
color: item.netBalance !== 0 ? balanceColor : "gray",
68103
}}
69-
left={(props) => <List.Icon {...props} icon="account" />}
104+
left={(props) =>
105+
isImage ? (
106+
<Avatar.Image
107+
{...props}
108+
size={40}
109+
source={{ uri: item.imageUrl }}
110+
/>
111+
) : (
112+
<Avatar.Text
113+
{...props}
114+
size={40}
115+
label={(item.name || "?").charAt(0)}
116+
/>
117+
)
118+
}
70119
>
71120
{item.groups.map((group) => {
72121
const groupBalanceColor = group.balance < 0 ? "red" : "green";

0 commit comments

Comments
 (0)