diff --git a/backend/src/controllers/post.go b/backend/src/controllers/post.go index dec5094c..357bd82e 100644 --- a/backend/src/controllers/post.go +++ b/backend/src/controllers/post.go @@ -4,8 +4,11 @@ import ( "net/http" "strconv" + "fmt" + "backend/src/models" "backend/src/services" + "backend/src/types" "github.com/gin-gonic/gin" ) @@ -115,36 +118,64 @@ func (pc *PostController) GetPostsFromSearch(c *gin.Context) { c.JSON(http.StatusOK, posts) } -// CreatePost godoc -// -// @Summary Creates a post -// @Description Creates a post -// @ID create-post -// @Tags post -// @Accept json -// @Produce json -// @Param first_name body string true "First name of the post" -// @Param last_name body string true "Last name of the post" -// @Param postname body string true "Postname of the post" -// @Param email body string true "Email of the post" -// @Param password body string true "Password of the post" -// @Success 201 {object} models.Post -// @Failure 400 {string} string "Failed to create post" -// @Router /api/posts/ [post] -func (pc *PostController) CreatePost(c *gin.Context) { - var post models.Post - if err := c.ShouldBindJSON(&post); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input"}) +// CreateTradePost godoc +func (pc *PostController) CreateTradePost(c *gin.Context) { + var req types.CreateTradePostRequest + if err := c.BindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request data"}) return } - createdPost, err := pc.postService.CreatePost(&post) + userId := c.Param("user_id") + + fmt.Println(req.Title, req.Description) + + tradePost, err := pc.postService.CreateTradePost(userId, req.PercentData, req.TickerSymbol, req.Title, req.Description) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to create post"}) + return + } + + c.JSON(http.StatusCreated, tradePost) +} + +// CreatePortfolioPost godoc +func (pc *PostController) CreatePortfolioPost(c *gin.Context) { + var req types.CreatePortfolioPostRequest + if err := c.BindJSON(&req); err != nil { + fmt.Println(err) + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request data"}) + return + } + + userId := c.Param("user_id") + + portfolioPost, err := pc.postService.CreatePortfolioPost(userId, req.PercentData, req.SummaryType) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to create post"}) + return + } + + c.JSON(http.StatusCreated, portfolioPost) +} + +// CreateTextPost godoc +func (pc *PostController) CreateTextPost(c *gin.Context) { + var req types.CreateTextPostRequest + if err := c.BindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request data"}) + return + } + + userId := c.Param("user_id") + + textPost, err := pc.postService.CreateTextPost(userId, req.Title, req.Description) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to create post"}) return } - c.JSON(http.StatusCreated, createdPost) + c.JSON(http.StatusCreated, textPost) } // GetPostById godoc diff --git a/backend/src/db/migrations/7_POST_V1.sql b/backend/src/db/migrations/7_POST_V1.sql index 1ac77eb0..a5a25beb 100644 --- a/backend/src/db/migrations/7_POST_V1.sql +++ b/backend/src/db/migrations/7_POST_V1.sql @@ -11,8 +11,8 @@ CREATE TYPE post_type_enum AS ENUM ( --Create post table CREATE TABLE IF NOT EXISTS posts ( id SERIAL PRIMARY KEY, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, user_id VARCHAR(255) REFERENCES users(id) ON DELETE CASCADE, post_type post_type_enum NOT NULL, num_data FLOAT NOT NULL, diff --git a/backend/src/routes/post.go b/backend/src/routes/post.go index 934a7294..bab10416 100644 --- a/backend/src/routes/post.go +++ b/backend/src/routes/post.go @@ -18,7 +18,9 @@ func SetupPostRoutes(router *gin.Engine, db *gorm.DB) { postRoutes.GET("/user-posts/:user_id", postController.GetPostsByUserId) postRoutes.GET("/followed-posts/:user_id", postController.GetPostsFromFollowedUsers) postRoutes.GET("/search-posts", postController.GetPostsFromSearch) - postRoutes.POST("", postController.CreatePost) + postRoutes.POST("/create-trade-post/:user_id", postController.CreateTradePost) + postRoutes.POST("/create-portfolio-post/:user_id", postController.CreatePortfolioPost) + postRoutes.POST("/create-text-post/:user_id", postController.CreateTextPost) postRoutes.GET("/:id", postController.GetPostById) postRoutes.PUT("/:id", postController.UpdatePostById) postRoutes.DELETE("/:id", postController.DeletePostById) diff --git a/backend/src/services/post.go b/backend/src/services/post.go index d81c0855..aa641758 100644 --- a/backend/src/services/post.go +++ b/backend/src/services/post.go @@ -69,11 +69,53 @@ func (ps *PostService) GetPostsFromSearch(postContentSearchTerm string) ([]model return posts, nil } -func (ps *PostService) CreatePost(post *models.Post) (*models.Post, error) { - if err := ps.DB.Create(post).Error; err != nil { +func (ps *PostService) CreateTradePost(userId string, percentData float64, tickerSymbol string, title string, description string) (*models.Post, error) { + tradePost := &models.Post{ + UserID: userId, + NumData: percentData, + PostType: models.RECENT_TRADE, + TickerSymbol: tickerSymbol, + Title: title, + Comment: description, + } + + if err := ps.DB.Create(tradePost).Error; err != nil { return nil, err } - return post, nil + + return tradePost, nil +} + +func (ps *PostService) CreatePortfolioPost(userId string, percentData float64, summaryType string) (*models.Post, error) { + portfolioPost := &models.Post{ + UserID: userId, + NumData: percentData, + PostType: models.ONE_MONTH_SUMMARY, + Title: "Portfolio Summary", + Comment: summaryType, + } + + if err := ps.DB.Create(portfolioPost).Error; err != nil { + return nil, err + } + + return portfolioPost, nil +} + +func (ps *PostService) CreateTextPost(userId string, title string, description string) (*models.Post, error) { + textPost := &models.Post{ + UserID: userId, + NumData: 0, + PostType: models.SHARE_COMMENT, + Title: title, + Comment: description, + } + + if err := ps.DB.Create(textPost).Error; err != nil { + return nil, err + } + + return textPost, nil } func (ps *PostService) GetPostById(id uint) (*models.Post, error) { diff --git a/backend/src/types/model.go b/backend/src/types/model.go index 445bd0c2..77cb3293 100644 --- a/backend/src/types/model.go +++ b/backend/src/types/model.go @@ -118,3 +118,20 @@ type ClerkWebhookEvent struct { Object string `json:"object"` Type string `json:"type"` } + +type CreateTradePostRequest struct { + PercentData float64 `json:"percent_data" binding:"required"` + TickerSymbol string `json:"ticker_symbol" binding:"required"` + Title string `json:"title" binding:"required"` + Description string `json:"description" binding:"required"` +} + +type CreatePortfolioPostRequest struct { + PercentData float64 `json:"percent_data" binding:"required"` + SummaryType string `json:"summary_type" binding:"required"` +} + +type CreateTextPostRequest struct { + Title string `json:"title" binding:"required"` + Description string `json:"description" binding:"required"` +} \ No newline at end of file diff --git a/frontend/App.tsx b/frontend/App.tsx index 6db7c355..e411770a 100644 --- a/frontend/App.tsx +++ b/frontend/App.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { Provider } from 'react-redux'; import { configureStore } from '@reduxjs/toolkit'; import onboardingReducer from './reducers/onboarding/onboardingReducer'; +import makePostReducer from './reducers/makePost/makePostReducer'; import LayoutWrapper from './components/LayoutWrapper'; import { ClerkProvider } from '@clerk/clerk-expo'; //import 'react-native-gesture-handler'; @@ -10,6 +11,7 @@ import * as SecureStore from 'expo-secure-store'; const store = configureStore({ reducer: { onboarding: onboardingReducer, + makePost: makePostReducer, }, }); diff --git a/frontend/components/LayoutWrapper.tsx b/frontend/components/LayoutWrapper.tsx index 28d74a11..0f8d4e21 100644 --- a/frontend/components/LayoutWrapper.tsx +++ b/frontend/components/LayoutWrapper.tsx @@ -4,11 +4,14 @@ import BottomNavBar from '../router/BottomNavBar'; import AuthNavigator from '../router/AuthNavigation'; import { configureStore } from '@reduxjs/toolkit'; import onboardingReducer from '../reducers/onboarding/onboardingReducer'; +import makePostReducer from '../reducers/makePost/makePostReducer'; import { useSelector } from 'react-redux'; +import MakePostNavigator from '../router/MakePostNavigation'; const store = configureStore({ reducer: { onboarding: onboardingReducer, + makePost: makePostReducer, }, }); @@ -28,7 +31,7 @@ export default function LayoutWrapper() { { onboarding: , normal: , - makingPost: (null), + makingPost: , }[onboarding.isOnboarding] } {/* {session?.user !== undefined && !onboarding.isOnboarding ? ( diff --git a/frontend/pages/FeedPage.tsx b/frontend/pages/FeedPage.tsx index badf0b1c..a605fb4c 100644 --- a/frontend/pages/FeedPage.tsx +++ b/frontend/pages/FeedPage.tsx @@ -1,5 +1,6 @@ import { TextInput, + TouchableOpacity, StyleSheet, View, Text, @@ -12,6 +13,8 @@ import DiscoverPeople from '../components/Feed/DiscoverPeople'; import PostNew from '../components/Feed/PostNew'; import { getPosts } from '../services/users'; import { Post } from '../types/types'; +import { useDispatch } from 'react-redux'; +import { makePost } from '../reducers/onboarding/onboardingReducer'; const AddSvg = ` @@ -37,78 +40,85 @@ const FeedPage = () => { }); }, []); + const dispatch = useDispatch(); + + const startMakePost = () => { + dispatch(makePost()) + }; + return ( - - - - - - - { - firstPost && - - Featured Post - - - } - + + + + + + + { + firstPost && + + Featured Post + + + } + - ] - }, - { - data: [ - - - - ] - }, - { - data: [ - - { - posts.map((p) => ( - - )) - } - - ] - }, - { - data: [ - - ] - } - - ]} - keyExtractor={(_, index) => index.toString()} - renderItem={({ item }) => <>{item}} - renderSectionHeader={() => } - stickySectionHeadersEnabled={false} - /> - + ] + }, + { + data: [ + + + + ] + }, + { + data: [ + + { + posts.map((p) => ( + + )) + } + + ] + }, + { + data: [ + + ] + } + + ]} + keyExtractor={(_, index) => index.toString()} + renderItem={({ item }) => <>{item}} + renderSectionHeader={() => } + stickySectionHeadersEnabled={false} + /> + + + + - - - - - - + + + + ); }; diff --git a/frontend/pages/MakePost/PortfolioSummary/PortfolioSummary.tsx b/frontend/pages/MakePost/PortfolioSummary/PortfolioSummary.tsx new file mode 100644 index 00000000..dd00df88 --- /dev/null +++ b/frontend/pages/MakePost/PortfolioSummary/PortfolioSummary.tsx @@ -0,0 +1,256 @@ +import React from 'react'; +import { View, Text, Image, StyleSheet, TouchableOpacity } from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import { MakePostNavigationProp } from '../../../types/navigationTypes'; + +import Icon from 'react-native-vector-icons/MaterialIcons'; +import LargeColorText from '../UtilityTextAbstractions/LargeColorText'; + +import { createPortfolioPost } from '../../../services/post'; +import { useSelector } from 'react-redux'; +import { useSession } from '@clerk/clerk-expo'; +import { RootState } from '../../../components/LayoutWrapper'; +import { useDispatch } from 'react-redux'; +import { finishPost } from '../../../reducers/onboarding/onboardingReducer'; + +const PortfolioSummary: React.FC = () => { + const navigation = useNavigation(); + + const makePost = useSelector((state: RootState) => { + return state.makePost; + }); + + const dispatch = useDispatch(); + + const { session } = useSession(); + + const createPost = async () => { + await createPortfolioPost( + //"user_2chL8dX6HdbBAuvu3DDM9f9NzKK", + session?.user.id ?? '', + makePost.percentData, // TODO: Fetch using financial API + makePost.summaryType, // TODO: Fetch using financial API + ).then(() => { + dispatch(finishPost()); + }); + } + + return ( + + + + navigation.goBack()} /> + + Post + + + + + + + + OCT 14 + + + NOW + + + + + Performance + + + + Yield Cost Ratio + 4.6% + + + Balance + $1900.3 + + + + + + + ); +}; + +const styles = StyleSheet.create({ + background: { + backgroundColor: '#FFFFFF', + flex: 1, + }, + navigateBefore: { + fontSize: 25, + opacity: 0.25, + color: '#000000' + }, + actionContainer: { + position: 'absolute', + top: 82, + left: 23, + width: 346, + height: 37, + justifyContent: 'space-between', + flexDirection: 'row', + alignItems: 'center' + }, + postButton: { + width: 71, + height: 37, + padding: 10, + borderRadius: 5, + borderWidth: 1, + borderColor: '#00000014', + backgroundColor: '#000000', + justifyContent: 'center', + alignItems: 'center' + }, + postButtonText: { + fontSize: 14, + lineHeight: 16.94, + fontWeight: "500", + fontFamily: 'Inter', + color: '#FFFFFF', + }, + lineGraphContainer: { + position: 'absolute', + top: 190 + }, + timelineContainer: { + position: 'absolute', + top: 303, + left: 25, + width: 343, + height: 26, + gap: 226, + justifyContent: 'center', + flexDirection: 'row', + alignItems: 'center' + }, + timelineStartContainer: { + width: 65, + height: 26, + gap: 10, + borderRadius: 5, + paddingHorizontal: 5, + paddingVertical: 5, + justifyContent: 'center', + flexDirection: 'row', + alignItems: 'center' + }, + timelineStartText: { + fontSize: 13, + lineHeight: 15.51, + letterSpacing: -0.03, + fontWeight: "500", + fontFamily: 'SF Pro Text', + color: '#A5A5A5' + }, + timelineEndContainer: { + width: 65, + height: 26, + gap: 10, + borderRadius: 5, + paddingHorizontal: 5, + paddingVertical: 5, + justifyContent: 'center', + flexDirection: 'row', + alignItems: 'center' + }, + timelineEndText: { + fontSize: 13, + lineHeight: 15.51, + letterSpacing: -0.03, + fontWeight: "500", + fontFamily: 'SF Pro Text', + color: '#A5A5A5' + }, + statsContainer: { + position: 'absolute', + top: 388, + left: 24, + width: 318, + height: 52, + gap: 25, + flexDirection: 'row', + alignItems: 'center' + }, + buySellContainer: { + minWidth: 77, + height: 52, + gap: 5 + }, + buySellText: { + fontSize: 13, + lineHeight: 15.51, + letterSpacing: -0.03, + fontWeight: "500", + fontFamily: 'SF Pro Text', + color: '#A5A5A5' + }, + buySellAmountText: { + fontSize: 26, + lineHeight: 31.03, + letterSpacing: -0.01, + fontWeight: "500", + fontFamily: 'SF Pro Display', + color: '#666666' + }, + allTimeContainer: { + minWidth: 94, + height: 52, + gap: 5 + }, + allTimeAmountContainer: { + minWidth: 62.21, + height: 31, + flexDirection: 'row', + alignItems: 'center' + }, + allTimeText: { + fontSize: 13, + lineHeight: 15.51, + letterSpacing: -0.03, + fontWeight: "500", + fontFamily: 'SF Pro Text', + color: '#A5A5A5' + }, + allTimeDownArrow: { + fontSize: 26, + color: '#FF2B51' + }, + allTimeDownAmountText: { + fontSize: 26, + lineHeight: 31.03, + letterSpacing: -0.01, + fontWeight: "500", + fontFamily: 'SF Pro Display', + color: '#FF2B51', + }, + allTimeUpArrow: { + fontSize: 26, + color: '#02AD98' + }, + allTimeUpAmountText: { + fontSize: 26, + lineHeight: 31.03, + letterSpacing: -0.01, + fontWeight: "500", + fontFamily: 'SF Pro Display', + color: '#02AD98', + }, + balanceContainer: { + minWidth: 97, + height: 52, + gap: 5 + }, + pieChartContainer: { + position: 'absolute', + left: 24, + top: 485 + }, +}); + +export default PortfolioSummary; \ No newline at end of file diff --git a/frontend/pages/MakePost/PortfolioSummary/SharePortfolioSummary.tsx b/frontend/pages/MakePost/PortfolioSummary/SharePortfolioSummary.tsx new file mode 100644 index 00000000..ca8f8901 --- /dev/null +++ b/frontend/pages/MakePost/PortfolioSummary/SharePortfolioSummary.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { View, Text, StyleSheet } from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import { MakePostNavigationProp } from '../../../types/navigationTypes'; + +import Icon from 'react-native-vector-icons/MaterialIcons'; +import SummaryOption from './UtilitySummaryAbstraction/SummaryOption'; + +const SharePortfolioSummary: React.FC = () => { + const navigation = useNavigation(); + + return ( + + + navigation.goBack()} /> + + + Share Portfolio Summary + Choose the duration you want to share + + + + + + + + + + + + + + ); +}; + +const styles = StyleSheet.create({ + background: { + backgroundColor: '#FFFFFF', + flex: 1, + }, + navigateBefore: { + fontSize: 25, + opacity: 0.25, + position: 'absolute', + top: 83, + left: 24, + color: '#000000' + }, + contentContainer: { + position: 'absolute', + top: 158, + left: 24, + width: 327, + height: 71, + gap: 15, + justifyContent: 'center', + }, + title: { + fontSize: 22, + lineHeight: 26.25, + letterSpacing: -0.01, + fontWeight: "500", + fontFamily: 'SF Pro Display', + color: '#000000', + }, + description: { + fontSize: 16, + lineHeight: 19.09, + letterSpacing: -0.02, + fontWeight: "400", + fontFamily: 'SF Pro Text', + color: '#666666', + }, + buttonsContainer: { + position: 'absolute', + top: 279, + left: 24, + width: 346, + height: 330, + gap: 15 + }, +}); + +export default SharePortfolioSummary; diff --git a/frontend/pages/MakePost/PortfolioSummary/TradeGraphs/BadTrade.png b/frontend/pages/MakePost/PortfolioSummary/TradeGraphs/BadTrade.png new file mode 100644 index 00000000..511d8505 Binary files /dev/null and b/frontend/pages/MakePost/PortfolioSummary/TradeGraphs/BadTrade.png differ diff --git a/frontend/pages/MakePost/PortfolioSummary/TradeGraphs/GoodTrade.png b/frontend/pages/MakePost/PortfolioSummary/TradeGraphs/GoodTrade.png new file mode 100644 index 00000000..0a1eae51 Binary files /dev/null and b/frontend/pages/MakePost/PortfolioSummary/TradeGraphs/GoodTrade.png differ diff --git a/frontend/pages/MakePost/PortfolioSummary/TradeGraphs/PieChart.png b/frontend/pages/MakePost/PortfolioSummary/TradeGraphs/PieChart.png new file mode 100644 index 00000000..f4898977 Binary files /dev/null and b/frontend/pages/MakePost/PortfolioSummary/TradeGraphs/PieChart.png differ diff --git a/frontend/pages/MakePost/PortfolioSummary/UtilitySummaryAbstraction/SummaryOption.tsx b/frontend/pages/MakePost/PortfolioSummary/UtilitySummaryAbstraction/SummaryOption.tsx new file mode 100644 index 00000000..2b11e755 --- /dev/null +++ b/frontend/pages/MakePost/PortfolioSummary/UtilitySummaryAbstraction/SummaryOption.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import { MakePostNavigationProp } from '../../../../types/navigationTypes'; + +import SmallColorText from '../../UtilityTextAbstractions/SmallColorText'; + +import { useDispatch } from 'react-redux'; +import { updatePercentData, updateSummaryType } from '../../../../reducers/makePost/makePostReducer'; + +const SummaryOption = ({ summaryType, percent }: { summaryType: string, percent: number }) => { + const navigation = useNavigation(); + + const dispatch = useDispatch(); + + const summary = () => { + dispatch(updateSummaryType(summaryType)); + dispatch(updatePercentData(percent)); + navigation.navigate('PortfolioSummary'); + } + + return ( + + + {summaryType} + + + + ); +}; + +const styles = StyleSheet.create({ + buttonContentContainer: { + width: 260, // Increased to fit description + height: 41, + gap: 6, + flexDirection: 'row', + alignItems: 'center' + }, + buttonTitle: { + fontSize: 16, + lineHeight: 19.09, + letterSpacing: -0.03, + fontWeight: "500", + fontFamily: 'SF Pro Text', + color: '#333333', + }, + button: { + width: 346, + height: 100, + paddingVertical: 22, + paddingHorizontal: 19, + borderRadius: 10, + borderWidth: 1, + borderColor: '#00000008', + backgroundColor: '#FDFDFD', + justifyContent: 'space-between', + flexDirection: 'row', + alignItems: 'center' + }, +}); + +export default SummaryOption; \ No newline at end of file diff --git a/frontend/pages/MakePost/SharePost.tsx b/frontend/pages/MakePost/SharePost.tsx new file mode 100644 index 00000000..919c1189 --- /dev/null +++ b/frontend/pages/MakePost/SharePost.tsx @@ -0,0 +1,148 @@ +import React from 'react'; +import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import { MakePostNavigationProp } from '../../types/navigationTypes'; + +import Icon from 'react-native-vector-icons/MaterialIcons'; +import { useDispatch } from 'react-redux'; +import { finishPost } from '../../reducers/onboarding/onboardingReducer'; + +const SharePost: React.FC = () => { + const navigation = useNavigation(); + + const dispatch = useDispatch(); + + const cancelMakePost = () => { + dispatch(finishPost()); + }; + + return ( + + + + + + Share a Post + Let people on Carbon know what you have been up to! + + + + + { navigation.navigate('SelectTrade') }}> + + Trade Post + Share a recent trade you made + + + + + { navigation.navigate('SharePortfolioSummary') }}> + + Portfolio Summary + Share a summary of your portfolio + + + + + { navigation.navigate('TextBasedPost') }}> + + Text Based Post + Talk about what's on your mind + + + + + + + + ); +}; + +const styles = StyleSheet.create({ + background: { + backgroundColor: '#FFFFFF', + flex: 1, + }, + navigateBefore: { + fontSize: 25, + opacity: 0.25, + position: 'absolute', + top: 83, + left: 24, + color: '#000000' + }, + contentContainer: { + position: 'absolute', + top: 158, + left: 24, + width: 327, + height: 71, + gap: 15, + justifyContent: 'center', + }, + title: { + fontSize: 22, + lineHeight: 26.25, + letterSpacing: -0.01, + fontWeight: "500", + fontFamily: 'SF Pro Display', + color: '#000000', + }, + description: { + fontSize: 16, + lineHeight: 19.09, + letterSpacing: -0.02, + fontWeight: "400", + fontFamily: 'SF Pro Text', + color: '#666666', + }, + buttonsContainer: { + position: 'absolute', + top: 279, + left: 24, + width: 346, + height: 330, + gap: 15 + }, + buttonContentContainer: { + width: 260, // Increased to fit description + height: 41, + gap: 6, + }, + buttonTitle: { + fontSize: 16, + lineHeight: 19.09, + letterSpacing: -0.03, + fontWeight: "500", + fontFamily: 'SF Pro Text', + color: '#333333', + }, + buttonDescription: { + fontSize: 13, + lineHeight: 15.51, + letterSpacing: -0.03, + fontWeight: "500", + fontFamily: 'SF Pro Text', + color: '#666666', + }, + button: { + width: 346, + height: 100, + paddingVertical: 22, + paddingHorizontal: 19, + borderRadius: 10, + borderWidth: 1, + borderColor: '#00000014', + backgroundColor: '#FDFDFD', + justifyContent: 'space-between', + flexDirection: 'row', + alignItems: 'center' + }, + navigateNext: { + fontSize: 25, + opacity: 0.25, + color: '#000000' + } +}); + +export default SharePost; diff --git a/frontend/pages/MakePost/TextBasedPost/TextBasedPost.tsx b/frontend/pages/MakePost/TextBasedPost/TextBasedPost.tsx new file mode 100644 index 00000000..a566d093 --- /dev/null +++ b/frontend/pages/MakePost/TextBasedPost/TextBasedPost.tsx @@ -0,0 +1,132 @@ +import React from 'react'; +import { View, Text, TextInput, StyleSheet, TouchableOpacity } from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import { MakePostNavigationProp } from '../../../types/navigationTypes'; + +import Icon from 'react-native-vector-icons/MaterialIcons'; + +import { createTextPost } from '../../../services/post'; +import { useSession } from '@clerk/clerk-expo'; +import { useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { updateTitle, updateDescription } from '../../../reducers/makePost/makePostReducer'; +import { finishPost } from '../../../reducers/onboarding/onboardingReducer'; + +const TextBasedPost: React.FC = () => { + const navigation = useNavigation(); + + const { session } = useSession(); + + const dispatch = useDispatch(); + + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + + const createPost = async () => { + dispatch(updateTitle(title)); + dispatch(updateDescription(description)); + + await createTextPost( + //"user_2chL8dX6HdbBAuvu3DDM9f9NzKK", + session?.user.id ?? '', + title, + description, + ).then(() => { + dispatch(finishPost()); + }); + } + + return ( + + + + navigation.goBack()} /> + + Post + + + + + + + + + + ); +}; + +const styles = StyleSheet.create({ + background: { + backgroundColor: '#FFFFFF', + flex: 1, + }, + navigateBefore: { + fontSize: 25, + opacity: 0.25, + color: '#000000' + }, + actionContainer: { + position: 'absolute', + top: 82, + left: 23, + width: 346, + height: 37, + justifyContent: 'space-between', + flexDirection: 'row', + alignItems: 'center' + }, + postButton: { + width: 71, + height: 37, + padding: 10, + borderRadius: 5, + borderWidth: 1, + borderColor: '#00000014', + backgroundColor: '#000000', + justifyContent: 'center', + alignItems: 'center' + }, + postButtonText: { + fontSize: 14, + lineHeight: 16.94, + fontWeight: "500", + fontFamily: 'Inter', + color: '#FFFFFF', + }, + descriptionContainer: { + position: 'absolute', + top: 169, + left: 25, + width: 327, + height: 62, + gap: 25 + }, + descriptionTitleText: { + fontSize: 22, + lineHeight: 26.25, + letterSpacing: -0.01, + fontWeight: "500", + fontFamily: 'SF Pro Display', + color: '#333333' + }, + descriptionText: { + fontSize: 16, + lineHeight: 19.09, + letterSpacing: -0.02, + fontWeight: "400", + fontFamily: 'SF Pro Text', + color: '#666666' + } +}); + +export default TextBasedPost; \ No newline at end of file diff --git a/frontend/pages/MakePost/TradePost/SelectTrade.tsx b/frontend/pages/MakePost/TradePost/SelectTrade.tsx new file mode 100644 index 00000000..a6afac78 --- /dev/null +++ b/frontend/pages/MakePost/TradePost/SelectTrade.tsx @@ -0,0 +1,152 @@ +import React from 'react'; +import { View, Text, StyleSheet } from 'react-native'; +import Icon from 'react-native-vector-icons/MaterialIcons'; +import { useNavigation } from '@react-navigation/native'; +import { MakePostNavigationProp } from '../../../types/navigationTypes'; +import ColorTrade from './UtilityTradeAbstraction/ColorTrade'; + +const SelectTrade: React.FC = () => { + const navigation = useNavigation(); + + return ( + + + navigation.goBack()} /> + + + Select a Trade + Choose a recent trade you want to share + + + + + + + + + + + + + + + + + + ); +}; + +const styles = StyleSheet.create({ + background: { + backgroundColor: '#FFFFFF', + flex: 1, + }, + navigateBefore: { + fontSize: 25, + opacity: 0.25, + position: 'absolute', + top: 83, + left: 24, + color: '#000000' + }, + contentContainer: { + position: 'absolute', + top: 158, + left: 24, + width: 327, + height: 52, + gap: 15, + justifyContent: 'center', + }, + title: { + fontSize: 22, + lineHeight: 26.25, + letterSpacing: -0.01, + fontWeight: "500", + fontFamily: 'SF Pro Display', + color: '#000000', + }, + description: { + fontSize: 16, + lineHeight: 19.09, + letterSpacing: -0.02, + fontWeight: "400", + fontFamily: 'SF Pro Text', + color: '#666666', + }, + buttonsContainer: { + position: 'absolute', + top: 260, + left: 24, + width: 346, + height: 435, + gap: 15 + }, + buttonContentContainer: { + minWidth: 150, + height: 17, + gap: 3, // Added to create space + flexDirection: 'row', + alignItems: 'center' + }, + buyDescription: { + fontSize: 16, + lineHeight: 19.09, + letterSpacing: -0.02, + fontWeight: "500", + fontFamily: 'SF Pro Text', + color: '#FF2B51', + }, + sellDescription: { + fontSize: 16, + lineHeight: 19.09, + letterSpacing: -0.02, + fontWeight: "500", + fontFamily: 'SF Pro Text', + color: '#02AD98', + }, + symbolDescription: { + fontSize: 16, + lineHeight: 19.09, + letterSpacing: -0.02, + fontWeight: "700", + fontFamily: 'SF Pro Text', + color: '#121212', + }, + priceDescription: { + fontSize: 16, + lineHeight: 19.09, + letterSpacing: -0.02, + fontWeight: "500", + fontFamily: 'SF Pro Text', + color: '#121212', + }, + buy: { + width: 346, + height: 75, + paddingVertical: 22, + paddingHorizontal: 20, + borderRadius: 5, + borderWidth: 1, + borderColor: '#FF385C0F', + backgroundColor: '#FF2B511A', + justifyContent: 'space-between', + flexDirection: 'row', + alignItems: 'center' + }, + sell: { + width: 346, + height: 75, + paddingVertical: 22, + paddingHorizontal: 20, + borderRadius: 5, + borderWidth: 1, + borderColor: '#FF385C0F', + backgroundColor: '#02AD9814', + justifyContent: 'space-between', + flexDirection: 'row', + alignItems: 'center' + } +}); + +export default SelectTrade; \ No newline at end of file diff --git a/frontend/pages/MakePost/TradePost/TradePostDetails.tsx b/frontend/pages/MakePost/TradePost/TradePostDetails.tsx new file mode 100644 index 00000000..ec56dd18 --- /dev/null +++ b/frontend/pages/MakePost/TradePost/TradePostDetails.tsx @@ -0,0 +1,231 @@ +import React from 'react'; +import { View, Text, TextInput, StyleSheet, TouchableOpacity } from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import { MakePostNavigationProp } from '../../../types/navigationTypes'; + +import Icon from 'react-native-vector-icons/MaterialIcons'; +import LargeColorText from '../UtilityTextAbstractions/LargeColorText'; + +import { useSelector } from 'react-redux'; +import { createTradePost } from '../../../services/post'; +import { useSession } from '@clerk/clerk-expo'; +import { RootState } from '../../../components/LayoutWrapper'; +import { useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { updateTitle, updateDescription } from '../../../reducers/makePost/makePostReducer'; +import { finishPost } from '../../../reducers/onboarding/onboardingReducer'; + +const TradePostDetails: React.FC = () => { + const navigation = useNavigation(); + + const makePost = useSelector((state: RootState) => { + return state.makePost; + }); + + const { session } = useSession(); + + const dispatch = useDispatch(); + + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + + const createPost = async () => { + dispatch(updateTitle(title)); + dispatch(updateDescription(description)); + + await createTradePost( + //"user_2chL8dX6HdbBAuvu3DDM9f9NzKK", + session?.user.id ?? '', + makePost.percentData, // TODO: Fetch using financial API + makePost.tickerSymbol, // TODO: Fetch using financial API + title, + description, + ).then(() => { + dispatch(finishPost()); + }); + } + + return ( + + + + navigation.goBack()} /> + + Post + + + + + + + Sell + $170.55 + + + All Time + + + + + + + + + + + + ); +}; + +const styles = StyleSheet.create({ + background: { + backgroundColor: '#FFFFFF', + flex: 1, + }, + navigateBefore: { + fontSize: 25, + opacity: 0.25, + color: '#000000' + }, + actionContainer: { + position: 'absolute', + top: 82, + left: 23, + width: 346, + height: 37, + justifyContent: 'space-between', + flexDirection: 'row', + alignItems: 'center' + }, + postButton: { + width: 71, + height: 37, + padding: 10, + borderRadius: 5, + borderWidth: 1, + borderColor: '#00000014', + backgroundColor: '#000000', + justifyContent: 'center', + alignItems: 'center' + }, + postButtonText: { + fontSize: 14, + lineHeight: 16.94, + fontWeight: "500", + fontFamily: 'Inter', + color: '#FFFFFF', + }, + detailsContainer: { + position: 'absolute', + top: 328, + left: 24, + width: 327, + height: 164, + gap: 50, + justifyContent: 'center', + }, + statsContainer: { + width: 181.21, + height: 52, + gap: 25, + flexDirection: 'row', + alignItems: 'center' + }, + buySellContainer: { + minWidth: 94, + height: 52, + gap: 5 + }, + buySellText: { + fontSize: 13, + lineHeight: 15.51, + letterSpacing: -0.03, + fontWeight: "500", + fontFamily: 'SF Pro Text', + color: '#A5A5A5' + }, + buySellAmountText: { + fontSize: 26, + lineHeight: 31.03, + letterSpacing: -0.01, + fontWeight: "500", + fontFamily: 'SF Pro Display', + color: '#666666' + }, + allTimeContainer: { + width: 62.21, + height: 52, + gap: 5 + }, + allTimeAmountContainer: { + minWidth: 62.21, + height: 31, + flexDirection: 'row', + alignItems: 'center' + }, + allTimeText: { + fontSize: 13, + lineHeight: 15.51, + letterSpacing: -0.03, + fontWeight: "500", + fontFamily: 'SF Pro Text', + color: '#A5A5A5' + }, + allTimeDownArrow: { + fontSize: 26, + color: '#FF2B51' + }, + allTimeDownAmountText: { + fontSize: 26, + lineHeight: 31.03, + letterSpacing: -0.01, + fontWeight: "500", + fontFamily: 'SF Pro Display', + color: '#FF2B51', + }, + allTimeUpArrow: { + fontSize: 26, + color: '#02AD98' + }, + allTimeUpAmountText: { + fontSize: 26, + lineHeight: 31.03, + letterSpacing: -0.01, + fontWeight: "500", + fontFamily: 'SF Pro Display', + color: '#02AD98', + }, + descriptionContainer: { + width: 327, + height: 62, + gap: 25 + }, + descriptionTitleText: { + fontSize: 22, + lineHeight: 26.25, + letterSpacing: -0.01, + fontWeight: "500", + fontFamily: 'SF Pro Display', + color: '#333333' + }, + descriptionText: { + fontSize: 16, + lineHeight: 19.09, + letterSpacing: -0.02, + fontWeight: "400", + fontFamily: 'SF Pro Text', + color: '#666666' + } +}); + +export default TradePostDetails; \ No newline at end of file diff --git a/frontend/pages/MakePost/TradePost/UtilityTradeAbstraction/ColorTrade.tsx b/frontend/pages/MakePost/TradePost/UtilityTradeAbstraction/ColorTrade.tsx new file mode 100644 index 00000000..e0aa4ec2 --- /dev/null +++ b/frontend/pages/MakePost/TradePost/UtilityTradeAbstraction/ColorTrade.tsx @@ -0,0 +1,136 @@ +import React from 'react'; +import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import { MakePostNavigationProp } from '../../../../types/navigationTypes'; + +import SmallColorText from '../../UtilityTextAbstractions/SmallColorText'; + +import { useDispatch } from 'react-redux'; +import { updatePercentData, updateTickerSymbol } from '../../../../reducers/makePost/makePostReducer'; + +const ColorTrade = ({ action, ticker, amount, percent }: { action: string, ticker: string, amount: number, percent: number }) => { + const navigation = useNavigation(); + + const dispatch = useDispatch(); + + const nextStep = () => { + dispatch(updateTickerSymbol(ticker)); + dispatch(updatePercentData(percent)); + + navigation.navigate('TradePostDetails') + } + + return ( + + + {action} + {ticker} + @ {amount} + + + + ); +}; + +const styles = StyleSheet.create({ + contentContainer: { + position: 'absolute', + top: 158, + left: 24, + width: 327, + height: 52, + gap: 15, + justifyContent: 'center', + }, + title: { + fontSize: 22, + lineHeight: 26.25, + letterSpacing: -0.01, + fontWeight: "500", + fontFamily: 'SF Pro Display', + color: '#000000', + }, + description: { + fontSize: 16, + lineHeight: 19.09, + letterSpacing: -0.02, + fontWeight: "400", + fontFamily: 'SF Pro Text', + color: '#666666', + }, + buttonsContainer: { + position: 'absolute', + top: 260, + left: 24, + width: 346, + height: 435, + gap: 15 + }, + buttonContentContainer: { + minWidth: 150, + height: 17, + gap: 3, // Added to create space + flexDirection: 'row', + alignItems: 'center' + }, + buyDescription: { + fontSize: 16, + lineHeight: 19.09, + letterSpacing: -0.02, + fontWeight: "500", + fontFamily: 'SF Pro Text', + color: '#FF2B51', + }, + sellDescription: { + fontSize: 16, + lineHeight: 19.09, + letterSpacing: -0.02, + fontWeight: "500", + fontFamily: 'SF Pro Text', + color: '#02AD98', + }, + symbolDescription: { + fontSize: 16, + lineHeight: 19.09, + letterSpacing: -0.02, + fontWeight: "700", + fontFamily: 'SF Pro Text', + color: '#121212', + }, + priceDescription: { + fontSize: 16, + lineHeight: 19.09, + letterSpacing: -0.02, + fontWeight: "500", + fontFamily: 'SF Pro Text', + color: '#121212', + }, + buy: { + width: 346, + height: 75, + paddingVertical: 22, + paddingHorizontal: 20, + borderRadius: 5, + borderWidth: 1, + borderColor: '#FF385C0F', + backgroundColor: '#FF2B511A', + justifyContent: 'space-between', + flexDirection: 'row', + alignItems: 'center' + }, + sell: { + width: 346, + height: 75, + paddingVertical: 22, + paddingHorizontal: 20, + borderRadius: 5, + borderWidth: 1, + borderColor: '#FF385C0F', + backgroundColor: '#02AD9814', + justifyContent: 'space-between', + flexDirection: 'row', + alignItems: 'center' + } +}); + +export default ColorTrade; \ No newline at end of file diff --git a/frontend/pages/MakePost/UtilityTextAbstractions/LargeColorText.tsx b/frontend/pages/MakePost/UtilityTextAbstractions/LargeColorText.tsx new file mode 100644 index 00000000..164a71b2 --- /dev/null +++ b/frontend/pages/MakePost/UtilityTextAbstractions/LargeColorText.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { View, Text, StyleSheet } from 'react-native'; + +import Icon from 'react-native-vector-icons/MaterialIcons'; + +const LargeColorText = ({ amount }: { amount: number }) => { + return ( + + + {amount < 0 ? -amount : amount}% + + ); +}; + +const styles = StyleSheet.create({ + allTimeAmountContainer: { + minWidth: 62.21, + height: 31, + flexDirection: 'row', + alignItems: 'center' + }, + allTimeDownArrow: { + fontSize: 26, + color: '#FF2B51' + }, + allTimeDownAmountText: { + fontSize: 26, + lineHeight: 31.03, + letterSpacing: -0.01, + fontWeight: "500", + fontFamily: 'SF Pro Display', + color: '#FF2B51', + }, + allTimeUpArrow: { + fontSize: 26, + color: '#02AD98' + }, + allTimeUpAmountText: { + fontSize: 26, + lineHeight: 31.03, + letterSpacing: -0.01, + fontWeight: "500", + fontFamily: 'SF Pro Display', + color: '#02AD98', + }, +}); + +export default LargeColorText; \ No newline at end of file diff --git a/frontend/pages/MakePost/UtilityTextAbstractions/SmallColorText.tsx b/frontend/pages/MakePost/UtilityTextAbstractions/SmallColorText.tsx new file mode 100644 index 00000000..8e484f1b --- /dev/null +++ b/frontend/pages/MakePost/UtilityTextAbstractions/SmallColorText.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { View, Text, StyleSheet } from 'react-native'; + +import Icon from 'react-native-vector-icons/MaterialIcons'; + +const SmallColorText = ({ amount }: { amount: number }) => { + return ( + + + {amount < 0 ? -amount : amount}% + + ); +}; + +const styles = StyleSheet.create({ + percentContentContainer: { + width: 51.14, + height: 24, + flexDirection: 'row', + alignItems: 'center' + }, + buyDownArrow: { + fontSize: 17, + color: '#FF2B51' + }, + buyDownPercent: { + fontSize: 17, + lineHeight: 24, + fontWeight: "500", + fontFamily: 'SF Pro', + color: '#FF2B51', + }, + sellUpArrow: { + fontSize: 17, + color: '#02AD98' + }, + sellUpPercent: { + fontSize: 17, + lineHeight: 24, + fontWeight: "500", + fontFamily: 'SF Pro', + color: '#02AD98', + } +}); + +export default SmallColorText; diff --git a/frontend/reducers/makePost/makePostReducer.ts b/frontend/reducers/makePost/makePostReducer.ts new file mode 100644 index 00000000..9a54c815 --- /dev/null +++ b/frontend/reducers/makePost/makePostReducer.ts @@ -0,0 +1,38 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const makePostSlice = createSlice({ + name: 'makePost', + initialState: { + percentData: 0, + tickerSymbol: '', + summaryType: '', // One Month, Six Month, One Year + title: '', + description: '', + }, + reducers: { + updatePercentData(state, action) { + state.percentData = action.payload; + }, + updateTickerSymbol(state, action) { + state.tickerSymbol = action.payload; + }, + updateSummaryType(state, action) { + state.summaryType = action.payload; + }, + updateTitle(state, action) { + state.title = action.payload; + }, + updateDescription(state, action) { + state.description = action.payload; + } + }, +}); + +export const { + updatePercentData, + updateTickerSymbol, + updateSummaryType, + updateTitle, + updateDescription +} = makePostSlice.actions; +export default makePostSlice.reducer; \ No newline at end of file diff --git a/frontend/router/MakePostNavigation.tsx b/frontend/router/MakePostNavigation.tsx new file mode 100644 index 00000000..1e3a321e --- /dev/null +++ b/frontend/router/MakePostNavigation.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import { createStackNavigator } from '@react-navigation/stack'; + +import SharePost from '../pages/MakePost/SharePost'; + +// Trade Post +import SelectTrade from '../pages/MakePost/TradePost/SelectTrade'; +import TradePostDetails from '../pages/MakePost/TradePost/TradePostDetails'; + +// Text Based Post +import TextBasedPost from '../pages/MakePost/TextBasedPost/TextBasedPost'; + +// Portfolio Summary +import SharePortfolioSummary from '../pages/MakePost/PortfolioSummary/SharePortfolioSummary'; +import PortfolioSummary from '../pages/MakePost/PortfolioSummary/PortfolioSummary'; + +const Stack = createStackNavigator(); + +const MakePostNavigator = () => { + return ( + + + + {/* Trade Post */} + + + + {/* Text Based Post */} + + + {/* Portfolio Summary */} + + + + ); +}; + +export default MakePostNavigator; \ No newline at end of file diff --git a/frontend/services/post.ts b/frontend/services/post.ts new file mode 100644 index 00000000..bcf21168 --- /dev/null +++ b/frontend/services/post.ts @@ -0,0 +1,44 @@ +import { Redirect } from '../types/types'; +import axios, { AxiosResponse } from 'axios'; +import { API_LINK } from './CommonDocs'; + +export const createTradePost = async (userId: string, percentData: number, tickerSymbol: string, title: string, description: string): Promise => { + const payload = { + percent_data: percentData, + ticker_symbol: tickerSymbol, + title: title, + description: description + } + + const response: AxiosResponse = await axios.post( + `http://${API_LINK}/posts/create-trade-post/${userId}`, + payload + ); + return response.data; +}; + +export const createPortfolioPost = async (userId: string, percentData: number, summaryType: string): Promise => { + const payload = { + percent_data: percentData, + summary_type: summaryType + } + + const response: AxiosResponse = await axios.post( + `http://${API_LINK}/posts/create-portfolio-post/${userId}`, + payload + ); + return response.data; +}; + +export const createTextPost = async (userId: string, title: string, description: string): Promise => { + const payload = { + title: title, + description: description + } + + const response: AxiosResponse = await axios.post( + `http://${API_LINK}/posts/create-text-post/${userId}`, + payload + ); + return response.data; +} \ No newline at end of file diff --git a/frontend/types/navigationTypes.ts b/frontend/types/navigationTypes.ts index 8654760d..0f570e21 100644 --- a/frontend/types/navigationTypes.ts +++ b/frontend/types/navigationTypes.ts @@ -28,8 +28,23 @@ export type ProfileOtherStack = { Profile: undefined | { screen: "FollowerProfile"; params: { user: User } }; }; + +export type MakePostParamList = { + SharePost: undefined; + SelectTrade: undefined; + TradePostDetails: undefined; + TextBasedPost: undefined; + SharePortfolioSummary: undefined; + PortfolioSummary: undefined; +}; + +export type MakePostNavigationProp = StackNavigationProp< + MakePostParamList +>; + export type OutsideProfileNavProp = StackNavigationProp; + export type AuthNavigationProp = StackNavigationProp< RootStackParamList >; @@ -64,4 +79,4 @@ export type LevelPageNavigationProp = StackNavigationProp< // 'AuthPage' // >; -export type SplashScreenRouteProp = RouteProp; +export type SplashScreenRouteProp = RouteProp; \ No newline at end of file