From aa3ba469a72acf63248158a2d47d6978694a479d Mon Sep 17 00:00:00 2001 From: DamianUduevbo <58115973+DamianUduevbo@users.noreply.github.com> Date: Fri, 12 Apr 2024 15:45:47 -0400 Subject: [PATCH] Dummy Profile Page (#41) * 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 --- backend/src/controllers/etrade.go | 4 +- backend/src/models/positions.go | 20 ++-- backend/src/services/etrade.go | 30 +++--- frontend/assets/SettingsIcon.svg | 3 + frontend/components/ActivityItem.tsx | 33 ++++++ frontend/components/ETradeAuth.tsx | 3 +- frontend/components/ProfileBanner.tsx | 86 ++++++++++++++++ frontend/components/ProfileBio.tsx | 18 ++++ frontend/components/ProfilePerformance.tsx | 25 +++++ frontend/components/ProfilePositions.tsx | 49 +++++++++ frontend/components/SubTabButton.tsx | 30 ++++++ frontend/constants.ts | 56 ++++++++++ frontend/pages/Profile.tsx | 114 +++++++++++++++++---- frontend/services/etrade.ts | 28 +++++ frontend/services/users.ts | 23 +---- frontend/services/utils.ts | 6 ++ frontend/types/types.d.ts | 28 ++++- 17 files changed, 485 insertions(+), 71 deletions(-) create mode 100644 frontend/assets/SettingsIcon.svg create mode 100644 frontend/components/ActivityItem.tsx create mode 100644 frontend/components/ProfileBanner.tsx create mode 100644 frontend/components/ProfileBio.tsx create mode 100644 frontend/components/ProfilePerformance.tsx create mode 100644 frontend/components/ProfilePositions.tsx create mode 100644 frontend/components/SubTabButton.tsx create mode 100644 frontend/constants.ts create mode 100644 frontend/services/etrade.ts create mode 100644 frontend/services/utils.ts diff --git a/backend/src/controllers/etrade.go b/backend/src/controllers/etrade.go index c82d2540..8f52ac0f 100644 --- a/backend/src/controllers/etrade.go +++ b/backend/src/controllers/etrade.go @@ -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) { @@ -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) { diff --git a/backend/src/models/positions.go b/backend/src/models/positions.go index aa7ec501..89eeaef1 100644 --- a/backend/src/models/positions.go +++ b/backend/src/models/positions.go @@ -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 diff --git a/backend/src/services/etrade.go b/backend/src/services/etrade.go index 94a6f1e3..a0627657 100644 --- a/backend/src/services/etrade.go +++ b/backend/src/services/etrade.go @@ -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) @@ -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 @@ -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 { @@ -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 { @@ -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) @@ -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 diff --git a/frontend/assets/SettingsIcon.svg b/frontend/assets/SettingsIcon.svg new file mode 100644 index 00000000..bfc9ace4 --- /dev/null +++ b/frontend/assets/SettingsIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/components/ActivityItem.tsx b/frontend/components/ActivityItem.tsx new file mode 100644 index 00000000..182d0696 --- /dev/null +++ b/frontend/components/ActivityItem.tsx @@ -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 ( + + + + + {title} + + {description} + + + + + + + + + ) +} + +export default ActivityItem \ No newline at end of file diff --git a/frontend/components/ETradeAuth.tsx b/frontend/components/ETradeAuth.tsx index 96b75ed5..c9c3fd79 100644 --- a/frontend/components/ETradeAuth.tsx +++ b/frontend/components/ETradeAuth.tsx @@ -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' @@ -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) diff --git a/frontend/components/ProfileBanner.tsx b/frontend/components/ProfileBanner.tsx new file mode 100644 index 00000000..3d1b2c47 --- /dev/null +++ b/frontend/components/ProfileBanner.tsx @@ -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 ( + + + + + + + + + + 10 + + Followers + + + + + {10} + + Following + + + + + console.log(user)} + > + Edit Profile + + + + + + + + ) +} + +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 + }, +}) \ No newline at end of file diff --git a/frontend/components/ProfileBio.tsx b/frontend/components/ProfileBio.tsx new file mode 100644 index 00000000..2d17db73 --- /dev/null +++ b/frontend/components/ProfileBio.tsx @@ -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 ( + + {fullName} + {description} + + ) +} + +export default ProfileBio \ No newline at end of file diff --git a/frontend/components/ProfilePerformance.tsx b/frontend/components/ProfilePerformance.tsx new file mode 100644 index 00000000..5189ec6a --- /dev/null +++ b/frontend/components/ProfilePerformance.tsx @@ -0,0 +1,25 @@ +import { View, Text, Pressable } from 'react-native' +import React from 'react' + +interface ProfilePerformanceProps { + portfolioValue: number +} + +const ProfilePerformance = ({portfolioValue} : ProfilePerformanceProps) => { + return ( + + Performance + + = 0 ? 'text-[#02AD98]' : 'text-[#FF2B51]'}`}> + {portfolioValue} % + + + Copy Trades + + + YTD Performance + + ) +} + +export default ProfilePerformance \ No newline at end of file diff --git a/frontend/components/ProfilePositions.tsx b/frontend/components/ProfilePositions.tsx new file mode 100644 index 00000000..dc58dd74 --- /dev/null +++ b/frontend/components/ProfilePositions.tsx @@ -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 ( + + + + Symbol + Last Price + Mkt Val / Qty + Open P/L + + {positions?.map((position, index) => ( + + {position.ticker} + {prettifyMoney(position.cost)} + + + + {prettifyMoney(position.cost * position.quantity)} + + + {prettifyMoney(position.quantity)} + + + + + + = 0 ? 'text-[#02ad99]' : 'text-[#fe2b51]'}> + {prettifyMoney(position.total_gain)} + + = 0 ? 'text-[#02ad99]' : 'text-[#fe2b51]'}> + {position.total_gain_pct}% + + + + + ))} + + +)} \ No newline at end of file diff --git a/frontend/components/SubTabButton.tsx b/frontend/components/SubTabButton.tsx new file mode 100644 index 00000000..8f10b76c --- /dev/null +++ b/frontend/components/SubTabButton.tsx @@ -0,0 +1,30 @@ +import { Text, Pressable } from 'react-native' +import React from 'react' + +interface SubTabButtonProps { + title: string + selected: boolean + onPress: () => void +} + +const SubTabButton = ({ title, selected, onPress }: SubTabButtonProps) => { + return ( + { + //setIsSelected(!isSelected); + onPress(); + }} + > + + {title} + + + ) +} + +export default SubTabButton \ No newline at end of file diff --git a/frontend/constants.ts b/frontend/constants.ts new file mode 100644 index 00000000..e0492b04 --- /dev/null +++ b/frontend/constants.ts @@ -0,0 +1,56 @@ +export const ProfileActivityData = [ + { + id: 1, + title: 'Notification Title', + description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent vel nisi sed diam ultricies viverra sit amet nec dolor....', + icon: 'bell', + }, + { + id: 2, + title: 'Notification Title', + description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent vel nisi sed diam ultricies viverra sit amet nec dolor....', + icon: 'bell', + }, + { + id: 3, + title: 'Notification Title', + description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent vel nisi sed diam ultricies viverra sit amet nec dolor....', + icon: 'bell', + }, + { + id: 4, + title: 'Notification Title', + description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent vel nisi sed diam ultricies viverra sit amet nec dolor....', + icon: 'bell', + }, + { + id: 5, + title: 'Notification Title', + description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent vel nisi sed diam ultricies viverra sit amet nec dolor....', + icon: 'bell', + }, + { + id: 6, + title: 'Notification Title', + description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent vel nisi sed diam ultricies viverra sit amet nec dolor....', + icon: 'bell', + }, + { + id: 7, + title: 'Notification Title', + description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent vel nisi sed diam ultricies viverra sit amet nec dolor....', + icon: 'bell', + }, + { + id: 8, + title: 'Notification Title', + description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent vel nisi sed diam ultricies viverra sit amet nec dolor....', + icon: 'bell', + }, + { + id: 9, + title: 'Notification Title', + description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent vel nisi sed diam ultricies viverra sit amet nec dolor....', + icon: 'bell', + } +]; \ No newline at end of file diff --git a/frontend/pages/Profile.tsx b/frontend/pages/Profile.tsx index 5e88f717..145eda15 100644 --- a/frontend/pages/Profile.tsx +++ b/frontend/pages/Profile.tsx @@ -1,43 +1,117 @@ import { useNavigation } from '@react-navigation/native'; -import React, { useEffect } from 'react'; -import { StyleSheet, View, Text } from 'react-native'; +import React, { useEffect, useState } from 'react'; +import { FlatList, Pressable, ScrollView, View } from 'react-native'; import { useSession } from '@clerk/clerk-expo'; +import ProfileBanner from '../components/ProfileBanner'; +import SubTabButton from '../components/SubTabButton'; +import { Icon } from '@rneui/themed'; +import ActivityItem from '../components/ActivityItem'; +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 { ProfilePositions } from '../components/ProfilePositions'; +// import SettingsSvg from '../assets/SettingsIcon.svg'; const Profile = () => { const { session } = useSession(); const navigation = useNavigation(); + const [isPortfolioSelected, setIsPortfolioSelected] = useState(true); + const [isActivitySelected, setIsActivitySelected] = useState(false); + const [pageNumber, setPageNumber] = useState(0); + const [portfolio, setPortfolio] = useState() + + 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(() => { - const unsubscribe = navigation.addListener('focus', () => { + // set the title of the page + navigation.setOptions({ + headerShown: true, + headerTitle: `@${session?.user.username}`, + headerTitleAlign: 'center', + headerRight: () => ( + + ), + headerLeft: () => ( + navigation.goBack()}> + + + ), + }) + + 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); } }); + }, []); - return unsubscribe; + useEffect(() => { + getPortoflio("user_2ceWSEk1tU7bByPHmtwsla94w7e").then(userPortfolio => { + setPortfolio(userPortfolio) + }) }, []); return ( - - Hello {session?.user.username}! + + + + + + + + + + { + const offset = event.nativeEvent.contentOffset.x; + const page = Math.round(offset / 375); + console.log(`Page number: ${page}`); + setPageNumber(page); + }} + > + {pageNumber === 0 && ( + + + + + )} + + {pageNumber == 1 && ( + item.id.toString()} + renderItem={({ item }) => ( + + )} /> + )} + + + + - + ); }; -const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - text: { - fontSize: 24, - textAlign: 'center', - }, -}); - -export default Profile; +export default Profile; \ No newline at end of file diff --git a/frontend/services/etrade.ts b/frontend/services/etrade.ts new file mode 100644 index 00000000..bc9b6228 --- /dev/null +++ b/frontend/services/etrade.ts @@ -0,0 +1,28 @@ +import { Redirect, UserPortfolio } from '../types/types'; +import axios, { AxiosResponse, HttpStatusCode } from 'axios'; +import { API_LINK } from './CommonDocs'; + +export const getCallbackUrl = async (id: string): Promise => { + const response: AxiosResponse = await axios.get( + `http://${API_LINK}/etrade/redirect/${id}`, + ); + return response.data; +}; + +export const verifyToken = async ( + id: string, + verifier: string, +): Promise => { + const response: AxiosResponse = await axios.post( + `http://${API_LINK}/etrade/verify/${id}`, + { verifier: verifier }, + ); + return response.status; +}; + +export const getPortoflio = async (id: string): Promise => { + const response: AxiosResponse = await axios.get( + `http://${API_LINK}/etrade/portfolio/${id}`, + ); + return response.data; +} diff --git a/frontend/services/users.ts b/frontend/services/users.ts index 18895a67..895c0c27 100644 --- a/frontend/services/users.ts +++ b/frontend/services/users.ts @@ -1,6 +1,6 @@ -import axios, { AxiosResponse, HttpStatusCode } from 'axios'; +import axios, { AxiosResponse } from 'axios'; import { API_LINK } from './CommonDocs'; -import { FinancialGoal, Redirect, TokenStatus, User } from '../types/types'; +import { FinancialGoal, TokenStatus, User } from '../types/types'; export const getAllUsers = async (): Promise => { console.log(API_LINK); @@ -11,25 +11,6 @@ export const getAllUsers = async (): Promise => { return response.data; }; -export const getCallbackUrl = async (id: string): Promise => { - const response: AxiosResponse = await axios.get( - `http://${API_LINK}/etrade/redirect/${id}`, - ); - console.log(response.data); - return response.data; -}; - -export const verifyToken = async ( - id: string, - verifier: string, -): Promise => { - const response: AxiosResponse = await axios.post( - `http://${API_LINK}/etrade/verify/${id}`, - { verifier: verifier }, - ); - return response.status; -}; - export const getTokenStatus = async (id: number): Promise => { const response: AxiosResponse = await axios.get( `http://${API_LINK}/etrade/status/${id}`, diff --git a/frontend/services/utils.ts b/frontend/services/utils.ts new file mode 100644 index 00000000..ec3d1172 --- /dev/null +++ b/frontend/services/utils.ts @@ -0,0 +1,6 @@ +export const prettifyMoney = (amount: number) => { + const numberAmount = parseFloat(String(amount)); + return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format( + numberAmount + ); +}; \ No newline at end of file diff --git a/frontend/types/types.d.ts b/frontend/types/types.d.ts index 8b116224..a2bfbae9 100644 --- a/frontend/types/types.d.ts +++ b/frontend/types/types.d.ts @@ -32,4 +32,30 @@ export type ClerkError = { code: string; message: string; longMessage: string; -}; \ No newline at end of file +}; + +interface UserPortfolio { + user_id: string; + day_gain: number; + day_gain_pct: number; + total_gain: number; + total_gain_pct: number; + positions: Position[]; +} + +interface Position { + portfolio_id: number; + position_id: number; + ticker: string; + quantity: number; + cost: number; + day_gain: number; + day_gain_pct: number; + total_gain: number; + total_gain_pct: number; + type: TradeType; +} +enum TradeType { + LONG = 'LONG', + SHORT = 'SHORT' +}