Skip to content

Commit

Permalink
Profile page / Follower + Following (#49)
Browse files Browse the repository at this point in the history
* profile page followers and following

* profile pages complete, still needs updates to work fully with clerk users

* backend changes

* remove console logs

* linting fix

* use clerk first name last name for user profile

* remove activity on follower profiles

* small tweaks

* fix profile image

* onboarding fix

---------

Co-authored-by: Jakob Philippe <[email protected]>
Co-authored-by: Nathan <[email protected]>
Co-authored-by: Ania Misiorek <[email protected]>
  • Loading branch information
4 people authored Apr 18, 2024
1 parent 89377b2 commit 586bd1d
Show file tree
Hide file tree
Showing 17 changed files with 303 additions and 72 deletions.
2 changes: 1 addition & 1 deletion backend/src/db/migrations/1_USER_V1.sql
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ VALUES
('user_2chL8dX6HdbBAuvu3DDM9f9NzKK', 'Ania', 'Misiorek', 'ania', 'https://ca.slack-edge.com/T2CHL6FEG-U05QP4M4M3P-349be7323f07-512'),
('user_2cpFbBLPGkPbszijtQneek7ZJxg', 'Leroy', 'Shaigorodsky', 'leroy', 'https://ca.slack-edge.com/T2CHL6FEG-U040ST08HM1-c3d453828123-512'),
('user_2dv5XFsCMYc4qLcsAnEJ1aUbxnk', 'Cam', 'Plume', 'campd10', 'https://ca.slack-edge.com/T2CHL6FEG-U06DCDZ3FB8-1c488c509f95-512'),
('user_2cwGfu9zcjsbxq5Lp8gy2rkVNlc', 'Nathan', 'Jung', 'nathan', 'https://ca.slack-edge.com/T2CHL6FEG-U05QL55RDBQ-8fd6c3499cac-512'),
('user_2fFVsSf4viW9pjx6Sd5dxEgutJ1', 'Nathan', 'Jung', 'nathan', 'https://ca.slack-edge.com/T2CHL6FEG-U05QL55RDBQ-8fd6c3499cac-512'),
('user_2dvSY6HpxotzWCfch5m4a4OVpAK', 'Aryan', 'Kale', 'aryankale', 'https://cdn-icons-png.freepik.com/256/552/552848.png');

4 changes: 3 additions & 1 deletion backend/src/db/migrations/2_FOLLOWING_V1.sql
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,6 @@ INSERT INTO followings (follower_user_id, following_user_id)
VALUES
('user_2chL8dX6HdbBAuvu3DDM9f9NzKK', 'user_2cpFbBLPGkPbszijtQneek7ZJxg'),
('user_2chL8dX6HdbBAuvu3DDM9f9NzKK', 'user_2dv5XFsCMYc4qLcsAnEJ1aUbxnk'),
('user_2cpFbBLPGkPbszijtQneek7ZJxg', 'user_2chL8dX6HdbBAuvu3DDM9f9NzKK');
('user_2cpFbBLPGkPbszijtQneek7ZJxg', 'user_2chL8dX6HdbBAuvu3DDM9f9NzKK'),
('user_2fFVsSf4viW9pjx6Sd5dxEgutJ1', 'user_2dv5XFsCMYc4qLcsAnEJ1aUbxnk'),
('user_2dv5XFsCMYc4qLcsAnEJ1aUbxnk', 'user_2fFVsSf4viW9pjx6Sd5dxEgutJ1');
2 changes: 1 addition & 1 deletion backend/src/db/migrations/5_USER_PORTFOLIO_V1.sql
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ VALUES
('user_2chL8dX6HdbBAuvu3DDM9f9NzKK', 130, 14, 680, 93),
('user_2cpFbBLPGkPbszijtQneek7ZJxg', -14, -8, 680, 93),
('user_2dv5XFsCMYc4qLcsAnEJ1aUbxnk', 400, 3, 680, 93),
('user_2cwGfu9zcjsbxq5Lp8gy2rkVNlc', 200, 9, 680, 93);
('user_2fFVsSf4viW9pjx6Sd5dxEgutJ1', 200, 9, 680, 93);
2 changes: 1 addition & 1 deletion backend/src/models/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package models
import "time"

type User struct {
ID string `gorm:"column:id;primaryKey;"`
ID string `gorm:"column:id;primaryKey;" json:"id"`
CreatedAt time.Time `json:"created_at" example:"2023-09-20T16:34:50Z"`
UpdatedAt time.Time `json:"updated_at" example:"2023-09-20T16:34:50Z"`
FirstName string `gorm:"type:varchar(255)" json:"first_name"`
Expand Down
4 changes: 4 additions & 0 deletions backend/src/services/etrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"backend/src/models"
"backend/src/types"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
Expand Down Expand Up @@ -326,6 +327,9 @@ func getETradePortfolio(client *http.Client, tokens *models.OAuthTokens, account
func (s *ETradeService) GetUserPortfolio(userID string) (models.UserPortfolio, error) {
var portfolio models.UserPortfolio
if err := s.DB.Preload("Positions").Where("user_id = ?", userID).First(&portfolio).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return models.UserPortfolio{}, nil
}
return models.UserPortfolio{}, fmt.Errorf("error retrieving all positions from the database: %s", err)
}

Expand Down
119 changes: 74 additions & 45 deletions frontend/components/ProfileBanner.tsx
Original file line number Diff line number Diff line change
@@ -1,86 +1,115 @@
import { StyleSheet, Text, View, Image, TouchableOpacity } from 'react-native'
import {
StyleSheet,
Text,
View,
Image,
TouchableOpacity,
Pressable,
} from 'react-native';
// import { theme } from '../../theme'
import React from 'react'
import React, { useEffect, useState } from 'react';
// import ActionButton from '../ActionButton'
// import { useNavigation } from '@react-navigation/native'
import ProfileBio from './ProfileBio'
// import { useSession } from '@clerk/clerk-expo'
import ProfileBio from './ProfileBio';
import { useNavigation } from '@react-navigation/native';
import { AuthNavigationProp } from '../types/navigationTypes';
import { getUserFollowers, getUserFollowing } from '../services/followers';
import { User } from '../types/types';
import { useSession } from '@clerk/clerk-expo';

interface ProfileBannerProps {
user?: string
user: User;
}

const ProfileBanner = ({ user }: ProfileBannerProps) => {
/*
const { session } = useSession()
const navigation = useNavigation()
const { session } = useSession();
const navigation = useNavigation<AuthNavigationProp>();
const [following, setFollowing] = useState<User[]>([]);
const [followers, setFollowers] = useState<User[]>([]);

const navigateToEditProfile = () => {
navigation.navigate({ name: "Edit My Profile" })
}
*/
const navigateToFollowers = () => {
navigation.navigate('Followers', { label: 'Followers', users: followers });
};

return (
<View className='flex flex-col px-4 mb-2'>
const navigateToFollowing = () => {
navigation.navigate('Followers', { label: 'Followers', users: following });
};

useEffect(() => {
getUserFollowers(user.id).then(users => {
setFollowers(users);
});
getUserFollowing(user.id).then(users => {
setFollowing(users);
});
}, []);

<View className='flex flex-row items-center justify-between gap-1 mb-4 mt-3'>
console.log(user);

return (
<View className="flex flex-col px-4 mb-2">
<View className="flex flex-row items-center justify-between gap-1 mb-4 mt-3">
<Image
// must be a perfect circle
className='w-32 h-32'
className="w-32 h-32"
style={profileStyles.profileImage}
source={{ uri: "currentAuth?.photoURL" }}
source={{ uri: session?.user.imageUrl }}
/>

<View className='flex flex-col items-center flex-1 gap-2'>
<View className='flex flex-row justify-evenly flex-1' >

<View className='flex flex-col items-center px-4 py-2'>
<Text className='text-sm font-semibold'>10</Text>
<Text className='text-sm font-semibold'>
Followers
<View className="flex flex-col items-center flex-1 gap-2">
<View className="flex flex-row justify-evenly flex-1">
<Pressable
className="flex flex-col items-center px-4 py-2"
onPress={navigateToFollowers}>
<Text className="text-sm font-semibold">
{followers?.length | 0}
</Text>
</View>
<Text className="text-sm font-semibold">Followers</Text>
</Pressable>

<View className='flex flex-col items-center px-4 py-2'>
<Text className='text-sm font-semibold'>{10}</Text>
<Text adjustsFontSizeToFit={true} numberOfLines={1} className='text-sm font-semibold'>
<Pressable
className="flex flex-col items-center px-4 py-2"
onPress={navigateToFollowing}>
<Text className="text-sm font-semibold">
{following?.length | 0}
</Text>
<Text
adjustsFontSizeToFit={true}
numberOfLines={1}
className="text-sm font-semibold">
Following
</Text>
</View>
</Pressable>
</View>

<TouchableOpacity
className='flex items-center justify-center flex-1 mb-5 w-48'
style={profileStyles.followButton}
onPress={() => console.log(user)}
>
<Text className='font-semibold text-[#02AD98]'>Edit Profile</Text>
className="flex items-center justify-center flex-1 mb-5 w-48"
style={profileStyles.followButton}>
<Text className="font-semibold text-[#02AD98]">Edit Profile</Text>
</TouchableOpacity>
</View>
</View>

<ProfileBio
fullName="first_name last_name"
fullName={`${user.first_name || session?.user.firstName} ${user.last_name || session?.user.lastName}`}
description="profile description? Lorem ipsum dolor sit amet, consectetur adipiscing elit.Praesent vel nisi sed diam ultricies viverra sit amet nec dolor...."
/>

</View>
)
}

export default ProfileBanner
);
};

export default ProfileBanner;

const profileStyles = StyleSheet.create({
profileImage: {
borderRadius: 180,
borderColor: "black",
borderWidth: 2,
borderColor: '#CCCCCC',
borderWidth: 1,
},
followButton: {
backgroundColor: "rgba(2, 173, 152, 0.18)",
backgroundColor: 'rgba(2, 173, 152, 0.18)',
paddingVertical: 5,
paddingHorizontal: 20,
borderRadius: 5
borderRadius: 5,
},
})
});
25 changes: 25 additions & 0 deletions frontend/components/UserItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { View, Text } from 'react-native'
import React from 'react'
import { Icon } from '@rneui/themed';
import { User } from '../types/types';

interface UserItemProps {
user: User;
}

const UserItem = ({ user }: UserItemProps) => {
return (
<View className='flex flex-row items-center p-4'>
<View className='flex flex-row items-center flex-1 space-x-3'>
<Icon type='material-community' name="account" size={30} color='white' backgroundColor="#D5D5D5" style={{
padding: 2
}} />
<View className='flex flex-col flex-1 pr-3'>
<Text className='font-medium' >{user.username}</Text>
</View>
</View>
</View>
)
}

export default UserItem
4 changes: 2 additions & 2 deletions frontend/pages/AuthPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React, {useEffect, useState} from 'react'
import {RefreshControl, ScrollView} from "nativewind/dist/preflight";
import ETradeAuth from "../components/ETradeAuth";
import {TokenStatus} from "../types/types";
import {getTokenStatus} from "../services/users";
import {getTokenStatus} from "../services/etrade";

const AuthPage = () => {
const [authenticated, setAuthenticated] = useState(false)
Expand All @@ -15,7 +15,7 @@ const AuthPage = () => {
}

const getETradeTokenStatus = async () => {
const callback: TokenStatus = await getTokenStatus(2);
const callback: TokenStatus = await getTokenStatus('user_2chL8dX6HdbBAuvu3DDM9f9NzKK');
if (callback.status === "active") {
setAuthenticated(true)
} else {
Expand Down
44 changes: 44 additions & 0 deletions frontend/pages/Followers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { FlatList, Pressable } from 'react-native';
import React, { useEffect } from 'react';
import { Icon } from '@rneui/themed';
import { useNavigation, useRoute } from '@react-navigation/native';
import { FollowerRouteParams, User } from '../types/types';
import UserItem from '../components/UserItem';
import { AuthNavigationProp } from '../types/navigationTypes';

const Followers = () => {
const navigation = useNavigation<AuthNavigationProp>();
const route = useRoute();
const users = (route.params as FollowerRouteParams)?.users;

useEffect(() => {
// set the title of the page
navigation.setOptions({
headerShown: true,
headerTitle: 'Followers',
headerTitleAlign: 'center',
headerLeft: () => (
<Pressable onPress={() => navigation.goBack()}>
<Icon type="material-community" name="chevron-left" size={30} color="black" style={{ paddingLeft: 5 }} />
</Pressable>
),
});
}, []);

const navigateToProfile = (user: User) => {
navigation.navigate('FollowerProfile', { user: user })
};

return (
<FlatList
data={users}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<Pressable onPress={() => navigateToProfile(item)}>
<UserItem user={item} />
</Pressable>
)} />
);
};

export default Followers;
28 changes: 13 additions & 15 deletions frontend/pages/Profile.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useNavigation } from '@react-navigation/native';
import { useNavigation, useRoute } from '@react-navigation/native';
import React, { useEffect, useState } from 'react';
import { FlatList, Pressable, ScrollView, View } from 'react-native';
import { useSession } from '@clerk/clerk-expo';
Expand All @@ -10,37 +10,34 @@ import { ProfileActivityData } from '../constants';
import ProfilePerformance from '../components/ProfilePerformance';
import SignOut from '../components/SignOutButton';
import { getPortoflio } from '../services/etrade';
import { UserPortfolio } from '../types/types';
import { ProfileRouteParams, UserPortfolio } from '../types/types';
import { ProfilePositions } from '../components/ProfilePositions';
// import SettingsSvg from '../assets/SettingsIcon.svg';



const Profile = () => {
const { session } = useSession();
const navigation = useNavigation();
const [isPortfolioSelected, setIsPortfolioSelected] = useState<boolean>(true);
const [isActivitySelected, setIsActivitySelected] = useState<boolean>(false);
const [pageNumber, setPageNumber] = useState<number>(0);
const [portfolio, setPortfolio] = useState<UserPortfolio>()
const route = useRoute()
const isFollowerProfile = (route.params as ProfileRouteParams)?.user !== undefined;
const user = (route.params as ProfileRouteParams)?.user || session!.user!;

const OnActivitySelected = () => {
setIsPortfolioSelected(false);
setIsActivitySelected(true);
setPageNumber(1)
console.log(`Activity selected: ${isActivitySelected}`);
}

const OnPortfolioSelected = () => {
setIsPortfolioSelected(true);
setIsActivitySelected(false);
setPageNumber(0)
console.log(`Portfolio selected: ${isPortfolioSelected}`);
}

useEffect(() => {
// set the title of the page
navigation.setOptions({
headerShown: true,
headerTitle: `@${session?.user.username}`,
headerTitle: `@${user.username}`,
headerTitleAlign: 'center',
headerRight: () => (
<Icon type='material-community' name='cog' size={30} color='black' style={{paddingRight: 10}} />
Expand All @@ -53,7 +50,6 @@ const Profile = () => {
})

return navigation.addListener('focus', () => {
console.log(`Profile Page | session token: ${session?.getToken()}`);
if (session?.user.username === undefined) {
/* Unsure why casting to never is required, issue to look into */
navigation.navigate('Signin' as never);
Expand All @@ -62,19 +58,21 @@ const Profile = () => {
}, []);

useEffect(() => {
getPortoflio("user_2ceWSEk1tU7bByPHmtwsla94w7e").then(userPortfolio => {
getPortoflio(user.id).then(userPortfolio => {
setPortfolio(userPortfolio)
})
}, []);

return (
<ScrollView className='bg-white'>
<View className='flex flex-col space-y-2'>
<ProfileBanner />
<ProfileBanner user={user}/>

<View className='flex flex-row'>
<SubTabButton title='Portfolio' selected={pageNumber == 0} onPress={OnPortfolioSelected} />
<SubTabButton title='Activity' selected={pageNumber == 1} onPress={OnActivitySelected} />
{!isFollowerProfile && (
<SubTabButton title='Activity' selected={pageNumber == 1} onPress={OnActivitySelected} />
)}
</View>

<ScrollView
Expand Down
2 changes: 1 addition & 1 deletion frontend/reducers/onboarding/onboardingReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const onboardingSlice = createSlice({
financialGoalsShortTerm: [],
financialGoalsLongTerm: [],
financialLiteracy: [],
isOnboarding: 'normal', // 'onboarding', 'normal', 'makingPost'
isOnboarding: 'onboarding', // 'onboarding', 'normal', 'makingPost'
},
reducers: {
updateFirstName(state, action) {
Expand Down
Loading

0 comments on commit 586bd1d

Please sign in to comment.