Skip to content

Commit

Permalink
Dummy Profile Page (#41)
Browse files Browse the repository at this point in the history
* profile page layout implemented. more stylings needs to be done, esp with the Edit Profile button. For that we will use a Pressable or TocuhOpacity component.

* stylized to closely represent the mid-fi from Figma. still needs an Activity and Profile section.

* Activity and Profile sections built.

* implemented page scrolling on the profile page

* linted

* working on portfolio position list, temp commit

* missing file

* added position table

* profile page updates, pos table

* padding

---------

Co-authored-by: Jakob Philippe <[email protected]>
  • Loading branch information
DamianUduevbo and jakobphilippe authored Apr 12, 2024
1 parent 36ded09 commit aa3ba46
Show file tree
Hide file tree
Showing 17 changed files with 485 additions and 71 deletions.
4 changes: 2 additions & 2 deletions backend/src/controllers/etrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ func (etc *ETradeController) Status(c *gin.Context) {
// @ID etrade-sync-portfolio
// @Tags etrade
// @Produce json
// @Success 200 {object} []UserPortfolio
// @Success 200 {object} UserPortfolio
// @Failure 500 {string} string "Failed to sync portfolio"
// @Router /etrade/sync/{user_id} [post]
func (etc *ETradeController) Sync(c *gin.Context) {
Expand All @@ -137,7 +137,7 @@ func (etc *ETradeController) Sync(c *gin.Context) {
// @ID etrade-get-portfolio
// @Tags etrade
// @Produce json
// @Success 200 {object} []UserPortfolio
// @Success 200 {object} UserPortfolio
// @Failure 500 {string} string "Failed to sync portfolio"
// @Router /etrade/portfolio/{user_id} [get]
func (etc *ETradeController) GetPortfolio(c *gin.Context) {
Expand Down
20 changes: 10 additions & 10 deletions backend/src/models/positions.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ import "backend/src/types"

type Position struct {
types.Model
UserPortfolioID uint `gorm:"not null" json:"portfolio_id,omitempty"`
PositionID int `gorm:"not null" json:"position_id,omitempty"`
Ticker string `gorm:"not null" json:"ticker,omitempty"`
Quantity int `gorm:"not null" json:"quantity,omitempty"`
Cost float64 `gorm:"type:numeric(12,2);not null" json:"cost,omitempty"`
DayGain float64 `gorm:"type:numeric(12,2);not null" json:"day_gain,omitempty"`
DayGainPct float64 `gorm:"type:numeric(12,2);not null" json:"day_gain_pct,omitempty"`
TotalGain float64 `gorm:"type:numeric(12,2);not null" json:"total_gain,omitempty"`
TotalGainPct float64 `gorm:"type:numeric(12,2);not null" json:"total_gain_pct,omitempty"`
Type TradeType `gorm:"type:trade_type_enum;not null" json:"type,omitempty"`
UserPortfolioID uint `gorm:"not null" json:"portfolio_id"`
PositionID int `gorm:"not null" json:"position_id"`
Ticker string `gorm:"not null" json:"ticker"`
Quantity int `gorm:"not null" json:"quantity"`
Cost float64 `gorm:"type:numeric(12,2);not null" json:"cost"`
DayGain float64 `gorm:"type:numeric(12,2);not null" json:"day_gain"`
DayGainPct float64 `gorm:"type:numeric(12,2);not null" json:"day_gain_pct"`
TotalGain float64 `gorm:"type:numeric(12,2);not null" json:"total_gain"`
TotalGainPct float64 `gorm:"type:numeric(12,2);not null" json:"total_gain_pct"`
Type TradeType `gorm:"type:trade_type_enum;not null" json:"type"`
}

// TradeType defines a custom type for trade type enum
Expand Down
30 changes: 15 additions & 15 deletions backend/src/services/etrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,17 +139,17 @@ func (s *ETradeService) GetAccessTokenStatus(userID string) (string, error) {

// SyncPortfolio fetches the portfolio data for each account
// inserts new trades, updates existing, and deletes ones not returned from ETrade
func (s *ETradeService) SyncPortfolio(userID string) ([]models.UserPortfolio, error) {
func (s *ETradeService) SyncPortfolio(userID string) (models.UserPortfolio, error) {
oauthTokens, err := s.getLastOAuthTokens(userID)
if err != nil {
return []models.UserPortfolio{}, fmt.Errorf("error getting oauth token: %s", err)
return models.UserPortfolio{}, fmt.Errorf("error getting oauth token: %s", err)
}

client := newJSONClient()

accounts, err := getETradeAccounts(client, oauthTokens)
if err != nil {
return []models.UserPortfolio{}, fmt.Errorf("error getting etrade accounts: %s", err)
return models.UserPortfolio{}, fmt.Errorf("error getting etrade accounts: %s", err)
}

visitedAccounts := make(map[string]bool)
Expand All @@ -159,13 +159,13 @@ func (s *ETradeService) SyncPortfolio(userID string) ([]models.UserPortfolio, er
if err := s.DB.Where("user_id = ?", userID).FirstOrCreate(
&dbPortfolio, models.UserPortfolio{UserID: userID},
).Error; err != nil {
return []models.UserPortfolio{}, fmt.Errorf("error retrieving portfolio from the database: %s", err)
return models.UserPortfolio{}, fmt.Errorf("error retrieving portfolio from the database: %s", err)
}

// Retrieve current positions from the database
var dbPositions []models.Position
if err := s.DB.Where("user_portfolio_id = ?", dbPortfolio.ID).Find(&dbPositions).Error; err != nil {
return []models.UserPortfolio{}, fmt.Errorf("error retrieving positions from the database: %s", err)
return models.UserPortfolio{}, fmt.Errorf("error retrieving positions from the database: %s", err)
}

// Map to store existing positions for quick lookup
Expand All @@ -181,7 +181,7 @@ func (s *ETradeService) SyncPortfolio(userID string) ([]models.UserPortfolio, er
for _, account := range accounts {
portfolioList, err = getETradePortfolio(client, oauthTokens, account.AccountIDKey)
if err != nil {
return []models.UserPortfolio{}, fmt.Errorf("error getting portfolio: %s", err)
return models.UserPortfolio{}, fmt.Errorf("error getting portfolio: %s", err)
}

for _, portfolio := range portfolioList {
Expand Down Expand Up @@ -215,7 +215,7 @@ func (s *ETradeService) SyncPortfolio(userID string) ([]models.UserPortfolio, er
existingPos.Type = models.TradeType(position.PositionType)
// Save updates to the database
if err := s.DB.Save(&existingPos).Error; err != nil {
return []models.UserPortfolio{}, fmt.Errorf("error updating position: %s", err)
return models.UserPortfolio{}, fmt.Errorf("error updating position: %s", err)
}
delete(existingPositions, position.PositionID) // Remove from existing positions map
} else {
Expand Down Expand Up @@ -251,19 +251,19 @@ func (s *ETradeService) SyncPortfolio(userID string) ([]models.UserPortfolio, er
dbPortfolio.DayGainPct = dayGainPct

if err := s.DB.Save(&dbPortfolio).Error; err != nil {
return []models.UserPortfolio{}, fmt.Errorf("error updating portfolio: %s", err)
return models.UserPortfolio{}, fmt.Errorf("error updating portfolio: %s", err)
}

// Delete positions that are not present in the current data set
for _, pos := range existingPositions {
if err := s.DB.Delete(&pos).Error; err != nil {
return []models.UserPortfolio{}, fmt.Errorf("error deleting position: %s", err)
return models.UserPortfolio{}, fmt.Errorf("error deleting position: %s", err)
}
}

tx := s.DB.CreateInBatches(&positions, 10)
if tx.Error != nil {
return []models.UserPortfolio{}, fmt.Errorf("error creating positions: %s", tx.Error.Error())
return models.UserPortfolio{}, fmt.Errorf("error creating positions: %s", tx.Error.Error())
}

return s.GetUserPortfolio(userID)
Expand Down Expand Up @@ -323,13 +323,13 @@ func getETradePortfolio(client *http.Client, tokens *models.OAuthTokens, account
}

// GetUserPortfolio returns the user portfolio from our db
func (s *ETradeService) GetUserPortfolio(userID string) ([]models.UserPortfolio, error) {
var allPositions []models.UserPortfolio
if err := s.DB.Preload("Positions").Where("user_id = ?", userID).Find(&allPositions).Error; err != nil {
return []models.UserPortfolio{}, fmt.Errorf("error retrieving all positions from the database: %s", err)
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 {
return models.UserPortfolio{}, fmt.Errorf("error retrieving all positions from the database: %s", err)
}

return allPositions, nil
return portfolio, nil
}

// newJSONClient is a helper function that creates an HTTP client to interact with the E*Trade API
Expand Down
3 changes: 3 additions & 0 deletions frontend/assets/SettingsIcon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 33 additions & 0 deletions frontend/components/ActivityItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { View, Text, TouchableOpacity } from 'react-native'
import React from 'react'
import { Icon } from '@rneui/themed';

interface ActivityItemProps {
title: string;
description: string;
icon: string;
}

const ActivityItem = ({ title, description, icon }: ActivityItemProps) => {
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={icon} size={30} color='white' backgroundColor="#D5D5D5" style={{
padding: 10
}} />
<View className='flex flex-col flex-1 pr-3'>
<Text className='font-medium' >{title}</Text>
<Text className='text-[#535353] text-sm' numberOfLines={1} ellipsizeMode='tail'>
{description}
</Text>
</View>
</View>

<TouchableOpacity className='bg-[#E7E7E7] rounded-lg px-2 py-2' >
<Icon type='material-community' name='chevron-right' size={30} color='black' />
</TouchableOpacity>
</View>
)
}

export default ActivityItem
3 changes: 1 addition & 2 deletions frontend/components/ETradeAuth.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {Text, View, Linking, TouchableOpacity, TextInput, Alert, StyleSheet} from 'react-native'
import React, {useState} from 'react'
import { Button } from 'react-native-paper'
import {getCallbackUrl, verifyToken} from '../services/users'
import {getCallbackUrl, verifyToken} from '../services/etrade'
import {Redirect} from '../types/types'
import {HttpStatusCode} from "axios";
import { useUser } from '@clerk/clerk-expo'
Expand All @@ -18,7 +18,6 @@ const AuthPage = (props: { successCallback: () => void }) => {
}

const id = user.id;
console.log(id)

const callback: Redirect = await getCallbackUrl(id);
setRedirectUrl(callback.redirect_url)
Expand Down
86 changes: 86 additions & 0 deletions frontend/components/ProfileBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { StyleSheet, Text, View, Image, TouchableOpacity } from 'react-native'
// import { theme } from '../../theme'
import React from 'react'
// import ActionButton from '../ActionButton'
// import { useNavigation } from '@react-navigation/native'
import ProfileBio from './ProfileBio'
// import { useSession } from '@clerk/clerk-expo'

interface ProfileBannerProps {
user?: string
}

const ProfileBanner = ({ user }: ProfileBannerProps) => {
/*
const { session } = useSession()
const navigation = useNavigation()
const navigateToEditProfile = () => {
navigation.navigate({ name: "Edit My Profile" })
}
*/

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'
style={profileStyles.profileImage}
source={{ uri: "currentAuth?.photoURL" }}
/>

<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
</Text>
</View>

<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'>
Following
</Text>
</View>
</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>
</TouchableOpacity>
</View>
</View>

<ProfileBio
fullName="first_name last_name"
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


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

interface ProfileBioProps {
fullName: string
description: string
}

const ProfileBio = ({ fullName, description }: ProfileBioProps) => {
return (
<View className='flex flex-col space-y-1 w-96'>
<Text className='text-base font-bold'>{fullName}</Text>
<Text>{description}</Text>
</View>
)
}

export default ProfileBio
25 changes: 25 additions & 0 deletions frontend/components/ProfilePerformance.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { View, Text, Pressable } from 'react-native'
import React from 'react'

interface ProfilePerformanceProps {
portfolioValue: number
}

const ProfilePerformance = ({portfolioValue} : ProfilePerformanceProps) => {
return (
<View className='flex flex-col justify-between space-y-1 py-3 px-6 border-b-[1px] border-b-gray-200'>
<Text className='font-semibold text-base'>Performance</Text>
<View className='flex flex-row justify-between items-center'>
<Text className={`text-2xl ${portfolioValue >= 0 ? 'text-[#02AD98]' : 'text-[#FF2B51]'}`}>
{portfolioValue} %
</Text>
<Pressable>
<Text>Copy Trades</Text>
</Pressable>
</View>
<Text>YTD Performance</Text>
</View>
)
}

export default ProfilePerformance
49 changes: 49 additions & 0 deletions frontend/components/ProfilePositions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { DataTable } from 'react-native-paper';
import { prettifyMoney } from '../services/utils';
import { Text, View } from 'react-native';
import React from 'react';
import { Position } from '../types/types';

interface ProfilePositionsProps {
positions: Position[] | undefined
}

export const ProfilePositions = ({positions}: ProfilePositionsProps) => {
return (
<View >
<DataTable>
<DataTable.Header>
<DataTable.Title>Symbol</DataTable.Title>
<DataTable.Title>Last Price</DataTable.Title>
<DataTable.Title>Mkt Val / Qty</DataTable.Title>
<DataTable.Title>Open P/L</DataTable.Title>
</DataTable.Header>
{positions?.map((position, index) => (
<DataTable.Row key={index}>
<DataTable.Cell>{position.ticker}</DataTable.Cell>
<DataTable.Cell>{prettifyMoney(position.cost)}</DataTable.Cell>
<DataTable.Cell>
<View className='flex flex-col'>
<Text>
{prettifyMoney(position.cost * position.quantity)}
</Text>
<Text>
{prettifyMoney(position.quantity)}
</Text>
</View>
</DataTable.Cell>
<DataTable.Cell>
<View className='flex flex-col'>
<Text className={position.total_gain >= 0 ? 'text-[#02ad99]' : 'text-[#fe2b51]'}>
{prettifyMoney(position.total_gain)}
</Text>
<Text className={position.total_gain >= 0 ? 'text-[#02ad99]' : 'text-[#fe2b51]'}>
{position.total_gain_pct}%
</Text>
</View>
</DataTable.Cell>
</DataTable.Row>
))}
</DataTable>
</View>
)}
Loading

0 comments on commit aa3ba46

Please sign in to comment.