From 2835bab753f837817d9bc4e87c9184c93c276337 Mon Sep 17 00:00:00 2001 From: halil Date: Tue, 3 Dec 2024 15:19:56 +0300 Subject: [PATCH 01/18] init: lab 8 --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index acb6f561..128805ac 100644 --- a/README.md +++ b/README.md @@ -36,3 +36,4 @@ The application is deployed on Digital Ocean. To access application frontend sim Similarly to view deployed api - API : 'http://159.223.28.163:30002/docs' + From 8476c28ee4c63167a23e89af9c1e3ba0b2ec951b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Muhammed=20Erkam=20G=C3=B6kcep=C4=B1nar?= Date: Tue, 3 Dec 2024 19:56:08 +0300 Subject: [PATCH 02/18] feat: add lab 8 acceptance criteria checklist --- docs/Lab 8 Criteria Checklist.md | 282 +++++++++++++++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 docs/Lab 8 Criteria Checklist.md diff --git a/docs/Lab 8 Criteria Checklist.md b/docs/Lab 8 Criteria Checklist.md new file mode 100644 index 00000000..f9e0f341 --- /dev/null +++ b/docs/Lab 8 Criteria Checklist.md @@ -0,0 +1,282 @@ +--- +title: Lab 8 Criteria Checklist + +--- + +# Criteria Checklist +1. **Primary features** +* Semantic Search + * Acceptance Criterias + - [ ] User should be able to search posts using their titles, contents and tags. + +* User Management + * Acceptance Criterias + - [x] User should be able to login with their credentials. + - [x] User should be able to log out. + - [x] User should be able to register with a unique email, username and secure password. + - [ ] User should be able to follow/unfollow other users. + - [ ] User should be able to manage his/her profile information. + - [ ] User should be able to view profile pages of other users. + - [ ] User should be able to view his/her followers. +* Post Creation and Interactions + * Acceptance Criterias + - [x] Post creation with necessary components like title, content and additive information like tags, portfolios and graphics should be implemented. + - [ ] Posts should be editable. + - [ ] Post like/unlike and comment features should be available. + - [x] Posts should be visible to other users in the community page. + +* Filtering + * Acceptance Criterias + - [ ] Filtering that filters news according to their sources should be available. + - [ ] User should be able to filter posts using the information of authors and if a graphic, portfolio and news is attached to post. + + + + +--- + +2. Domain-specific features (e.g. why they're specific, how they're implemented), + +* **Financial and Cryptocurrency news with RSS feeds** + * Acceptance Criterias: + - [x] News should be successfully fetched + - [x] News should link to mentioned news on the page + - [ ] News should not require a subscription on the page to read + - [x] News should be up to date +* **Portfolio Tracking** + * Acceptance Criterias: + - [ ] Portfolios should be created with existing stocks only + - [ ] Portfolios should show current price + - [x] Portfolios should show profit/loss using a pie chart + - [x] Stocks can be deleted and added to portfolios then portfolio should be updated + - [x] Portfolios should show number of stocks and current value +* **Up-to-date Stock Details with graphs** + * Acceptance Criterias: + - [x] Stock prices should be up to date, no late than 16 minutes old. + - [ ] Stock details should support all Turkish and US stock markets' stocks + - [ ] Stock details should have company description, website, phone number, and sector + - [ ] Stock details should include line or bar chart +* **Posts with portfolios, hot news, and graphs** + * Acceptance Criterias: + - [ ] Portfolios of post owner should be embeddable within posts + - [ ] News from RSS feeds should be embeddable within posts + - [ ] Graphs of stocks should be embeddable within posts + - [ ] Combinations of portfolio, news and graphs should be embeddable within posts + + + +--- + + +3. **API and its documentation** + +#### General Criteria for API + +1. **Consistency**: + - [ ] API naming conventions and structures (e.g., endpoints, request/response bodies) must follow RESTful principles. + - [ ] Use standard HTTP methods: GET, POST, PUT, PATCH, DELETE. + +2. **Authentication and Authorization**: + - [ ] Secure endpoints requiring authentication with tokens (e.g., access and refresh tokens). + - [ ] Prevent unauthorized access by validating user credentials and tokens. + +3. **Validation**: + - [ ] All request bodies must be validated for required fields, types, and formats before processing. + - [ ] Invalid requests should return descriptive error messages with appropriate status codes. + +4. **Error Handling**: + - [ ] API should return meaningful error codes (e.g., 400 for bad requests, 401 for unauthorized, 404 for not found). + - [ ] Include detailed error messages for debugging purposes. + +5. **Pagination**: + - [ ] Endpoints returning multiple resources must support pagination with `count`, `next`, and `previous` metadata. + +6. **Rate Limiting**: + - [ ] Implement rate limiting to avoid abuse or overloading the API. + +7. **Documentation**: + - [ ] Each endpoint should include: + - Purpose and description. + - Supported HTTP methods. + - Required headers, query parameters, and body examples. + - Sample responses (both success and error). + + +#### Specific Criteria for API Endpoints + +1. **Register** + - [ ] Users must provide valid `username`, `password`, and `email`. + - [ ] Passwords must follow a secure format (e.g., minimum length, mixed characters). + - [ ] Email verification step must be enforced before the user can log in. + - [ ] Returns `201 Created` on success with a message confirming registration. + +2. **Login** + - [ ] Accepts `username` and `password`. + - [ ] Returns access and refresh tokens upon success. + - [ ] Invalid credentials should return `401 Unauthorized` with a descriptive error message. + +3. **Login Refresh** + - [ ] Accepts a valid refresh token. + - [ ] Returns new access and refresh tokens upon success. + - [ ] Expired or invalid tokens return `403 Forbidden`. + +4. **Logout** + - [ ] Revokes the refresh token provided in the request body. + - [ ] Returns `204 No Content` on success. + +5. **Profiles** + - [ ] `GET All`: Returns paginated profile data. + - [ ] `POST`: Requires `profile_picture`, `bio`, `location`, and empty follower/following arrays on creation. + - [ ] `PUT`/`PATCH`: Requires a valid profile ID and allows full or partial updates. + - [ ] `DELETE`: Deletes a profile by ID and returns `204 No Content` on success. + +6. **Posts** + - [ ] `GET All`: Returns paginated post data with metadata. + - [ ] `POST`: Requires valid `title`, `content`, `author`, and optional arrays (`liked_by`, `tags`, `portfolios`). + - [ ] `PUT`/`PATCH`: Updates a post with a valid ID. + - [ ] `DELETE`: Deletes a post by ID. + +7. **Comments** + - [ ] Follows similar criteria as `Posts` for CRUD operations. + - [ ] Requires `post_id`, `user_id`, and `content`. + +8. **Currencies** + - [ ] Enforce valid `name` and `code` formats for creation. + - [ ] Return all currencies with pagination support. + +9. **News** + - [ ] Requires a valid `feed_name` to fetch news. + - [ ] Returns an array of news objects with metadata (`title`, `published`, `description`, `image`). + +0. **Tags** + - [ ] Requires `name` and `user_id` for creation. + - [ ] CRUD operations adhere to general standards. + +1. **Token** + - [ ] Token creation requires `username` and `password`. + - [ ] Token refresh should verify the existing token's validity. + +2. **Stocks** + - [ ] Automatically generate stocks for creation. + - [ ] Return up-to-date `price` information for stock retrieval. + + +#### Examples of Well-Documented API + +1. **Register Endpoint Example**: + ``` + POST /api/register + Content-Type: application/json + Body: + { + "username": "roketatar", + "password": "roket123", + "email": "borsakaplani@hotmail.com" + } + Response: + Status: 201 Created + { + "message": "Registration successful. Please verify your email." + } + ``` + +2. **Error Response Example**: + ``` + POST /api/login + Content-Type: application/json + Body: + { + "username": "wronguser", + "password": "wrongpass" + } + Response: + Status: 401 Unauthorized + { + "error": "Invalid credentials. Please check your username and password." + } + ``` + + +--- +4. **Standard being followed** +Web Annotation Data Model will be used. + 1. Document Web AnnotationData Model + - [x] Documentation can be found [here](https://github.com/bounswe/bounswe2024group2/wiki/W3C%E2%80%90Annotation/9135d3225cd7f56f968dc719c12da384101d78a0). + 2. Implementation + * Backend + - [ ] Another django project should be implemented for annotations. + - [ ] Within the annotation project, annotation REST endpoints should be implemented. + * Frontend + - [ ] Annotation creation feature should be implemented. When a user highlights a part from text body of a post, annotate feature, which calls create annotation from backend, should be visible. + - [ ] When annotation is selected by user, a pop-up screen should appear where users can add their comments as text. + - [ ] When annotation is created, create annotate endpoint from backend should be called. + - [ ] Annotations created should be visible as a pop-up when a user hovers over an annotated field. + * Mobile + - [ ] Annotation creation feature should be implemented. When a user highlights a part from text body of a post, annotate feature, which calls create annotation from backend, should be visible. + - [ ] When annotation is selected by user, a pop-up screen should appear where users can add their comments as text. + - [ ] When annotation is created, create annotate endpoint from backend should be called. + - [ ] Annotations created should be visible as a pop-up when a user hovers over an annotated field. + +--- +5. **Testing strategies** + + 5.1 Backend Testing + + - [ ] Write unit tests for individual components or functions. + - Tests cover serializers, models, or individual methods in views. + - Tests are isolated from external dependencies, ensuring internal logic correctness. + - Unit tests cover at least 90% of the lines for each individual component. + + - [ ] Write integration tests to verify interactions between components. + - Tests ensure correct interactions between views, serializers, and external services. + - End-to-end functionality for critical features is validated. + + + 5.2. Frontend Testing + - [ ] Define critical UI components and their expected behavior. + - List of components finalized. + - Documentation includes expected behavior for each component. + + - [ ] Implement unit tests for React components. + - All key components have unit test coverage >90%. + - Tests validate rendering, props handling, and state updates. + + - [ ] Conduct integration tests for user workflows. + - Test cases cover login, profile, and portfolio features. + - Navigation flows are verified for correctness. + + + 5.3. Mobile Testing + - [ ] Identify core features for React Native testing. + - Features like login, profile navigation, and form submissions are documented. + + - [ ] Develop unit tests for mobile components. + - Key components render correctly for different states and props. + + - [ ] Perform integration and E2E tests using Detox. + - Navigation flows are fully tested and validated across screens. + + + 5.4. Mock Data Strategy + - [ ] Set up Faker.js for realistic data generation. + - Mock data mimics real-world constraints and relationships. + + + 5.5. Reporting + - [ ] Generate unit test reports for frontend and backend. + - Jest generates HTML and text-summary reports. + - Reports uploaded to repository wiki. + + - [ ] Integrate test reports into CI pipeline. + - GitHub Actions automatically runs tests and reports coverage. + + 5.6. Documentation + + - [ ] Document the testing strategy plan in the repository wiki. + - Testing strategy is clearly outlined, including the purpose and scope of each type of testing (unit, integration, E2E). + - Step-by-step guide on how to run tests for backend, frontend, and mobile applications is included. + - Examples of test cases for each layer are provided (e.g., sample unit tests for serializers, integration tests for news endpoints). + - Mock data strategy with Faker.js and MSW is explained. + - Instructions on generating and interpreting test reports are documented. + + From 18fef269c74daa25abc2d34d12d717e0d7d4c134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Muhammed=20Erkam=20G=C3=B6kcep=C4=B1nar?= Date: Sun, 8 Dec 2024 23:02:05 +0300 Subject: [PATCH 03/18] fix: create config file --- mobile/src/pages/Community.js | 45 ++++------ mobile/src/pages/CreatePost.js | 23 +++-- mobile/src/pages/Home.js | 10 +-- mobile/src/pages/News.js | 5 +- mobile/src/pages/Post.js | 142 ++++++++++++++---------------- mobile/src/pages/Register.js | 6 +- mobile/src/pages/config/config.js | 3 + 7 files changed, 106 insertions(+), 128 deletions(-) create mode 100644 mobile/src/pages/config/config.js diff --git a/mobile/src/pages/Community.js b/mobile/src/pages/Community.js index c879ecfd..274b300f 100644 --- a/mobile/src/pages/Community.js +++ b/mobile/src/pages/Community.js @@ -1,39 +1,17 @@ import React from 'react'; -import { View, Text, FlatList, ScrollView, StyleSheet, TextInput, TouchableOpacity, Image } from 'react-native'; +import { View, Text, FlatList, ScrollView, StyleSheet, TextInput, TouchableOpacity, Image, Alert } from 'react-native'; import { useState, useEffect } from 'react'; import { useFocusEffect } from '@react-navigation/native'; +import config from './config/config'; +import { useAuth } from './context/AuthContext'; + const Community = ({navigation}) => { - /* - const posts = [ - { - id: 1, - title: 'Lilium\'un yan kuruluşları iflas tehlikesiyle karşı karşıya, "pennystock" olur mu', - author: 'Hüsnü Çoban', - date: '10/12/2024', - tags: ['Lilium', 'Hisse Analizi', 'Amerika'], - content: `Alman hava taksisi geliştiricisi Lilium, bugün iki yan kuruluşunun iflas başvurusunda bulunacağını duyurdu. Bu haber, şirketin ABD borsasında işlem gören hisselerinde sert düşüşe neden oldu ve hisseler %57 değer kaybetti.`, - graph: null, // No graph for this post - likes: 45, - comments: 12 - }, - { - id: 2, - title: 'Borsa İstanbul’da kazandıran hisse senetleri', - author: 'Ahmet Atak', - date: '10/12/2024', - tags: ['BIST', 'Yatırım', 'Hisse Senedi'], - content: `BIST 100, pozitif açılışın ardından yükselişine devam ederken gün içinde 8.920 puana kadar yükseldi.`, - graph: 'https://via.placeholder.com/150', // Placeholder image - likes: 32, - comments: 8 - } - ]; -*/ + const { user } = useAuth(); + const { baseURL } = config; const [posts, setPosts] = useState([]); const fetchPosts = async () => { - const baseURL = 'http://159.223.28.163:30002'; const postURL = baseURL + '/posts/'; try { @@ -41,7 +19,7 @@ const Community = ({navigation}) => { method: 'GET', headers: { 'Content-Type': 'application/json', - 'X-CSRFToken': 'WTyfHMRCB4yI4D5IhdreWdnFDe6skYPyBbenY9Z5F5VWc7lyii9zV0qXKjtEDGRN', + /* 'X-CSRFToken': 'WTyfHMRCB4yI4D5IhdreWdnFDe6skYPyBbenY9Z5F5VWc7lyii9zV0qXKjtEDGRN' ,*/ }, }); @@ -77,10 +55,17 @@ const Community = ({navigation}) => { }; const handleCreatePost = () => { - navigation.navigate('CreatePost'); + if(!user){ + Alert.alert('Please login to create a post'); + navigation.navigate('Login&Register'); + }else{ + navigation.navigate('CreatePost'); + } + } const renderItem = ({ item: post }) => ( + //console.log(post), {post.title} diff --git a/mobile/src/pages/CreatePost.js b/mobile/src/pages/CreatePost.js index ada545e3..941edb9b 100644 --- a/mobile/src/pages/CreatePost.js +++ b/mobile/src/pages/CreatePost.js @@ -2,31 +2,27 @@ import React, { useContext,useState } from "react"; import { View, Text, TextInput, TouchableOpacity, StyleSheet } from "react-native"; import { Chip } from "react-native-paper"; import { useAuth } from './context/AuthContext'; +import config from "./config/config"; const CreatePost = ({navigation}) => { + const { baseURL } = config; const { user, accessToken, refreshToken } = useAuth(); const [postTitle, setPostTitle] = useState(""); const [postContent, setPostContent] = useState(""); - //const [tags, setTags] = useState(["Lilium", "Hisse Analizi", "Amerika"]); const [tags, setTags] = useState([]); const removeTag = (tag) => { setTags(tags.filter((t) => t !== tag)); }; const handleCreation = async () => { - const baseURL = 'http://159.223.28.163:30002'; + const postData = { title: postTitle, content: postContent, liked_by:[], - tags:tags, + tags:[], portfolios:[] }; - console.log('title:', postTitle); - console.log('content:', postContent); - console.log('tag:', tags); - console.log('access:', accessToken); - console.log('refresh:', refreshToken); const postURL = baseURL + '/posts/'; @@ -43,18 +39,18 @@ const CreatePost = ({navigation}) => { if (response.ok) { const jsonResponse = await response.json(); - //console.log('Response:', jsonResponse); + console.log('Response:', jsonResponse); navigation.navigate("CommunityPage"); } else { const errorResponse = await response.json(); + console.log('Access', accessToken); console.log('Error Response:', errorResponse); throw new Error('Network response was not ok.'); } } catch (error) { - console.error('Error:', error); } }; @@ -62,10 +58,11 @@ const CreatePost = ({navigation}) => { return ( + console.log('Access', accessToken), @@ -80,12 +77,12 @@ const CreatePost = ({navigation}) => { ))} - + Tag Ekle + + Add Tag { + const { baseURL } = config; const { theme, toggleTheme, isDarkMode } = useContext(ThemeContext); const [latestPosts, setLatestPosts] = useState([]); const [loading, setLoading] = useState(true); const fetchLatestPosts = async () => { - const baseURL = 'http://159.223.28.163:30002'; const postURL = `${baseURL}/posts/`; try { const response = await fetch(postURL); const data = await response.json(); - setLatestPosts(data); // Assuming the API returns a paginated response + setLatestPosts(data); } catch (error) { console.error('Error fetching latest posts:', error); }finally { @@ -51,9 +51,9 @@ const Home = () => { data: loading ? mockPosts: latestPosts.map(post => ({ id: post.id, type: 'post', - username: post.author, // API'ye göre düzenle + username: post.author, title: post.title, - tag: post.tags, // API'ye göre düzenle + tag: post.tags, content: post.content, })), }, diff --git a/mobile/src/pages/News.js b/mobile/src/pages/News.js index 90d16884..eb4fafec 100644 --- a/mobile/src/pages/News.js +++ b/mobile/src/pages/News.js @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import { View, Text, StyleSheet, FlatList, Image, TouchableOpacity, ActivityIndicator } from 'react-native'; import { useFocusEffect } from '@react-navigation/native'; +import config from './config/config'; // Mock Data for some categories const newsData = [ @@ -46,8 +47,10 @@ const decodeHtmlEntities = (str) => { // Fetch News Data from API const fetchNews = async (feedName) => { + const { baseURL } = config; + const newsURL = `${baseURL}/news/`; try { - const response = await fetch('http://159.223.28.163:30002/news/', { + const response = await fetch(newsURL, { method: 'POST', headers: { accept: 'application/json', diff --git a/mobile/src/pages/Post.js b/mobile/src/pages/Post.js index 4cdc5288..df4b6d97 100644 --- a/mobile/src/pages/Post.js +++ b/mobile/src/pages/Post.js @@ -1,33 +1,31 @@ import React, { useState, useEffect } from 'react'; -import { ScrollView, Text, StyleSheet, View, Dimensions } from 'react-native'; +import { ScrollView, Text, StyleSheet, View, Dimensions, Button, TouchableOpacity } from 'react-native'; import { LineChart } from 'react-native-chart-kit'; +import config from './config/config'; -const Post = ({route}) => { + +const Post = ({ route }) => { const { postId } = route.params; + const { baseURL } = config; const [post, setPost] = useState(null); const [user, setUser] = useState({}); - - + const [likes, setLikes] = useState(0); // State to manage like count const fetchPost = async () => { - const baseURL = 'http://159.223.28.163:30002'; const postURL = `${baseURL}/posts/${postId}/`; - + try { const response = await fetch(postURL, { method: 'GET', headers: { 'Content-Type': 'application/json', - 'X-CSRFToken': 'WTyfHMRCB4yI4D5IhdreWdnFDe6skYPyBbenY9Z5F5VWc7lyii9zV0qXKjtEDGRN', + /* 'X-CSRFToken': 'WTyfHMRCB4yI4D5IhdreWdnFDe6skYPyBbenY9Z5F5VWc7lyii9zV0qXKjtEDGRN', */ }, }); if (response.ok) { const postData = await response.json(); setPost(postData); - - // Kullanıcı bilgisi çekme - const authorId = postData.author; - fetchUser(authorId); + setLikes(postData.likes || 0); // Set likes from post data } else { console.error('Error fetching post:', response.status); } @@ -35,51 +33,46 @@ const Post = ({route}) => { console.error('Error fetching post:', error.message, error.stack); } }; + + const handleLike = () => { + setLikes(likes + 1); - const fetchUser = async (authorId) => { - const baseURL = 'http://159.223.28.163:30002'; - const userURL = `${baseURL}/users/${authorId}/`; - - try { - const response = await fetch(userURL, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': 'WTyfHMRCB4yI4D5IhdreWdnFDe6skYPyBbenY9Z5F5VWc7lyii9zV0qXKjtEDGRN', - }, - }); - if (response.ok) { - const userData = await response.json(); - setUser(userData); - } else { - console.log('Error fetching user:', response); - console.error('Error fetching user:', response.status); - } - } catch (error) { - console.error('Error fetching user:', error.message, error.stack); - } }; - + + const handleAddComment = () => { + console.log('Add Comment button pressed'); + }; useEffect(() => { fetchPost(); - }, []); - + if (!post) { return Loading...; } return ( - + {post.title} {user.username} {post.created_at} {post.content} - + + + + + 👍 Like ({likes}) + + + + + 💬 Add Comment + + + ); - + // Sample stock data (Replace this with dynamic data) const stockData = [142, 145, 143, 141, 144, 140, 138, 139]; // Closing prices const [tooltip, setTooltip] = useState(null); // State for tooltip info @@ -118,8 +111,8 @@ const Post = ({route}) => { backgroundGradientFrom: '#1e2923', backgroundGradientTo: '#08130d', decimalPlaces: 2, // Rounds values to 2 decimal places - color: (opacity = 1) => `rgba(26, 255, 146, ${opacity})`, - labelColor: (opacity = 1) => `rgba(255, 255, 255, ${opacity})`, + color: (opacity = 1) => rgba(26, 255, 146, `${opacity}`), + labelColor: (opacity = 1) => rgba(255, 255, 255, `${opacity}`), style: { borderRadius: 16, }, @@ -133,7 +126,7 @@ const Post = ({route}) => { style={styles.chart} onDataPointClick={({ value, x, y, index }) => { setTooltip({ - value: `$${value}`, + value: `${value}`, x, y, index, @@ -158,64 +151,59 @@ const Post = ({route}) => { ); + }; const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: '#f5f5f5', - padding: 10, + backgroundColor: '#ffffff', + padding: 15, }, title: { - fontSize: 20, + fontSize: 24, fontWeight: 'bold', - marginBottom: 5, + color: '#333333', + marginBottom: 10, }, author: { fontSize: 16, - color: '#666', + color: '#555555', marginBottom: 5, }, date: { fontSize: 14, - color: '#888', - marginBottom: 10, - }, - tagsContainer: { - flexDirection: 'row', - marginBottom: 10, - }, - tag: { - backgroundColor: '#e0f7fa', - color: '#007BFF', - paddingHorizontal: 10, - paddingVertical: 5, - borderRadius: 15, - marginRight: 5, + color: '#999999', + marginBottom: 20, }, content: { fontSize: 16, + lineHeight: 24, + color: '#444444', marginBottom: 20, }, - graphTitle: { - fontSize: 18, - fontWeight: 'bold', - marginBottom: 10, + buttonContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + marginTop: 20, }, - chart: { - marginVertical: 8, - borderRadius: 16, + likeButton: { + backgroundColor: '#0073e6', + paddingVertical: 10, + paddingHorizontal: 20, + borderRadius: 8, }, - tooltip: { - position: 'absolute', - backgroundColor: '#000', - padding: 5, - borderRadius: 5, - alignItems: 'center', + commentButton: { + backgroundColor: '#28a745', + paddingVertical: 10, + paddingHorizontal: 20, + borderRadius: 8, }, - tooltipText: { - color: '#fff', - fontSize: 12, + buttonText: { + color: '#ffffff', + fontSize: 16, + fontWeight: 'bold', + textAlign: 'center', }, }); diff --git a/mobile/src/pages/Register.js b/mobile/src/pages/Register.js index 71d1a0c7..06a99021 100644 --- a/mobile/src/pages/Register.js +++ b/mobile/src/pages/Register.js @@ -8,6 +8,7 @@ import { Image, Alert, } from 'react-native'; +import config from './config/config'; const Register = ({ navigation }) => { const [email, setEmail] = useState(''); @@ -16,6 +17,7 @@ const Register = ({ navigation }) => { const [username, setUsername] = useState(''); const [showPassword, setShowPassword] = useState(false); const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const { baseURL } = config; const handleRegister = async () => { if (password !== confirmPassword) { @@ -24,7 +26,7 @@ const Register = ({ navigation }) => { } // Backend URL for registration - const registerUrl = 'http://159.223.28.163:30002/register/'; + const registerUrl = `${baseURL}/register/`; // Registration data const registerData = { @@ -71,7 +73,7 @@ const Register = ({ navigation }) => { const handleEmailVerification = async () => { // Backend URL for email verification - const verifyUrl = 'http://159.223.28.163:30002/email-verify/'; + const verifyUrl = `${baseURL}/verify-email/`; try { // Make the GET request to verify the email diff --git a/mobile/src/pages/config/config.js b/mobile/src/pages/config/config.js new file mode 100644 index 00000000..b748f253 --- /dev/null +++ b/mobile/src/pages/config/config.js @@ -0,0 +1,3 @@ +export default config = { + baseURL: 'http://159.223.28.163:30002', +} \ No newline at end of file From a2a3be38525fb167a4665a407378b01a28e37ab3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Muhammed=20Erkam=20G=C3=B6kcep=C4=B1nar?= Date: Sun, 8 Dec 2024 23:04:15 +0300 Subject: [PATCH 04/18] fix: solve problem related to post creation --- mobile/src/pages/Login.js | 15 +++++++-------- mobile/src/pages/context/AuthContext.js | 4 ++-- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/mobile/src/pages/Login.js b/mobile/src/pages/Login.js index 951e3e30..23dc4568 100644 --- a/mobile/src/pages/Login.js +++ b/mobile/src/pages/Login.js @@ -10,9 +10,10 @@ import { ActivityIndicator, } from 'react-native'; import { useAuth } from './context/AuthContext'; // Import AuthContext - +import config from './config/config'; const Login = ({ navigation }) => { + const { baseURL } = config; const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [showPassword, setShowPassword] = useState(false); @@ -21,9 +22,7 @@ const Login = ({ navigation }) => { const { login } = useAuth(); // Access the login function from AuthContext const handleLogin = async () => { - - const url = 'http://159.223.28.163:30002/login/'; - + const loginUrl = `${baseURL}/login/`; // Login data const loginData = { @@ -31,13 +30,11 @@ const Login = ({ navigation }) => { password: password, }; - setLoading(true); // Show loading spinner - try { // Make the POST request to the backend - const response = await fetch(url, { + const response = await fetch(loginUrl, { method: 'POST', headers: { Accept: 'application/json', @@ -52,7 +49,9 @@ const Login = ({ navigation }) => { // Check for a successful login if (response.ok) { // Handle successful login - + console.log('Login data:', data); + //console.log('Access', data.access); + //console.log('Refresh', data.refresh); const { access, refresh } = data; login(username, access, refresh); diff --git a/mobile/src/pages/context/AuthContext.js b/mobile/src/pages/context/AuthContext.js index 4456cc38..af4cc049 100644 --- a/mobile/src/pages/context/AuthContext.js +++ b/mobile/src/pages/context/AuthContext.js @@ -14,7 +14,7 @@ setUser({ username }); setAccessToken(access); setRefreshToken(refresh); - + console.log("access token",accessToken); }; const logout = async () => { @@ -35,7 +35,7 @@ return ( - + {children} ); From 0198c5f840f179b33b52083140d94af4dddb6f54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Muhammed=20Erkam=20G=C3=B6kcep=C4=B1nar?= Date: Fri, 13 Dec 2024 21:00:41 +0300 Subject: [PATCH 05/18] fix: show authors of the posts correctly --- mobile/src/pages/Community.js | 55 ++++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/mobile/src/pages/Community.js b/mobile/src/pages/Community.js index 274b300f..b3d8edab 100644 --- a/mobile/src/pages/Community.js +++ b/mobile/src/pages/Community.js @@ -11,6 +11,8 @@ const Community = ({navigation}) => { const { user } = useAuth(); const { baseURL } = config; const [posts, setPosts] = useState([]); + const [users, setUsers] = useState([]); + const [userMap, setUserMap] = useState([]); const fetchPosts = async () => { const postURL = baseURL + '/posts/'; @@ -39,15 +41,52 @@ const Community = ({navigation}) => { console.error('Error:', error); } + }; - + const fetchUsers = async () => { + const postURL = baseURL + '/users/'; + + try { + const response = await fetch(postURL, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + /* 'X-CSRFToken': 'WTyfHMRCB4yI4D5IhdreWdnFDe6skYPyBbenY9Z5F5VWc7lyii9zV0qXKjtEDGRN' ,*/ + }, + }); + + if (response.ok) { + const jsonResponse = await response.json(); + //console.log('Response:', jsonResponse); + setUsers(jsonResponse); + + const map = {}; + jsonResponse.forEach((user) => { + map[user.id] = user; + }); + setUserMap(map); + + + } else { + const errorResponse = await response.json(); + console.log('Error Response:', errorResponse); + + throw new Error('Network response was not ok.'); + + } + } catch (error) { + + console.error('Error:', error); + } }; - useFocusEffect( - React.useCallback(() => { - fetchPosts(); - }, []) - ); + useFocusEffect( + React.useCallback(() => { + fetchPosts(); + fetchUsers(); + }, []) + ); + const handleViewPost = (post) => { @@ -65,11 +104,11 @@ const Community = ({navigation}) => { } const renderItem = ({ item: post }) => ( - //console.log(post), + console.log(post), {post.title} - Published on: {post.date} by {post.author} + Published on: {post.date} by {userMap[post.author].username} {post.content} From d7b937e444bfd3948fe0b98ec68853ffc2c25274 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Muhammed=20Erkam=20G=C3=B6kcep=C4=B1nar?= Date: Fri, 13 Dec 2024 21:26:22 +0300 Subject: [PATCH 06/18] fix: save userId by decoding access token --- mobile/src/pages/Login.js | 7 +++---- mobile/src/pages/context/AuthContext.js | 20 +++++++++++--------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/mobile/src/pages/Login.js b/mobile/src/pages/Login.js index 23dc4568..7402b0bd 100644 --- a/mobile/src/pages/Login.js +++ b/mobile/src/pages/Login.js @@ -11,6 +11,7 @@ import { } from 'react-native'; import { useAuth } from './context/AuthContext'; // Import AuthContext import config from './config/config'; +import { jwtDecode } from 'jwt-decode'; const Login = ({ navigation }) => { const { baseURL } = config; @@ -49,11 +50,9 @@ const Login = ({ navigation }) => { // Check for a successful login if (response.ok) { // Handle successful login - console.log('Login data:', data); - //console.log('Access', data.access); - //console.log('Refresh', data.refresh); const { access, refresh } = data; - login(username, access, refresh); + const decodedToken = jwtDecode(access); + login(decodedToken.username, decodedToken.user_id, access, refresh); Alert.alert('Login Successful', 'Welcome!'); navigation.navigate('Home'); // Navigate to the Home screen diff --git a/mobile/src/pages/context/AuthContext.js b/mobile/src/pages/context/AuthContext.js index af4cc049..da4461e8 100644 --- a/mobile/src/pages/context/AuthContext.js +++ b/mobile/src/pages/context/AuthContext.js @@ -4,28 +4,30 @@ const AuthContext = createContext(); export const AuthProvider = ({ children }) => { - const [user, setUser] = useState(null); // Store logged-in user + const [username, setUsername] = useState(null); // Store logged-in user + const [userId, setUserId] = useState(null); // Store logged-in user const [accessToken, setAccessToken] = useState(null); const [refreshToken, setRefreshToken] = useState(null); + + - - - const login = (username, access, refresh) => { - setUser({ username }); + const login = (username, userId, access, refresh) => { + setUsername(username ); + setUserId(userId); setAccessToken(access); setRefreshToken(refresh); - console.log("access token",accessToken); + }; const logout = async () => { if (!refreshToken) { console.warn('No refresh token available for logout.'); - setUser(null); + setUsername(null); setRefreshToken(null); return; }else{ - setUser(null); + setUsername(null); setRefreshToken(null); return; } @@ -35,7 +37,7 @@ return ( - + {children} ); From aa9f7e25f225054acfdf8ba99086a3760e169cd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Muhammed=20Erkam=20G=C3=B6kcep=C4=B1nar?= Date: Sat, 14 Dec 2024 11:01:51 +0300 Subject: [PATCH 07/18] feat: implement tag creation --- mobile/src/pages/CreatePost.js | 195 ++++++++++++++++++++++----------- 1 file changed, 133 insertions(+), 62 deletions(-) diff --git a/mobile/src/pages/CreatePost.js b/mobile/src/pages/CreatePost.js index 941edb9b..c822d092 100644 --- a/mobile/src/pages/CreatePost.js +++ b/mobile/src/pages/CreatePost.js @@ -1,64 +1,109 @@ -import React, { useContext,useState } from "react"; -import { View, Text, TextInput, TouchableOpacity, StyleSheet } from "react-native"; +import React, { useState, useEffect } from "react"; +import { View, Text, TextInput, TouchableOpacity, ScrollView, StyleSheet, Alert } from "react-native"; import { Chip } from "react-native-paper"; -import { useAuth } from './context/AuthContext'; +import { useAuth } from "./context/AuthContext"; import config from "./config/config"; -const CreatePost = ({navigation}) => { +const CreatePost = ({ navigation }) => { const { baseURL } = config; - const { user, accessToken, refreshToken } = useAuth(); + const { accessToken, userId } = useAuth(); const [postTitle, setPostTitle] = useState(""); const [postContent, setPostContent] = useState(""); - const [tags, setTags] = useState([]); - const removeTag = (tag) => { - setTags(tags.filter((t) => t !== tag)); - }; + const [availableTags, setAvailableTags] = useState([]); + const [selectedTags, setSelectedTags] = useState([]); + const [newTag, setNewTag] = useState(""); - const handleCreation = async () => { - - const postData = { - title: postTitle, - content: postContent, - liked_by:[], - tags:[], - portfolios:[] - }; - - const postURL = baseURL + '/posts/'; + useEffect(() => { + console.log("CreatePost access", userId); + fetchTags(); + }, []); + const fetchTags = async () => { try { - const response = await fetch(postURL, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${accessToken}`, - 'X-CSRFToken': 'WTyfHMRCB4yI4D5IhdreWdnFDe6skYPyBbenY9Z5F5VWc7lyii9zV0qXKjtEDGRN', - }, - body: JSON.stringify(postData) - }); + const response = await fetch(`${baseURL}/tags/`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + if (response.ok) { + const data = await response.json(); + setAvailableTags(data); + + } else { + console.error("Failed to fetch tags"); + } + } catch (error) { + console.error("Error fetching tags:", error); + } + }; - if (response.ok) { - const jsonResponse = await response.json(); - console.log('Response:', jsonResponse); - navigation.navigate("CommunityPage"); - - } else { - const errorResponse = await response.json(); - console.log('Access', accessToken); - console.log('Error Response:', errorResponse); - - throw new Error('Network response was not ok.'); - - } + const addTag = async () => { + if (newTag.trim() === "") return; + const tagData = { + name: newTag, + }; + try { + const response = await fetch(`${baseURL}/tags/`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify(tagData), + }); + if (response.ok) { + const tag = await response.json(); + setAvailableTags([...availableTags, tag]); + setNewTag(""); + Alert.alert("Tag added successfully"); + } else { + console.error(response); + console.error("Failed to add tag"); + } } catch (error) { - console.error('Error:', error); + console.error("Error adding tag:", error); } }; + const toggleTagSelection = (tag) => { + if (selectedTags.includes(tag.id)) { + setSelectedTags(selectedTags.filter((id) => id !== tag.id)); + } else { + setSelectedTags([...selectedTags, tag.id]); + } + }; + const handleCreation = async () => { + const postData = { + title: postTitle, + content: postContent, + liked_by: [], + tags: selectedTags, + portfolios: [], + }; + try { + const response = await fetch(`${baseURL}/posts/`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify(postData), + }); + if (response.ok) { + Alert.alert("Post created successfully"); + navigation.navigate("CommunityPage"); + } else { + console.error(response); + console.error("Failed to create post"); + } + } catch (error) { + console.error("Error creating post:", error); + } + }; return ( - console.log('Access', accessToken), { value={postTitle} onChangeText={setPostTitle} /> - - {tags.map((tag, index) => ( + + {availableTags.map((tag) => ( removeTag(tag)} + key={tag.id} + style={[ + styles.tag, + selectedTags.includes(tag.id) && styles.selectedTag, + ]} + onPress={() => toggleTagSelection(tag)} > - {tag} + {tag.name} ))} - + + + + + Add Tag @@ -87,10 +143,7 @@ const CreatePost = ({navigation}) => { onChangeText={setPostContent} multiline /> - handleCreation()} - > + Post @@ -111,22 +164,40 @@ const styles = StyleSheet.create({ marginBottom: 16, padding: 8, }, - tagContainer: { - flexDirection: "row", - flexWrap: "wrap", + tagScrollView: { + maxHeight: 50, // Limit height for vertical scrolling marginBottom: 16, }, tag: { margin: 4, + borderRadius: 16, + paddingHorizontal: 8, + paddingVertical: 4, + backgroundColor: "#e0e0e0", + }, + selectedTag: { + backgroundColor: "#007BFF", + }, + addTagRow: { + flexDirection: "row", + alignItems: "center", + marginBottom: 16, + }, + newTagInput: { + flex: 1, + borderBottomWidth: 1, + borderColor: "#ccc", + marginRight: 8, + padding: 4, }, addTagButton: { - margin: 4, + backgroundColor: "#007BFF", + borderRadius: 8, padding: 8, - backgroundColor: "#e0e0e0", - borderRadius: 16, }, addTagText: { - color: "#555", + color: "#fff", + fontWeight: "bold", }, contentInput: { flex: 1, From ffdcb4bdb9482afc717e8be9925ca87959fe1be2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Muhammed=20Erkam=20G=C3=B6kcep=C4=B1nar?= Date: Sun, 15 Dec 2024 13:03:17 +0300 Subject: [PATCH 08/18] fix: solve user context problems --- mobile/src/pages/App.js | 12 ++++++------ mobile/src/pages/ProfilePage.js | 6 +++--- mobile/src/pages/context/AuthContext.js | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/mobile/src/pages/App.js b/mobile/src/pages/App.js index 4f2fad02..c0328ed8 100644 --- a/mobile/src/pages/App.js +++ b/mobile/src/pages/App.js @@ -26,12 +26,12 @@ const Drawer = createDrawerNavigator(); // Custom Header const CustomHeader = ({ navigation }) => { - const { user } = useAuth(); - + + const { username, userId } = useAuth(); const handleProfileNavigation = () => { - if (user) { - navigation.navigate('Profile', { username: user.username }); + if (userId) { + navigation.navigate('Profile', { username: username }); } else { navigation.navigate('Login&Register'); } @@ -72,7 +72,7 @@ const PostStack = () => { ) } const DrawerNavigator = () => { - const { user } = useAuth(); + const { username, userId } = useAuth(); @@ -89,7 +89,7 @@ const DrawerNavigator = () => { component={Home} /> - { user ? ( + { userId ? ( { - const { user, logout } = useAuth(); // Access user and logout function from AuthContext + const { username, userId, logout } = useAuth(); // Access user and logout function from AuthContext @@ -53,9 +53,9 @@ const Profile = ({ navigation }) => { source={require('../../assets/stock-logos/Profile.png')} style={styles.profilePhoto} /> - @{user.username} + @{username} - Followers: {user.followers || '0'} {/* Follower count should be rendered properly */} + Followers: {username.followers || '0'} {/* Follower count should be rendered properly */} Logout diff --git a/mobile/src/pages/context/AuthContext.js b/mobile/src/pages/context/AuthContext.js index da4461e8..f41a1dcd 100644 --- a/mobile/src/pages/context/AuthContext.js +++ b/mobile/src/pages/context/AuthContext.js @@ -5,7 +5,7 @@ export const AuthProvider = ({ children }) => { const [username, setUsername] = useState(null); // Store logged-in user - const [userId, setUserId] = useState(null); // Store logged-in user + const [userId, setUserId] = useState(0); const [accessToken, setAccessToken] = useState(null); const [refreshToken, setRefreshToken] = useState(null); From ba048a03b379f8a4abdf8da1976716b05eec90d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Muhammed=20Erkam=20G=C3=B6kcep=C4=B1nar?= Date: Sun, 15 Dec 2024 14:09:07 +0300 Subject: [PATCH 09/18] feat: show tags in post page --- mobile/src/pages/CreatePost.js | 1 + mobile/src/pages/Post.js | 229 +++++++++++++++++---------------- 2 files changed, 122 insertions(+), 108 deletions(-) diff --git a/mobile/src/pages/CreatePost.js b/mobile/src/pages/CreatePost.js index c822d092..19e43391 100644 --- a/mobile/src/pages/CreatePost.js +++ b/mobile/src/pages/CreatePost.js @@ -57,6 +57,7 @@ const CreatePost = ({ navigation }) => { setAvailableTags([...availableTags, tag]); setNewTag(""); Alert.alert("Tag added successfully"); + fetchTags(); } else { console.error(response); console.error("Failed to add tag"); diff --git a/mobile/src/pages/Post.js b/mobile/src/pages/Post.js index df4b6d97..318769c9 100644 --- a/mobile/src/pages/Post.js +++ b/mobile/src/pages/Post.js @@ -1,51 +1,68 @@ import React, { useState, useEffect } from 'react'; -import { ScrollView, Text, StyleSheet, View, Dimensions, Button, TouchableOpacity } from 'react-native'; +import { ScrollView, Text, StyleSheet, View, Dimensions, TouchableOpacity } from 'react-native'; import { LineChart } from 'react-native-chart-kit'; import config from './config/config'; - const Post = ({ route }) => { - const { postId } = route.params; + const { postId, author } = route.params; const { baseURL } = config; + const [post, setPost] = useState(null); - const [user, setUser] = useState({}); - const [likes, setLikes] = useState(0); // State to manage like count + const [likes, setLikes] = useState(0); + const [tooltip, setTooltip] = useState(null); + //const [author, setAuthor] = useState(null); + useEffect(() => { + fetchPost(); + }, [postId]); + const fetchPost = async () => { const postURL = `${baseURL}/posts/${postId}/`; - try { - const response = await fetch(postURL, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - /* 'X-CSRFToken': 'WTyfHMRCB4yI4D5IhdreWdnFDe6skYPyBbenY9Z5F5VWc7lyii9zV0qXKjtEDGRN', */ - }, - }); + const response = await fetch(postURL); if (response.ok) { const postData = await response.json(); + + // Use postData directly to fetch the author setPost(postData); - setLikes(postData.likes || 0); // Set likes from post data + setLikes(postData.liked_by.length || 0); + + // Fetch author data based on postData.author + /* if (postData.author) { + fetchAuthor(postData.author); + } else { + console.warn('Post does not contain an author field.'); + } */ } else { - console.error('Error fetching post:', response.status); + console.error(`Failed to fetch post: ${response.status}`); } } catch (error) { - console.error('Error fetching post:', error.message, error.stack); + console.error('Error fetching post:', error); } }; - const handleLike = () => { - setLikes(likes + 1); - + + + const fetchAuthor = async (authorId) => { + const authorURL = `${baseURL}/users/${authorId}/`; // Replace with your API's author endpoint + try { + const response = await fetch(authorURL); + if (response.ok) { + const authorData = await response.json(); + setAuthor(authorData); + } else { + console.error(`Failed to fetch author: ${response.status}`); + } + } catch (error) { + console.error('Error fetching author:', error); + } }; - const handleAddComment = () => { - console.log('Add Comment button pressed'); + const handleLike = () => { + setLikes((prevLikes) => prevLikes + 1); }; - useEffect(() => { - fetchPost(); - }, []); + const stockData = [142, 145, 143, 141, 144, 140, 138, 139]; if (!post) { return Loading...; @@ -54,106 +71,84 @@ const Post = ({ route }) => { return ( {post.title} - {user.username} - {post.created_at} + Author: {author ? author : 'Unknown'} + + {new Date(post.created_at).toLocaleDateString()} at {new Date(post.created_at).toLocaleTimeString()} + + + {post.tags.length > 0 ? ( + post.tags.map((tag) => ( + + #{tag.name} + + )) + ) : ( + No tags available + )} + {post.content} - + + + {/* Buttons */} 👍 Like ({likes}) - - - + 💬 Add Comment - - ); - - // Sample stock data (Replace this with dynamic data) - const stockData = [142, 145, 143, 141, 144, 140, 138, 139]; // Closing prices - const [tooltip, setTooltip] = useState(null); // State for tooltip info - - return ( - - NVIDIA Stock Analysis - By: Elif Demir - Published on: 2024-10-12 - - NVIDIA - Stock Analysis - Investments - - - NVIDIA stocks have recently seen a sharp decline, raising concerns among investors. Here's the latest trend for better understanding. - + {/* Chart Section */} Stock Price Chart - - {/* Line Chart */} - - rgba(26, 255, 146, `${opacity}`), - labelColor: (opacity = 1) => rgba(255, 255, 255, `${opacity}`), - style: { - borderRadius: 16, - }, - propsForDots: { - r: '6', - strokeWidth: '2', - stroke: '#ffa726', + `rgba(26, 255, 146, ${opacity})`, + labelColor: (opacity = 1) => `rgba(255, 255, 255, ${opacity})`, + style: { + borderRadius: 16, + }, + propsForDots: { + r: '6', + strokeWidth: '2', + stroke: '#ffa726', + }, + }} + bezier + style={styles.chart} + onDataPointClick={({ value, x, y }) => setTooltip({ value, x, y })} + /> + {tooltip && ( + { - setTooltip({ - value: `${value}`, - x, - y, - index, - }); - }} - /> - - {/* Tooltip for the clicked point */} - {tooltip && ( - - {tooltip.value} - - )} - + ]} + > + ${tooltip.value} + + )} ); - }; + + const styles = StyleSheet.create({ container: { flex: 1, @@ -205,6 +200,24 @@ const styles = StyleSheet.create({ fontWeight: 'bold', textAlign: 'center', }, + tagsContainer: { + flexDirection: 'row', + flexWrap: 'wrap', + marginBottom: 10, + }, + tag: { + backgroundColor: '#e0f7fa', + color: '#007BFF', + paddingHorizontal: 10, + paddingVertical: 5, + borderRadius: 15, + marginRight: 5, + marginBottom: 5, + }, + noTags: { + fontSize: 14, + color: '#aaa', + }, }); export default Post; From 512e6519b490e3399e80290236112fc7c9f576bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Muhammed=20Erkam=20G=C3=B6kcep=C4=B1nar?= Date: Sun, 15 Dec 2024 14:10:08 +0300 Subject: [PATCH 10/18] fix: solve problems related with showing authors in community --- mobile/src/pages/Community.js | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/mobile/src/pages/Community.js b/mobile/src/pages/Community.js index b3d8edab..cb4932fd 100644 --- a/mobile/src/pages/Community.js +++ b/mobile/src/pages/Community.js @@ -8,37 +8,31 @@ import { useAuth } from './context/AuthContext'; const Community = ({navigation}) => { - const { user } = useAuth(); + const { userId } = useAuth(); const { baseURL } = config; const [posts, setPosts] = useState([]); const [users, setUsers] = useState([]); const [userMap, setUserMap] = useState([]); const fetchPosts = async () => { - const postURL = baseURL + '/posts/'; + const postURL = baseURL + '/posts/?page=1'; try { const response = await fetch(postURL, { method: 'GET', headers: { 'Content-Type': 'application/json', - /* 'X-CSRFToken': 'WTyfHMRCB4yI4D5IhdreWdnFDe6skYPyBbenY9Z5F5VWc7lyii9zV0qXKjtEDGRN' ,*/ }, }); if (response.ok) { const jsonResponse = await response.json(); - //console.log('Response:', jsonResponse); setPosts(jsonResponse); - } else { const errorResponse = await response.json(); console.log('Error Response:', errorResponse); - throw new Error('Network response was not ok.'); - } } catch (error) { - console.error('Error:', error); } }; @@ -52,15 +46,12 @@ const Community = ({navigation}) => { method: 'GET', headers: { 'Content-Type': 'application/json', - /* 'X-CSRFToken': 'WTyfHMRCB4yI4D5IhdreWdnFDe6skYPyBbenY9Z5F5VWc7lyii9zV0qXKjtEDGRN' ,*/ }, }); if (response.ok) { const jsonResponse = await response.json(); - //console.log('Response:', jsonResponse); setUsers(jsonResponse); - const map = {}; jsonResponse.forEach((user) => { map[user.id] = user; @@ -76,7 +67,6 @@ const Community = ({navigation}) => { } } catch (error) { - console.error('Error:', error); } }; @@ -90,11 +80,11 @@ const Community = ({navigation}) => { const handleViewPost = (post) => { - navigation.navigate('Post', { postId: post.id }); + navigation.navigate('Post', { postId: post.id, author: userMap[post.author] ? userMap[post.author].username : post.author }); }; const handleCreatePost = () => { - if(!user){ + if(!userId){ Alert.alert('Please login to create a post'); navigation.navigate('Login&Register'); }else{ @@ -103,17 +93,25 @@ const Community = ({navigation}) => { } + const renderUsername = (post) => { + if(userMap[post.author]){ + return userMap[post.author].username; + }else{ + return post.author; + } + } + const renderItem = ({ item: post }) => ( - console.log(post), + console.log("post", post), {post.title} - Published on: {post.date} by {userMap[post.author].username} + Published on: {new Date(post.created_at).toLocaleDateString()} by {renderUsername(post)} {post.content} {post.tags.map((tag) => ( - {tag} + {tag.name} ))} {post.graph && ( From fb3c614c5ddc066cffe129fd4ca893513c22dd30 Mon Sep 17 00:00:00 2001 From: furkansenkal <72168386+furkansenkal@users.noreply.github.com> Date: Sun, 15 Dec 2024 17:29:58 +0300 Subject: [PATCH 11/18] feat: connect markets page to backend --- mobile/src/pages/Markets.js | 245 ++++++++++++++---------------------- 1 file changed, 91 insertions(+), 154 deletions(-) diff --git a/mobile/src/pages/Markets.js b/mobile/src/pages/Markets.js index 2d55cfb4..9bf9bf8d 100644 --- a/mobile/src/pages/Markets.js +++ b/mobile/src/pages/Markets.js @@ -1,147 +1,109 @@ -import React, { useState } from 'react'; -import { View, Text, FlatList, TouchableOpacity, StyleSheet, ScrollView } from 'react-native'; - -const mockStocks = { - "BIST30": [// BIST 30 - { code: 'AKBNK', name: 'Akbank T.A.Ş.', price: 8.76, about: 'Akbank is one of the largest banks in Turkey, offering a wide range of banking services.' }, - { code: 'ALARK', name: 'Alarko Holding A.Ş.', price: 6.78, about: 'Alarko is a major Turkish conglomerate involved in construction, energy, and tourism.' }, - { code: 'ARCLK', name: 'Arçelik A.Ş.', price: 29.56, about: 'Arçelik is a global home appliances manufacturer, known for brands like Beko and Grundig.' }, - { code: 'ASELS', name: 'Aselsan Elektronik Sanayi ve Ticaret A.Ş.', price: 23.67, about: 'Aselsan is Turkey’s leading defense electronics company, producing advanced technology solutions.' }, - { code: 'BIMAS', name: 'BİM Birleşik Mağazalar A.Ş.', price: 14.89, about: 'BIM is a prominent Turkish retail chain, offering low-cost consumer products.' }, - { code: 'DOHOL', name: 'Doğan Holding A.Ş.', price: 3.12, about: 'Doğan Holding is active in various sectors, including media, energy, and finance.' }, - { code: 'EKGYO', name: 'Emlak Konut Gayrimenkul Yatırım Ortaklığı A.Ş.', price: 3.78, about: 'Emlak Konut is a leading real estate investment trust in Turkey, primarily focused on housing projects.' }, - { code: 'ENJSA', name: 'Enerjisa Enerji A.Ş.', price: 13.22, about: 'Enerjisa operates in Turkey’s energy market, providing electricity distribution and sales.' }, - { code: 'EREGL', name: 'Ereğli Demir ve Çelik Fabrikaları T.A.Ş.', price: 9.47, about: 'Ereğli is one of Turkey’s largest steel producers, supplying a wide range of industries.' }, - { code: 'FROTO', name: 'Ford Otosan A.Ş.', price: 25.45, about: 'Ford Otosan is a joint venture between Ford and Koç Holding, producing commercial vehicles in Turkey.' }, - { code: 'GARAN', name: 'Türkiye Garanti Bankası A.Ş.', price: 11.23, about: 'Garanti is one of Turkey’s leading private banks, known for its innovative banking services.' }, - { code: 'ISCTR', name: 'Türkiye İş Bankası A.Ş.', price: 6.35, about: 'İşbank is Turkey’s largest private bank, providing a broad range of financial services.' }, - { code: 'KCHOL', name: 'Koç Holding A.Ş.', price: 18.90, about: 'Koç Holding is the largest industrial conglomerate in Turkey, with interests in energy, automotive, and finance.' }, - { code: 'KOZAA', name: 'Koza Altın İşletmeleri A.Ş.', price: 10.22, about: 'Koza Altın is Turkey’s largest gold mining company, engaged in exploration and production of gold.' }, - { code: 'KRDMD', name: 'Kardemir Karabük Demir Çelik Sanayi ve Ticaret A.Ş.', price: 4.56, about: 'Kardemir is a major Turkish steel producer, primarily serving the construction and automotive industries.' }, - { code: 'ODAS', name: 'Odaş Elektrik Üretim Sanayi Ticaret A.Ş.', price: 1.82 }, - { code: 'PETKM', name: 'Petkim Petrokimya Holding A.Ş.', price: 6.78 }, - { code: 'PGSUS', name: 'Pegasus Hava Taşımacılığı A.Ş.', price: 20.50 }, - { code: 'SAHOL', name: 'Hacı Ömer Sabancı Holding A.Ş.', price: 12.67 }, - { code: 'SISE', name: 'Şişecam A.Ş.', price: 10.33 }, - { code: 'SODA', name: 'Soda Sanayii A.Ş.', price: 7.44 }, - { code: 'TAVHL', name: 'TAV Havalimanları Holding A.Ş.', price: 19.05 }, - { code: 'TCELL', name: 'Turkcell İletişim Hizmetleri A.Ş.', price: 14.32 }, - { code: 'THYAO', name: 'Türk Hava Yolları A.O.', price: 15.56 }, - { code: 'TOASO', name: 'Tofaş Türk Otomobil Fabrikası A.Ş.', price: 20.87 }, - { code: 'TTKOM', name: 'Türk Telekomünikasyon A.Ş.', price: 7.68 }, - { code: 'TUPRS', name: 'Tüpraş-Türkiye Petrol Rafinerileri A.Ş.', price: 31.23 }, - { code: 'VAKBN', name: 'Türkiye Vakıflar Bankası T.A.O.', price: 7.12 }, - { code: 'YKBNK', name: 'Yapı ve Kredi Bankası A.Ş.', price: 8.23 }], - "S&P50": [ // S&P top 50 - { code: 'AAPL', name: 'Apple Inc.', price: 175.00, about: 'Apple is a global technology company known for its consumer electronics, including the iPhone, Mac, and Apple Watch.' }, - { code: 'MSFT', name: 'Microsoft Corporation', price: 350.00, about: 'Microsoft is a multinational technology company, best known for its Windows operating system and Office suite.' }, - { code: 'AMZN', name: 'Amazon.com, Inc.', price: 145.00, about: 'Amazon is the world’s largest online retailer, also heavily involved in cloud computing and artificial intelligence.' }, - { code: 'GOOGL', name: 'Alphabet Inc. (Class A)', price: 120.00, about: 'Alphabet is the parent company of Google, specializing in internet-related services and products.' }, - { code: 'FB', name: 'Meta Platforms, Inc.', price: 300.00, about: 'Meta (formerly Facebook) operates the world’s largest social media platforms, including Facebook and Instagram.' }, - { code: 'TSLA', name: 'Tesla, Inc.', price: 720.00, about: 'Tesla is a leading electric vehicle manufacturer, also involved in renewable energy and battery technology.' }, - { code: 'BRK.B', name: 'Berkshire Hathaway Inc. (Class B)', price: 325.00, about: 'Berkshire Hathaway is a multinational conglomerate headed by Warren Buffett, with diverse holdings in various industries.' }, - { code: 'NVDA', name: 'NVIDIA Corporation', price: 480.00, about: 'NVIDIA is a global leader in graphics processing units (GPUs) and AI computing technology.' }, - { code: 'JPM', name: 'JPMorgan Chase & Co.', price: 140.00, about: 'JPMorgan Chase is one of the largest global financial services companies, providing investment banking and financial services.' }, - { code: 'JNJ', name: 'Johnson & Johnson', price: 160.00, about: 'Johnson & Johnson is a multinational healthcare company, known for its pharmaceutical, medical device, and consumer health products.' }, - { code: 'V', name: 'Visa Inc.', price: 250.00, about: 'Visa is a global payments technology company, facilitating electronic funds transfers worldwide.' }, - { code: 'PG', name: 'Procter & Gamble Co.', price: 145.00, about: 'Procter & Gamble is a multinational consumer goods company, known for brands like Tide, Pampers, and Gillette.' }, - { code: 'UNH', name: 'UnitedHealth Group Incorporated', price: 490.00, about: 'UnitedHealth Group is a healthcare company, offering insurance services and healthcare products.' }, - { code: 'HD', name: 'The Home Depot, Inc.', price: 330.00, about: 'Home Depot is the largest home improvement retailer in the US, selling tools, construction products, and services.' }, - { code: 'DIS', name: 'The Walt Disney Company', price: 120.00, about: 'Disney is a global entertainment conglomerate, known for its film studios, theme parks, and media networks.' }, - { code: 'PYPL', name: 'PayPal Holdings, Inc.', price: 80.00 }, - { code: 'MA', name: 'Mastercard Incorporated', price: 380.00 }, - { code: 'CMCSA', name: 'Comcast Corporation', price: 40.00 }, - { code: 'VZ', name: 'Verizon Communications Inc.', price: 35.00 }, - { code: 'NFLX', name: 'Netflix, Inc.', price: 490.00 }, - { code: 'PEP', name: 'PepsiCo, Inc.', price: 190.00 }, - { code: 'T', name: 'AT&T Inc.', price: 15.00 }, - { code: 'CSCO', name: 'Cisco Systems, Inc.', price: 55.00 }, - { code: 'INTC', name: 'Intel Corporation', price: 30.00 }, - { code: 'IBM', name: 'International Business Machines Corporation', price: 135.00 }, - { code: 'TXN', name: 'Texas Instruments Incorporated', price: 185.00 }, - { code: 'LLY', name: 'Eli Lilly and Company', price: 560.00 }, - { code: 'MDT', name: 'Medtronic plc', price: 90.00 }, - { code: 'COST', name: 'Costco Wholesale Corporation', price: 500.00 }, - { code: 'NOW', name: 'ServiceNow, Inc.', price: 550.00 }, - { code: 'QCOM', name: 'QUALCOMM Incorporated', price: 120.00 }, - { code: 'NKE', name: 'Nike, Inc.', price: 150.00 }, - { code: 'MRK', name: 'Merck & Co., Inc.', price: 110.00 }, - { code: 'AMGN', name: 'Amgen Inc.', price: 250.00 }, - { code: 'ISRG', name: 'Intuitive Surgical, Inc.', price: 300.00 }, - { code: 'LMT', name: 'Lockheed Martin Corporation', price: 420.00 }, - { code: 'SPGI', name: 'S&P Global Inc.', price: 400.00 }, - { code: 'MDLZ', name: 'Mondelez International, Inc.', price: 62.00 }, - { code: 'HON', name: 'Honeywell International Inc.', price: 220.00 }, - { code: 'TMO', name: 'Thermo Fisher Scientific Inc.', price: 550.00 }, - { code: 'ADBE', name: 'Adobe Inc.', price: 550.00 }, - { code: 'CAT', name: 'Caterpillar Inc.', price: 260.00 }, - { code: 'SYK', name: 'Stryker Corporation', price: 280.00 }, - { code: 'SYY', name: 'Sysco Corporation', price: 80.00 }, - { code: 'FIS', name: 'Fidelity National Information Services, Inc.', price: 70.00 }, - { code: 'C', name: 'Citigroup Inc.', price: 55.00 }, - { code: 'AXP', name: 'American Express Company', price: 180.00 }, - { code: 'MCO', name: 'Moody\'s Corporation', price: 360.00 }, - { code: 'BKNG', name: 'Booking Holdings Inc.', price: 2200.00 }, - { code: 'SCHW', name: 'The Charles Schwab Corporation', price: 90.00 }, - { code: 'DHR', name: 'Danaher Corporation', price: 310.00 }, - { code: 'ZTS', name: 'Zoetis Inc.', price: 200.00 }, - { code: 'LRCX', name: 'Lam Research Corporation', price: 600.00 }, - { code: 'FISV', name: 'FISV', price: 120.00 }, - { code: 'ADP', name: 'Automatic Data Processing, Inc.', price: 240.00 }], - }; +import React, { useState, useEffect } from 'react'; +import { View, Text, FlatList, TouchableOpacity, StyleSheet, ActivityIndicator, Alert } from 'react-native'; const Markets = () => { const [selectedStock, setSelectedStock] = useState(null); - const [activeCategory, setActiveCategory] = useState('BIST30'); + const [stocks, setStocks] = useState([]); // Store all loaded stocks + const [loading, setLoading] = useState(true); + const [loadingMore, setLoadingMore] = useState(false); // Loading state for pagination + const [page, setPage] = useState(1); // Current page number + const [hasMore, setHasMore] = useState(true); // To check if there are more pages to load + + const fetchStocks = async (pageNumber = 1) => { + try { + const response = await fetch(`http://159.223.28.163:30002/stocks/?page=${pageNumber}`, { + method: 'GET', + headers: { + accept: 'application/json', + Authorization: 'Basic ZnVya2Fuc2Vua2FsOkxvc29sdmlkYWRvcy41NQ==', + 'X-CSRFToken': 'HN4gYGlxSnwtGKK91OG9c6WC6gr8091Pm5Kof3t0WoTHOe0Z2ToubTZUdlOkjR34', + }, + }); + + if (!response.ok) { + throw new Error(`HTTP Error: ${response.status}`); + } + + const data = await response.json(); + console.log(`Fetched stocks for page ${pageNumber}:`, data); + + if (!Array.isArray(data) || data.length === 0) { + setHasMore(false); // No more data to load + return; + } + + setStocks((prevStocks) => [...prevStocks, ...data]); // Append new stocks to the list + } catch (error) { + console.error('Error fetching stocks:', error); + Alert.alert('Error', 'Unable to fetch stocks. Please try again later.'); + } finally { + setLoading(false); + setLoadingMore(false); + } + }; + + useEffect(() => { + fetchStocks(); // Load the first page on component mount + }, []); + + const loadMoreStocks = () => { + if (loadingMore || !hasMore) return; + + setLoadingMore(true); + setPage((prevPage) => { + const nextPage = prevPage + 1; + fetchStocks(nextPage); + return nextPage; + }); + }; const renderStockItem = ({ item }) => ( setSelectedStock(item)} > - {item.code} - {item.name} - ${item.price.toFixed(2)} + {item.symbol || 'N/A'} + {item.name || 'No Name'} + + ${parseFloat(item.price || 0).toFixed(2)} + ); - return ( - - {/* Header with Categories */} - - {Object.keys(mockStocks).map((category) => ( - { - setActiveCategory(category); - setSelectedStock(null); - }} - > - {category} - - ))} + if (loading) { + return ( + + + ); + } + return ( + {/* Stock List */} - - item.code} - /> - + item.id.toString()} + onEndReached={loadMoreStocks} // Trigger pagination when reaching the end + onEndReachedThreshold={0.5} // Adjust how close to the bottom the user needs to be to trigger + ListFooterComponent={ + loadingMore && ( + + + + ) + } + /> {/* Stock Details */} {selectedStock && ( - {selectedStock.code} - {selectedStock.name} - Price: ${selectedStock.price.toFixed(2)} - {selectedStock.about} + {selectedStock.symbol || 'No Symbol'} + {selectedStock.name || 'No Name'} + + Price: ${parseFloat(selectedStock.price || 0).toFixed(2)} + )} @@ -153,30 +115,6 @@ const styles = StyleSheet.create({ flex: 1, backgroundColor: '#F5F5F5', }, - header: { - flexDirection: 'row', - justifyContent: 'space-around', - paddingVertical: 10, - backgroundColor: '#FFFFFF', - borderBottomWidth: 1, - borderBottomColor: '#DDDDDD', - }, - categoryButton: { - padding: 10, - borderRadius: 20, - backgroundColor: '#E0E0E0', - }, - activeCategory: { - backgroundColor: '#007AFF', - }, - categoryText: { - color: '#FFFFFF', - fontWeight: 'bold', - }, - listContainer: { - flex: 1, - backgroundColor: '#FFFFFF', - }, stockItem: { flexDirection: 'row', justifyContent: 'space-between', @@ -195,6 +133,10 @@ const styles = StyleSheet.create({ color: '#007AFF', fontWeight: 'bold', }, + footer: { + padding: 10, + alignItems: 'center', + }, detailsContainer: { padding: 20, backgroundColor: '#FFFFFF', @@ -216,11 +158,6 @@ const styles = StyleSheet.create({ marginTop: 5, color: '#555555', }, - detailsAbout: { - fontSize: 14, - marginTop: 10, - color: '#666666', - }, }); export default Markets; From d633f8cd6e2a5170145e22e9326e2f08a7bd50f2 Mon Sep 17 00:00:00 2001 From: furkansenkal <72168386+furkansenkal@users.noreply.github.com> Date: Sun, 15 Dec 2024 19:33:43 +0300 Subject: [PATCH 12/18] feat: add stock details --- mobile/package-lock.json | 266 ++++++++++++++++--------------- mobile/package.json | 2 + mobile/src/pages/App.js | 21 ++- mobile/src/pages/Home.js | 2 - mobile/src/pages/StockDetails.js | 238 +++++++++++++++++++++++++++ 5 files changed, 398 insertions(+), 131 deletions(-) create mode 100644 mobile/src/pages/StockDetails.js diff --git a/mobile/package-lock.json b/mobile/package-lock.json index 6b0701c7..017e1cba 100644 --- a/mobile/package-lock.json +++ b/mobile/package-lock.json @@ -12,6 +12,7 @@ "@react-navigation/drawer": "^6.7.2", "@react-navigation/native": "^6.1.18", "@react-navigation/stack": "^6.4.1", + "jwt-decode": "^4.0.0", "react": "18.3.1", "react-native": "0.75.4", "react-native-chart-kit": "^6.12.0", @@ -30,6 +31,7 @@ }, "devDependencies": { "@babel/core": "^7.20.0", + "@babel/plugin-proposal-decorators": "^7.25.9", "@babel/preset-env": "^7.20.0", "@babel/runtime": "^7.20.0", "@react-native/babel-preset": "0.75.4", @@ -62,11 +64,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.7.tgz", - "integrity": "sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", "picocolors": "^1.0.0" }, "engines": { @@ -129,11 +133,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.7.tgz", - "integrity": "sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz", + "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.25.7", + "@babel/parser": "^7.26.2", + "@babel/types": "^7.26.0", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -143,11 +149,12 @@ } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.7.tgz", - "integrity": "sha512-4xwU8StnqnlIhhioZf1tqnVWeQ9pvH/ujS8hRfw/WOza+/a+1qv69BWNy+oY231maTCWgKWhfBU7kDpsds6zAA==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.25.7" + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -181,16 +188,17 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.7.tgz", - "integrity": "sha512-bD4WQhbkx80mAyj/WCm4ZHcF4rDxkoLFO6ph8/5/mQ3z4vAzltQXAmbc7GvVJx5H+lk5Mi5EmbTeox5nMGCsbw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz", + "integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==", + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.7", - "@babel/helper-member-expression-to-functions": "^7.25.7", - "@babel/helper-optimise-call-expression": "^7.25.7", - "@babel/helper-replace-supers": "^7.25.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7", - "@babel/traverse": "^7.25.7", + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/traverse": "^7.25.9", "semver": "^6.3.1" }, "engines": { @@ -232,12 +240,13 @@ } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.7.tgz", - "integrity": "sha512-O31Ssjd5K6lPbTX9AAYpSKrZmLeagt9uwschJd+Ixo6QiRyfpvgtVQp8qrDR9UNFjZ8+DO34ZkdrN+BnPXemeA==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", + "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", + "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.7", - "@babel/types": "^7.25.7" + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -273,20 +282,22 @@ } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.7.tgz", - "integrity": "sha512-VAwcwuYhv/AT+Vfr28c9y6SHzTan1ryqrydSTFGjU0uDJHw3uZ+PduI8plCLkRsDnqK2DMEDmwrOQRsK/Ykjng==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", + "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.25.7" + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.7.tgz", - "integrity": "sha512-eaPZai0PiqCi09pPs3pAFfl/zYgGaE6IdXtYvmf0qlcDTd3WCtO7JWCcRd64e0EQrcYgiHibEZnOGsSY4QSgaw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", + "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -308,13 +319,14 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.7.tgz", - "integrity": "sha512-iy8JhqlUW9PtZkd4pHM96v6BdJ66Ba9yWSE4z0W4TvSZwLBPkyDsiIU3ENe4SmrzRBs76F7rQXTy1lYC49n6Lw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz", + "integrity": "sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==", + "license": "MIT", "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.25.7", - "@babel/helper-optimise-call-expression": "^7.25.7", - "@babel/traverse": "^7.25.7" + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/traverse": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -336,29 +348,32 @@ } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.7.tgz", - "integrity": "sha512-pPbNbchZBkPMD50K0p3JGcFMNLVUCuU/ABybm/PGNj4JiHrpmNyqqCphBk4i19xXtNV0JhldQJJtbSW5aUvbyA==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", + "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", + "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.7", - "@babel/types": "^7.25.7" + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz", - "integrity": "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", - "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -396,26 +411,13 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/highlight": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.7.tgz", - "integrity": "sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==", - "dependencies": { - "@babel/helper-validator-identifier": "^7.25.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/parser": { - "version": "7.25.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.8.tgz", - "integrity": "sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", + "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.25.8" + "@babel/types": "^7.26.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -514,6 +516,24 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.25.9.tgz", + "integrity": "sha512-smkNLL/O1ezy9Nhy4CNosc4Va+1wo5w4gzSZeLe6y6dM4mmHfYOCPolXQPHQxonZCF+ZyebxN9vqOolkYrSn5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/plugin-syntax-decorators": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-proposal-export-default-from": { "version": "7.25.8", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.25.8.tgz", @@ -623,6 +643,22 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.25.9.tgz", + "integrity": "sha512-ryzI0McXUPJnRCvMo4lumIKZUzhYUO/ScI+Mz4YVaTLt04DHNSjEUjKVvbzQjZFLuod/cYEc07mJWhzl6v4DPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-dynamic-import": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", @@ -1880,28 +1916,30 @@ } }, "node_modules/@babel/template": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.7.tgz", - "integrity": "sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.7", - "@babel/parser": "^7.25.7", - "@babel/types": "^7.25.7" + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.7.tgz", - "integrity": "sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz", + "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.7", - "@babel/generator": "^7.25.7", - "@babel/parser": "^7.25.7", - "@babel/template": "^7.25.7", - "@babel/types": "^7.25.7", + "@babel/code-frame": "^7.25.9", + "@babel/generator": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/template": "^7.25.9", + "@babel/types": "^7.25.9", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -1910,13 +1948,13 @@ } }, "node_modules/@babel/types": { - "version": "7.25.8", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.8.tgz", - "integrity": "sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", + "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.7", - "@babel/helper-validator-identifier": "^7.25.7", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -5595,19 +5633,6 @@ "@webgpu/types": "0.1.21" } }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/char-regex": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", @@ -6877,6 +6902,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, "engines": { "node": ">=0.8.0" } @@ -8247,14 +8273,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, "node_modules/has-property-descriptors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", @@ -10826,6 +10844,15 @@ "node": ">=4.0" } }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -12673,6 +12700,7 @@ "version": "5.12.5", "resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-5.12.5.tgz", "integrity": "sha512-Qpqd1g9PClmjGj/Dkr1htAwt8cTZ3SCHVmhttxRuG/QML7KzHm5ArLNgR7vz5dW1EwJqTmyl/3gd6gnrtw90mw==", + "license": "MIT", "dependencies": { "@callstack/react-theme-provider": "^3.0.9", "color": "^3.1.2", @@ -12706,6 +12734,7 @@ "version": "3.16.2", "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.16.2.tgz", "integrity": "sha512-Jk8y+iOLcK3J8YK3Qj/U+zclwfetgM1fFhlYaxFrJ5TPvuwdRG5YY1pvO91FcZ3C1+0meGHR6BZGl9d/Z0xh3Q==", + "license": "MIT", "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.0.0-0", "@babel/plugin-transform-class-properties": "^7.0.0-0", @@ -13976,17 +14005,6 @@ "resolved": "https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-9.2.1.tgz", "integrity": "sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==" }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -14145,14 +14163,6 @@ "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==" }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/mobile/package.json b/mobile/package.json index 21110f62..d916fffa 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -14,6 +14,7 @@ "@react-navigation/drawer": "^6.7.2", "@react-navigation/native": "^6.1.18", "@react-navigation/stack": "^6.4.1", + "jwt-decode": "^4.0.0", "react": "18.3.1", "react-native": "0.75.4", "react-native-chart-kit": "^6.12.0", @@ -32,6 +33,7 @@ }, "devDependencies": { "@babel/core": "^7.20.0", + "@babel/plugin-proposal-decorators": "^7.25.9", "@babel/preset-env": "^7.20.0", "@babel/runtime": "^7.20.0", "@react-native/babel-preset": "0.75.4", diff --git a/mobile/src/pages/App.js b/mobile/src/pages/App.js index c0328ed8..1dcf309f 100644 --- a/mobile/src/pages/App.js +++ b/mobile/src/pages/App.js @@ -18,6 +18,8 @@ import Markets from './Markets'; import Community from './Community'; import Post from './Post'; import CreatePost from './CreatePost'; +import StockDetails from './StockDetails'; + import { AuthProvider, useAuth } from './context/AuthContext'; @@ -71,6 +73,22 @@ const PostStack = () => { ) } + +const MarketsStack = () => ( + + }} + /> + + +); + const DrawerNavigator = () => { const { username, userId } = useAuth(); @@ -104,7 +122,8 @@ const DrawerNavigator = () => { )} { setLatestPosts(data); } catch (error) { console.error('Error fetching latest posts:', error); - }finally { - setLoading(false); } }; diff --git a/mobile/src/pages/StockDetails.js b/mobile/src/pages/StockDetails.js new file mode 100644 index 00000000..1b3c2bb1 --- /dev/null +++ b/mobile/src/pages/StockDetails.js @@ -0,0 +1,238 @@ +import React, { useState, useEffect } from 'react'; +import { View, Text, StyleSheet, ActivityIndicator, ScrollView, Alert } from 'react-native'; + +const StockDetails = ({ route }) => { + const { id } = route.params; // Get the stock ID from navigation params + const [stockDetails, setStockDetails] = useState(null); + const [loading, setLoading] = useState(true); + + const fetchStockDetails = async () => { + const url = `http://159.223.28.163:30002/stocks/${id}/`; + + try { + const response = await fetch(url, { + method: 'GET', + headers: { + accept: 'application/json', + Authorization: 'Basic ZnVya2Fuc2Vua2FsOkxvc29sdmlkYWRvcy41NQ==', + 'X-CSRFToken': 'HN4gYGlxSnwtGKK91OG9c6WC6gr8091Pm5Kof3t0WoTHOe0Z2ToubTZUdlOkjR34', + }, + }); + + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + + const data = await response.json(); + console.log('Fetched Stock Details:', data); // Debugging log + setStockDetails(data); + } catch (error) { + console.error('Error fetching stock details:', error); + Alert.alert('Error', 'Unable to fetch stock details. Please try again later.'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchStockDetails(); + }, [id]); + + if (loading) { + return ( + + + + ); + } + + if (!stockDetails) { + return ( + + Unable to load stock details. + + ); + } + + const { + name, + symbol, + currency, + detail, + } = stockDetails; + + const { + currentPrice, + marketCap, + fiftyTwoWeekHigh, + fiftyTwoWeekLow, + volume, + averageVolume, + open, + dayLow, + dayHigh, + sector, + industry, + longBusinessSummary, + } = detail || {}; + + const currencyCode = currency?.code || 'N/A'; + + return ( + + {/* Stock Overview */} + + {name || 'N/A'} + {symbol || 'N/A'} + + + + {currencyCode} {currentPrice || 'N/A'} + + Current Price + + + {/* Stock Highlights */} + + Stock Highlights + + Market Cap: {currencyCode} {marketCap || 'N/A'} + + + 52-Week High: {currencyCode} {fiftyTwoWeekHigh || 'N/A'} + + + 52-Week Low: {currencyCode} {fiftyTwoWeekLow || 'N/A'} + + + Volume: {volume || 'N/A'} + + + Average Volume: {averageVolume || 'N/A'} + + + + {/* Financial Highlights */} + + Financial Highlights + + Open: {currencyCode} {open || 'N/A'} + + + Day Low: {currencyCode} {dayLow || 'N/A'} + + + Day High: {currencyCode} {dayHigh || 'N/A'} + + + Sector: {sector || 'N/A'} + + + Industry: {industry || 'N/A'} + + + + {/* Business Summary */} + {longBusinessSummary && ( + + Business Summary + {longBusinessSummary} + + )} + + ); +}; + +const styles = StyleSheet.create({ + loaderContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + errorContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + errorText: { + fontSize: 18, + color: 'red', + textAlign: 'center', + }, + container: { + padding: 16, + backgroundColor: '#f4f4f4', + }, + scrollContent: { + paddingBottom: 40, // Add padding to ensure space for the last element + }, + header: { + marginBottom: 20, + borderBottomWidth: 2, + borderBottomColor: '#007AFF', + paddingBottom: 10, + }, + title: { + fontSize: 28, + fontWeight: 'bold', + color: '#000', + marginBottom: 5, + }, + symbol: { + fontSize: 20, + color: '#555', + marginTop: 5, + }, + priceContainer: { + alignItems: 'center', + marginBottom: 20, + }, + currentPrice: { + fontSize: 32, + fontWeight: 'bold', + color: '#007AFF', + }, + subDetail: { + fontSize: 16, + color: '#777', + marginTop: 5, + }, + section: { + marginTop: 20, + paddingVertical: 15, + borderRadius: 10, + backgroundColor: '#fff', + paddingHorizontal: 20, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3, + }, + lastSection: { + marginBottom: 40, // Add extra margin for the last section + }, + sectionTitle: { + fontSize: 22, + fontWeight: 'bold', + color: '#007AFF', + marginBottom: 10, + }, + detail: { + fontSize: 16, + color: '#333', + marginBottom: 8, + }, + detailLabel: { + fontWeight: 'bold', + color: '#000', + }, + summary: { + fontSize: 16, + color: '#555', + lineHeight: 24, + marginTop: 10, + }, +}); + +export default StockDetails; From c0603dc6e6b6699fe6940007bb8282930f5c5eb1 Mon Sep 17 00:00:00 2001 From: furkansenkal <72168386+furkansenkal@users.noreply.github.com> Date: Sun, 15 Dec 2024 19:35:30 +0300 Subject: [PATCH 13/18] style: change markets style --- mobile/src/pages/Markets.js | 127 +++++++++++++++++++----------------- 1 file changed, 66 insertions(+), 61 deletions(-) diff --git a/mobile/src/pages/Markets.js b/mobile/src/pages/Markets.js index 9bf9bf8d..888b5549 100644 --- a/mobile/src/pages/Markets.js +++ b/mobile/src/pages/Markets.js @@ -1,8 +1,15 @@ import React, { useState, useEffect } from 'react'; -import { View, Text, FlatList, TouchableOpacity, StyleSheet, ActivityIndicator, Alert } from 'react-native'; +import { + View, + Text, + FlatList, + TouchableOpacity, + StyleSheet, + ActivityIndicator, + Alert, +} from 'react-native'; -const Markets = () => { - const [selectedStock, setSelectedStock] = useState(null); +const Markets = ({ navigation }) => { const [stocks, setStocks] = useState([]); // Store all loaded stocks const [loading, setLoading] = useState(true); const [loadingMore, setLoadingMore] = useState(false); // Loading state for pagination @@ -11,28 +18,39 @@ const Markets = () => { const fetchStocks = async (pageNumber = 1) => { try { - const response = await fetch(`http://159.223.28.163:30002/stocks/?page=${pageNumber}`, { - method: 'GET', - headers: { - accept: 'application/json', - Authorization: 'Basic ZnVya2Fuc2Vua2FsOkxvc29sdmlkYWRvcy41NQ==', - 'X-CSRFToken': 'HN4gYGlxSnwtGKK91OG9c6WC6gr8091Pm5Kof3t0WoTHOe0Z2ToubTZUdlOkjR34', - }, - }); + const response = await fetch( + `http://159.223.28.163:30002/stocks/?page=${pageNumber}`, + { + method: 'GET', + headers: { + accept: 'application/json', + Authorization: 'Basic ZnVya2Fuc2Vua2FsOkxvc29sdmlkYWRvcy41NQ==', + 'X-CSRFToken': 'HN4gYGlxSnwtGKK91OG9c6WC6gr8091Pm5Kof3t0WoTHOe0Z2ToubTZUdlOkjR34', + }, + } + ); if (!response.ok) { throw new Error(`HTTP Error: ${response.status}`); } const data = await response.json(); - console.log(`Fetched stocks for page ${pageNumber}:`, data); + console.log("Fetched data IDs:", data.map(item => item.id)); if (!Array.isArray(data) || data.length === 0) { setHasMore(false); // No more data to load return; } - setStocks((prevStocks) => [...prevStocks, ...data]); // Append new stocks to the list + // Deduplicate stocks by ID + setStocks((prevStocks) => { + const combinedStocks = [...prevStocks, ...data]; + const uniqueStocks = Array.from( + new Map(combinedStocks.map((stock) => [stock.id, stock])).values() + ); + console.log("Unique stock IDs:", uniqueStocks.map(stock => stock.id)); + return uniqueStocks; + }); } catch (error) { console.error('Error fetching stocks:', error); Alert.alert('Error', 'Unable to fetch stocks. Please try again later.'); @@ -59,20 +77,22 @@ const Markets = () => { const renderStockItem = ({ item }) => ( setSelectedStock(item)} + style={styles.stockCard} + onPress={() => navigation.navigate('StockDetails', { id: item.id })} > - {item.symbol || 'N/A'} - {item.name || 'No Name'} + + {item.symbol || 'N/A'} + {item.name || 'No Name'} + - ${parseFloat(item.price || 0).toFixed(2)} + {parseFloat(item.price || 0).toFixed(2)} {item.currency?.code || ''} ); if (loading) { return ( - + ); @@ -84,7 +104,7 @@ const Markets = () => { item.id.toString()} + keyExtractor={(item) => item.id.toString()} // Ensure unique keys onEndReached={loadMoreStocks} // Trigger pagination when reaching the end onEndReachedThreshold={0.5} // Adjust how close to the bottom the user needs to be to trigger ListFooterComponent={ @@ -95,17 +115,6 @@ const Markets = () => { ) } /> - - {/* Stock Details */} - {selectedStock && ( - - {selectedStock.symbol || 'No Symbol'} - {selectedStock.name || 'No Name'} - - Price: ${parseFloat(selectedStock.price || 0).toFixed(2)} - - - )} ); }; @@ -113,51 +122,47 @@ const Markets = () => { const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: '#F5F5F5', + backgroundColor: '#F4F4F4', + padding: 10, + }, + loaderContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', }, - stockItem: { + stockCard: { + backgroundColor: '#FFFFFF', + borderRadius: 10, + padding: 15, + marginVertical: 8, flexDirection: 'row', justifyContent: 'space-between', - padding: 15, - borderBottomWidth: 1, - borderBottomColor: '#EEEEEE', + alignItems: 'center', + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3, }, stockCode: { + fontSize: 18, fontWeight: 'bold', - color: '#333333', + color: '#007AFF', }, stockName: { - color: '#555555', + fontSize: 14, + color: '#333333', + marginTop: 4, }, stockPrice: { - color: '#007AFF', - fontWeight: 'bold', + fontSize: 16, + fontWeight: '600', + color: '#4CAF50', }, footer: { padding: 10, alignItems: 'center', }, - detailsContainer: { - padding: 20, - backgroundColor: '#FFFFFF', - borderTopWidth: 1, - borderTopColor: '#DDDDDD', - }, - detailsTitle: { - fontSize: 18, - fontWeight: 'bold', - marginBottom: 10, - color: '#007AFF', - }, - detailsName: { - fontSize: 16, - color: '#333333', - }, - detailsPrice: { - fontSize: 14, - marginTop: 5, - color: '#555555', - }, }); export default Markets; From 985ab6c6f762a0deb20070617dff46d0fcdcb16b Mon Sep 17 00:00:00 2001 From: furkansenkal <72168386+furkansenkal@users.noreply.github.com> Date: Sun, 15 Dec 2024 19:51:14 +0300 Subject: [PATCH 14/18] feat: add stock search to markets page --- mobile/src/pages/Markets.js | 127 +++++++++++++++++++++++++++++++----- 1 file changed, 109 insertions(+), 18 deletions(-) diff --git a/mobile/src/pages/Markets.js b/mobile/src/pages/Markets.js index 888b5549..36a9deb1 100644 --- a/mobile/src/pages/Markets.js +++ b/mobile/src/pages/Markets.js @@ -2,17 +2,22 @@ import React, { useState, useEffect } from 'react'; import { View, Text, + TextInput, FlatList, TouchableOpacity, StyleSheet, ActivityIndicator, Alert, } from 'react-native'; +import Icon from 'react-native-vector-icons/MaterialIcons'; // Using MaterialIcons for the magnifying glass const Markets = ({ navigation }) => { const [stocks, setStocks] = useState([]); // Store all loaded stocks const [loading, setLoading] = useState(true); const [loadingMore, setLoadingMore] = useState(false); // Loading state for pagination + const [searchLoading, setSearchLoading] = useState(false); // Loading state for search + const [searchQuery, setSearchQuery] = useState(''); // Current search input + const [searchResults, setSearchResults] = useState([]); // Search results const [page, setPage] = useState(1); // Current page number const [hasMore, setHasMore] = useState(true); // To check if there are more pages to load @@ -29,28 +34,27 @@ const Markets = ({ navigation }) => { }, } ); - + if (!response.ok) { throw new Error(`HTTP Error: ${response.status}`); } - + const data = await response.json(); - console.log("Fetched data IDs:", data.map(item => item.id)); - - if (!Array.isArray(data) || data.length === 0) { + + // Filter out stocks with a price of -1 + const filteredData = data.filter((stock) => stock.price !== -1); + + console.log(`Fetched stocks for page ${pageNumber}:`, filteredData); + + if (!Array.isArray(filteredData) || filteredData.length === 0) { setHasMore(false); // No more data to load return; } - - // Deduplicate stocks by ID - setStocks((prevStocks) => { - const combinedStocks = [...prevStocks, ...data]; - const uniqueStocks = Array.from( - new Map(combinedStocks.map((stock) => [stock.id, stock])).values() - ); - console.log("Unique stock IDs:", uniqueStocks.map(stock => stock.id)); - return uniqueStocks; - }); + + setStocks((prevStocks) => [ + ...prevStocks, + ...filteredData.filter((stock) => !prevStocks.find((s) => s.id === stock.id)), // Ensure unique stocks + ]); } catch (error) { console.error('Error fetching stocks:', error); Alert.alert('Error', 'Unable to fetch stocks. Please try again later.'); @@ -59,6 +63,49 @@ const Markets = ({ navigation }) => { setLoadingMore(false); } }; + + const searchStocks = async (query) => { + if (!query) { + setSearchResults([]); // Clear search results if query is empty + return; + } + + setSearchLoading(true); + + try { + const response = await fetch('http://159.223.28.163:30002/stocks/search/', { + method: 'POST', + headers: { + accept: 'application/json', + Authorization: 'Basic ZnVya2Fuc2Vua2FsOkxvc29sdmlkYWRvcy41NQ==', + 'Content-Type': 'application/json', + 'X-CSRFToken': 'xGhS17H7qedbZRMF0ULpzKQhKe6mG11WcYX0iuPAufAp7l2v1ZtKyxTzRjtyZJ3b', + }, + body: JSON.stringify({ + pattern: query, + limit: 10, + }), + }); + + if (!response.ok) { + throw new Error(`HTTP Error: ${response.status}`); + } + + const data = await response.json(); + + // Filter out stocks with a price of -1 + const filteredData = data.filter((stock) => stock.price !== -1); + + console.log('Search results:', filteredData); + + setSearchResults(filteredData); + } catch (error) { + console.error('Error searching stocks:', error); + Alert.alert('Error', 'Unable to search stocks. Please try again later.'); + } finally { + setSearchLoading(false); + } + }; useEffect(() => { fetchStocks(); // Load the first page on component mount @@ -100,15 +147,35 @@ const Markets = ({ navigation }) => { return ( + {/* Search Bar */} + + + { + setSearchQuery(text); + searchStocks(text); // Trigger search on input change + }} + /> + + + {/* Loading Indicator for Search */} + {searchLoading && ( + + )} + {/* Stock List */} item.id.toString()} // Ensure unique keys - onEndReached={loadMoreStocks} // Trigger pagination when reaching the end + onEndReached={searchQuery ? null : loadMoreStocks} // Disable pagination during search onEndReachedThreshold={0.5} // Adjust how close to the bottom the user needs to be to trigger ListFooterComponent={ - loadingMore && ( + loadingMore && !searchQuery && ( @@ -125,6 +192,30 @@ const styles = StyleSheet.create({ backgroundColor: '#F4F4F4', padding: 10, }, + searchContainer: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: '#FFFFFF', + borderRadius: 8, + paddingHorizontal: 10, + marginBottom: 10, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 2, + }, + searchIcon: { + marginRight: 10, + }, + searchInput: { + fontSize: 16, + color: '#333', + flex: 1, + }, + searchLoader: { + marginBottom: 10, + }, loaderContainer: { flex: 1, justifyContent: 'center', From 3e1876e28b6bef63206abf3f13dc812d64c8c179 Mon Sep 17 00:00:00 2001 From: furkansenkal <72168386+furkansenkal@users.noreply.github.com> Date: Mon, 16 Dec 2024 13:05:36 +0300 Subject: [PATCH 15/18] feat: add portfolio page --- mobile/src/pages/App.js | 43 +++- mobile/src/pages/Portfolio.js | 258 ++++++++++++++++++++++++ mobile/src/pages/context/AuthContext.js | 4 + 3 files changed, 304 insertions(+), 1 deletion(-) create mode 100644 mobile/src/pages/Portfolio.js diff --git a/mobile/src/pages/App.js b/mobile/src/pages/App.js index 1dcf309f..0f370fab 100644 --- a/mobile/src/pages/App.js +++ b/mobile/src/pages/App.js @@ -19,6 +19,9 @@ import Community from './Community'; import Post from './Post'; import CreatePost from './CreatePost'; import StockDetails from './StockDetails'; +import Portfolio from './Portfolio'; +import PortfolioDetails from './PortfolioDetails'; +import CreatePortfolio from './CreatePortfolio'; import { AuthProvider, useAuth } from './context/AuthContext'; @@ -89,6 +92,31 @@ const MarketsStack = () => ( ); +const PortfolioStack = () => ( + + }} + /> + + + + +); + const DrawerNavigator = () => { const { username, userId } = useAuth(); @@ -134,8 +162,21 @@ const DrawerNavigator = () => { + { userId ? ( + + ) : ( + + )} ); diff --git a/mobile/src/pages/Portfolio.js b/mobile/src/pages/Portfolio.js new file mode 100644 index 00000000..6f01666d --- /dev/null +++ b/mobile/src/pages/Portfolio.js @@ -0,0 +1,258 @@ +import React, { useState, useCallback } from 'react'; +import { + View, + Text, + FlatList, + StyleSheet, + Alert, + ActivityIndicator, + TouchableOpacity, +} from 'react-native'; +import { useAuth } from './context/AuthContext'; +import MaterialIcons from 'react-native-vector-icons/MaterialIcons'; +import { useFocusEffect } from '@react-navigation/native'; + +const Portfolio = ({ navigation }) => { + const { userId, accessToken } = useAuth(); + const [portfolios, setPortfolios] = useState([]); + const [fetchingPortfolios, setFetchingPortfolios] = useState(true); + + // Fetch portfolios from the API + const fetchUserPortfolios = async () => { + setFetchingPortfolios(true); + + try { + const response = await fetch( + `http://159.223.28.163:30002/portfolios/portfolios-by-user/${userId}/`, + { + method: 'GET', + headers: { + accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + } + ); + + if (!response.ok) { + throw new Error(`HTTP Error: ${response.status}`); + } + + const data = await response.json(); + + const enrichedPortfolios = await Promise.all( + data.map(async (portfolio) => { + const stocksWithDetails = await Promise.all( + portfolio.stocks.map(async (stockItem) => { + const stockDetails = await fetchStockDetails(stockItem.stock); + return { + ...stockItem, + currentPrice: stockDetails?.price || 0, + name: stockDetails?.name || 'N/A', + symbol: stockDetails?.symbol || 'N/A', + currency: stockDetails?.currency?.code || 'N/A', + }; + }) + ); + + const totalInvestment = stocksWithDetails.reduce( + (total, stock) => total + parseFloat(stock.price_bought) * stock.quantity, + 0 + ); + + const currentValue = stocksWithDetails.reduce( + (total, stock) => total + stock.currentPrice * stock.quantity, + 0 + ); + + const totalProfitOrLoss = currentValue - totalInvestment; + const totalProfitOrLossPercentage = + totalInvestment === 0 + ? 'N/A' + : ((totalProfitOrLoss / totalInvestment) * 100).toFixed(2); + + return { + ...portfolio, + stocks: stocksWithDetails, + totalProfitOrLoss, + totalProfitOrLossPercentage, + }; + }) + ); + + setPortfolios(enrichedPortfolios); + } catch (error) { + console.error('Error fetching portfolios:', error); + Alert.alert('Error', 'Unable to fetch portfolios. Please try again later.'); + } finally { + setFetchingPortfolios(false); + } + }; + + // Fetch stock details by ID + const fetchStockDetails = async (stockId) => { + try { + const response = await fetch(`http://159.223.28.163:30002/stocks/${stockId}/`, { + method: 'GET', + headers: { + accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }); + + if (!response.ok) { + throw new Error(`HTTP Error: ${response.status}`); + } + + return await response.json(); + } catch (error) { + console.error(`Error fetching stock details for stockId ${stockId}:`, error); + return null; + } + }; + + // Use useFocusEffect to fetch portfolios when the screen is focused + useFocusEffect( + useCallback(() => { + if (userId && accessToken) { + fetchUserPortfolios(); + } + }, [userId, accessToken]) + ); + + const renderPortfolio = ({ item }) => { + const isProfit = item.totalProfitOrLoss >= 0; + return ( + navigation.navigate('PortfolioDetails', { portfolio: item })} + > + {item.name} + + {isProfit ? 'Total Profit' : 'Total Loss'}: {parseFloat(item.totalProfitOrLoss).toFixed(2)}{' '} + {item.stocks[0]?.currency || 'N/A'} ({item.totalProfitOrLossPercentage}%) + + + stock.stock.toString()} + renderItem={({ item: stock }) => ( + navigation.navigate('StockDetails', { id: stock.stock })} + > + {stock.symbol} + + {stock.name}: {stock.quantity} shares + + + Bought at: {parseFloat(stock.price_bought).toFixed(2)} {stock.currency} | Current:{' '} + {stock.currentPrice.toFixed(2)} {stock.currency} + + + )} + /> + + ); + }; + + return ( + + My Portfolios + {fetchingPortfolios ? ( + + ) : portfolios.length > 0 ? ( + item.id.toString()} + renderItem={renderPortfolio} + /> + ) : ( + You have no portfolios yet. + )} + + {/* Floating Action Button */} + navigation.navigate('CreatePortfolio')} + > + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + padding: 16, + backgroundColor: '#F5F5F5', + }, + header: { + fontSize: 22, + fontWeight: 'bold', + color: '#000', + marginBottom: 16, + }, + portfolioCard: { + backgroundColor: '#FFFFFF', + borderRadius: 8, + padding: 16, + marginBottom: 12, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 2, + }, + portfolioTitle: { + fontSize: 18, + fontWeight: 'bold', + color: '#000', + marginBottom: 8, + }, + totalProfitOrLoss: { + fontSize: 16, + fontWeight: 'bold', + marginBottom: 8, + }, + stockCard: { + backgroundColor: '#EFEFEF', + padding: 10, + marginRight: 10, + borderRadius: 8, + }, + stockSymbol: { + fontSize: 14, + fontWeight: 'bold', + color: '#000', + marginBottom: 4, + }, + stockDetail: { + fontSize: 12, + color: '#333', + }, + fab: { + alignSelf: 'center', + backgroundColor: '#0077B6', + width: 60, + height: 60, + borderRadius: 30, + alignItems: 'center', + justifyContent: 'center', + marginTop: 20, + }, + message: { + fontSize: 16, + textAlign: 'center', + marginVertical: 20, + color: '#555', + }, +}); + +export default Portfolio; diff --git a/mobile/src/pages/context/AuthContext.js b/mobile/src/pages/context/AuthContext.js index f41a1dcd..ef582f5d 100644 --- a/mobile/src/pages/context/AuthContext.js +++ b/mobile/src/pages/context/AuthContext.js @@ -24,11 +24,15 @@ if (!refreshToken) { console.warn('No refresh token available for logout.'); setUsername(null); + setUserId(null); setRefreshToken(null); + setAccessToken(null); return; }else{ setUsername(null); + setUserId(null); setRefreshToken(null); + setAccessToken(null); return; } From b053553112a5429a28c1e9096c8e83a505977a8b Mon Sep 17 00:00:00 2001 From: furkansenkal <72168386+furkansenkal@users.noreply.github.com> Date: Mon, 16 Dec 2024 13:07:00 +0300 Subject: [PATCH 16/18] feat: add create portfolio --- mobile/src/pages/CreatePortfolio.js | 355 ++++++++++++++++++++++++++++ 1 file changed, 355 insertions(+) create mode 100644 mobile/src/pages/CreatePortfolio.js diff --git a/mobile/src/pages/CreatePortfolio.js b/mobile/src/pages/CreatePortfolio.js new file mode 100644 index 00000000..0a26a6fc --- /dev/null +++ b/mobile/src/pages/CreatePortfolio.js @@ -0,0 +1,355 @@ +import React, { useState } from 'react'; +import { + View, + Text, + TextInput, + StyleSheet, + TouchableOpacity, + Alert, + ActivityIndicator, + ScrollView, + FlatList, +} from 'react-native'; +import { useAuth } from './context/AuthContext'; +import MaterialIcons from 'react-native-vector-icons/MaterialIcons'; +import { Swipeable } from 'react-native-gesture-handler'; + +const CreatePortfolio = ({ navigation }) => { + const { accessToken } = useAuth(); + const [name, setName] = useState(''); + const [description, setDescription] = useState(''); + const [searchTerm, setSearchTerm] = useState(''); + const [searchResults, setSearchResults] = useState([]); + const [selectedStocks, setSelectedStocks] = useState([]); + const [loading, setLoading] = useState(false); + const [searchLoading, setSearchLoading] = useState(false); + + const searchStocks = async () => { + if (!searchTerm.trim()) return; + + setSearchLoading(true); + + try { + const response = await fetch('http://159.223.28.163:30002/stocks/search/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ + pattern: searchTerm, + limit: 10, + }), + }); + + if (!response.ok) { + throw new Error(`HTTP Error: ${response.status}`); + } + + const data = await response.json(); + setSearchResults(data); + } catch (error) { + console.error('Error searching stocks:', error); + Alert.alert('Error', 'Unable to search stocks. Please try again later.'); + } finally { + setSearchLoading(false); + } + }; + + const addStockToPortfolio = (stock) => { + if (selectedStocks.some((s) => s.id === stock.id)) { + Alert.alert('Warning', 'This stock is already added.'); + return; + } + + setSelectedStocks((prev) => [ + ...prev, + { + ...stock, + price_bought: stock.price === -1 ? '0' : stock.price.toString(), + currency: 'TRY', // Fixed currency to TRY + quantity: '', // Default to empty for user input + }, + ]); + }; + + const removeStockFromPortfolio = (stockId) => { + setSelectedStocks((prev) => prev.filter((stock) => stock.id !== stockId)); + }; + + const updateStockDetails = (stockId, field, value) => { + setSelectedStocks((prev) => + prev.map((stock) => + stock.id === stockId ? { ...stock, [field]: value } : stock + ) + ); + }; + + const createPortfolio = async () => { + if (!name.trim()) { + Alert.alert('Error', 'Please enter a name for the portfolio.'); + return; + } + + if (!description.trim()) { + Alert.alert('Error', 'Please enter a description for the portfolio.'); + return; + } + + if (selectedStocks.length === 0) { + Alert.alert('Error', 'Please add at least one stock to the portfolio.'); + return; + } + + const invalidStocks = selectedStocks.some( + (stock) => !stock.price_bought || isNaN(stock.quantity) || stock.quantity <= 0 + ); + if (invalidStocks) { + Alert.alert('Error', 'Please fill in the price and quantity for all selected stocks.'); + return; + } + + const formattedStocks = selectedStocks.map((stock) => ({ + stock: stock.id, + price_bought: stock.price_bought, + quantity: parseInt(stock.quantity, 10), + })); + + setLoading(true); + + try { + const response = await fetch('http://159.223.28.163:30002/portfolios/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ + name, + description, + stocks: formattedStocks, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('Error response:', errorText); + throw new Error(`HTTP Error: ${response.status}`); + } + + Alert.alert('Success', 'Portfolio created successfully!'); + navigation.goBack(); + } catch (error) { + console.error('Error creating portfolio:', error); + Alert.alert('Error', 'Unable to create portfolio. Please try again later.'); + } finally { + setLoading(false); + } + }; + + const renderStockCard = (stock) => { + const renderRightActions = () => ( + removeStockFromPortfolio(stock.id)} + > + + Remove + + ); + + return ( + + + {stock.name} ({stock.symbol}) + updateStockDetails(stock.id, 'price_bought', value)} + /> + updateStockDetails(stock.id, 'quantity', value)} + /> + + + ); + }; + + return ( + + Create Portfolio + + + + + Search and Add Stocks + + + + + + + + {searchLoading ? ( + + ) + : + searchResults.length > 0 ? ( item.id.toString()} + renderItem={({ item }) => ( + addStockToPortfolio(item)} + > + {item.name} ({item.symbol}) + + )} + />): (No stocks found.)} + + Selected Stocks + {selectedStocks.map(renderStockCard)} + + + {loading ? 'Creating...' : 'Create Portfolio'} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + padding: 16, + backgroundColor: '#F9FAFB', + }, + header: { + fontSize: 24, + fontWeight: '700', + color: '#222', + marginBottom: 16, + }, + sectionHeader: { + fontSize: 20, + fontWeight: '700', + color: '#222', + marginTop: 16, + marginBottom: 8, + }, + input: { + borderWidth: 1, + borderColor: '#E5E7EB', + borderRadius: 10, + padding: 10, + marginBottom: 12, + backgroundColor: '#FFFFFF', + color: '#111', + }, + inputSmall: { + borderWidth: 1, + borderColor: '#E5E7EB', + borderRadius: 8, + padding: 8, + marginBottom: 8, + backgroundColor: '#FFFFFF', + color: '#111', + width: '45%', + marginRight: 8, + }, + searchContainer: { + flexDirection: 'row', + alignItems: 'center', + }, + searchInput: { + flex: 1, + }, + searchButton: { + backgroundColor: '#0077B6', + borderRadius: 50, + padding: 10, + marginLeft: 8, + }, + stockResult: { + padding: 12, + backgroundColor: '#F3F4F6', + borderRadius: 10, + marginVertical: 5, + }, + stockResultText: { + fontSize: 16, + color: '#333', + }, + stockCard: { + backgroundColor: '#EFF6FF', + borderRadius: 12, + padding: 12, + marginVertical: 8, + position: 'relative', + }, + swipeDelete: { + backgroundColor: '#EF4444', + justifyContent: 'center', + alignItems: 'center', + width: 80, + borderRadius: 12, + marginVertical: 8, + }, + swipeDeleteText: { + color: '#FFF', + fontSize: 14, + fontWeight: '700', + }, + stockName: { + fontSize: 16, + fontWeight: '600', + color: '#1F2937', + }, + button: { + backgroundColor: '#0077B6', + padding: 15, + borderRadius: 10, + alignItems: 'center', + marginTop: 16, + }, + buttonText: { + color: '#FFF', + fontSize: 16, + fontWeight: '700', + }, + message: { + fontSize: 16, + textAlign: 'center', + marginVertical: 5, + color: '#555', + }, +}); + +export default CreatePortfolio; From 9d1f52c2c669354ed9bd8467fba6b35759ce9140 Mon Sep 17 00:00:00 2001 From: furkansenkal <72168386+furkansenkal@users.noreply.github.com> Date: Mon, 16 Dec 2024 13:07:29 +0300 Subject: [PATCH 17/18] feat: add portfolio details --- mobile/src/pages/PortfolioDetails.js | 226 +++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 mobile/src/pages/PortfolioDetails.js diff --git a/mobile/src/pages/PortfolioDetails.js b/mobile/src/pages/PortfolioDetails.js new file mode 100644 index 00000000..754d96be --- /dev/null +++ b/mobile/src/pages/PortfolioDetails.js @@ -0,0 +1,226 @@ +import React from 'react'; +import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Dimensions } from 'react-native'; +import { PieChart } from 'react-native-chart-kit'; + +const PortfolioDetails = ({ route, navigation }) => { + const { portfolio } = route.params; + + // Predefined colors for the first five slices + const predefinedColors = ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF']; + + // Helper function to generate non-black/white random colors + const generateColor = () => { + let color; + do { + color = `#${Math.floor(Math.random() * 16777215).toString(16)}`; + } while (color === '#000000' || color === '#ffffff'); + return color; + }; + + // Prepare chart data + const chartData = portfolio.stocks.map((stock, index) => ({ + name: stock.name, + val: stock.currentPrice * stock.quantity, + color: index < predefinedColors.length ? predefinedColors[index] : generateColor(), + legendFontColor: '#000', + legendFontSize: 12, + })); + + // Calculate portfolio metrics + const totalInvestment = portfolio.stocks.reduce( + (total, stock) => total + parseFloat(stock.price_bought) * stock.quantity, + 0 + ); + const currentValue = portfolio.stocks.reduce( + (total, stock) => total + stock.currentPrice * stock.quantity, + 0 + ); + const portfolioProfitLoss = currentValue - totalInvestment; + const portfolioProfitLossPercentage = + totalInvestment === 0 + ? 'N/A' + : ((portfolioProfitLoss / totalInvestment) * 100).toFixed(2); + + const isPortfolioProfit = portfolioProfitLoss >= 0; + + // Format creation date + const formattedDate = new Date(portfolio.created_at).toLocaleDateString(); + + return ( + + {portfolio.name} + {portfolio.description} + Created on: {formattedDate} + + {/* Portfolio Metrics */} + + + Total Investment: {totalInvestment.toFixed(2)} {portfolio.stocks[0]?.currency || 'N/A'} + + + Current Value: {currentValue.toFixed(2)} {portfolio.stocks[0]?.currency || 'N/A'} + + + {isPortfolioProfit ? 'Total Profit' : 'Total Loss'}: {portfolioProfitLoss.toFixed(2)}{' '} + {portfolio.stocks[0]?.currency || 'N/A'} ({portfolioProfitLossPercentage}%) + + + + {/* Pie Chart */} + + `rgba(0, 0, 0, ${opacity})`, + }} + accessor="val" + backgroundColor="transparent" + paddingLeft="15" + hasLegend={false} // Prevent default legends + /> + + + {/* Custom Legends Below Chart */} + + {chartData.map((item, index) => ( + + + + {item.val.toFixed(2)} - {item.name} + + + ))} + + + {/* Stocks Section (Vertically Stacked) */} + Stocks + {portfolio.stocks.map((stock) => { + const investment = parseFloat(stock.price_bought) * stock.quantity; + const currentValue = stock.currentPrice * stock.quantity; + const profitLoss = currentValue - investment; + const profitLossPercentage = + investment === 0 ? 'N/A' : ((profitLoss / investment) * 100).toFixed(2); + const isProfit = profitLoss >= 0; + + return ( + navigation.navigate('StockDetails', { id: stock.stock })} + > + {stock.name} + + Quantity: {stock.quantity}, Bought at: {parseFloat(stock.price_bought).toFixed(2)}{' '} + {stock.currency}, Current: {stock.currentPrice.toFixed(2)} {stock.currency} + + + {isProfit ? 'Profit' : 'Loss'}: {profitLoss.toFixed(2)} {stock.currency} ( + {profitLossPercentage}%) + + + ); + })} + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + padding: 16, + backgroundColor: '#F5F5F5', + }, + header: { + fontSize: 22, + fontWeight: 'bold', + color: '#000', + marginBottom: 16, + }, + description: { + fontSize: 16, + color: '#555', + marginBottom: 16, + }, + dateText: { + fontSize: 14, + color: '#777', + marginBottom: 16, + }, + portfolioMetrics: { + marginBottom: 20, + }, + metricText: { + fontSize: 16, + fontWeight: 'bold', + color: '#000', + marginBottom: 8, + }, + chartContainer: { + alignItems: 'center', + marginBottom: 20, + }, + legendContainer: { + flexDirection: 'column', + marginTop: 10, + marginBottom: 20, + }, + legendItem: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 8, + }, + legendColor: { + width: 16, + height: 16, + borderRadius: 8, + marginRight: 8, + }, + legendText: { + fontSize: 14, + color: '#000', + }, + sectionHeader: { + fontSize: 18, + fontWeight: 'bold', + color: '#000', + marginVertical: 10, + }, + stockCard: { + backgroundColor: '#EFEFEF', + padding: 10, + marginVertical: 10, + borderRadius: 8, + }, + stockName: { + fontSize: 16, + fontWeight: 'bold', + color: '#000', + }, + stockDetails: { + fontSize: 14, + color: '#333', + }, + stockProfitLoss: { + fontSize: 14, + fontWeight: 'bold', + }, + bottomSpace: { + height: 20, // Space at the bottom of the page + }, +}); + +export default PortfolioDetails; From 684306c9cfe2e40778ccac3c584f749628cc695a Mon Sep 17 00:00:00 2001 From: furkansenkal <72168386+furkansenkal@users.noreply.github.com> Date: Mon, 16 Dec 2024 18:04:36 +0300 Subject: [PATCH 18/18] feat: add edit/delete portfolio --- mobile/src/pages/PortfolioDetails.js | 481 ++++++++++++++++++++++++--- 1 file changed, 428 insertions(+), 53 deletions(-) diff --git a/mobile/src/pages/PortfolioDetails.js b/mobile/src/pages/PortfolioDetails.js index 754d96be..a01f35fe 100644 --- a/mobile/src/pages/PortfolioDetails.js +++ b/mobile/src/pages/PortfolioDetails.js @@ -1,14 +1,35 @@ -import React from 'react'; -import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Dimensions } from 'react-native'; +import React, { useState } from 'react'; +import { + View, + Text, + TextInput, + StyleSheet, + ScrollView, + Alert, + FlatList, + ActivityIndicator, + TouchableOpacity, + Dimensions, +} from 'react-native'; import { PieChart } from 'react-native-chart-kit'; +import { Swipeable } from 'react-native-gesture-handler'; +import MaterialIcons from 'react-native-vector-icons/MaterialIcons'; +import { useAuth } from './context/AuthContext'; const PortfolioDetails = ({ route, navigation }) => { + const { accessToken } = useAuth(); const { portfolio } = route.params; + const [stocks, setStocks] = useState(portfolio.stocks); + const [fabActive, setFabActive] = useState(false); + const [searchTerm, setSearchTerm] = useState(''); + const [searchResults, setSearchResults] = useState([]); + const [priceBought, setPriceBought] = useState(''); + const [quantity, setQuantity] = useState(''); + const [selectedStock, setSelectedStock] = useState(null); + const [searchLoading, setSearchLoading] = useState(false); - // Predefined colors for the first five slices const predefinedColors = ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF']; - // Helper function to generate non-black/white random colors const generateColor = () => { let color; do { @@ -17,8 +38,7 @@ const PortfolioDetails = ({ route, navigation }) => { return color; }; - // Prepare chart data - const chartData = portfolio.stocks.map((stock, index) => ({ + const chartData = stocks.map((stock, index) => ({ name: stock.name, val: stock.currentPrice * stock.quantity, color: index < predefinedColors.length ? predefinedColors[index] : generateColor(), @@ -26,12 +46,11 @@ const PortfolioDetails = ({ route, navigation }) => { legendFontSize: 12, })); - // Calculate portfolio metrics - const totalInvestment = portfolio.stocks.reduce( + const totalInvestment = stocks.reduce( (total, stock) => total + parseFloat(stock.price_bought) * stock.quantity, 0 ); - const currentValue = portfolio.stocks.reduce( + const currentValue = stocks.reduce( (total, stock) => total + stock.currentPrice * stock.quantity, 0 ); @@ -43,22 +62,245 @@ const PortfolioDetails = ({ route, navigation }) => { const isPortfolioProfit = portfolioProfitLoss >= 0; - // Format creation date const formattedDate = new Date(portfolio.created_at).toLocaleDateString(); + const searchStocks = async () => { + if (!searchTerm.trim()) return; + + setSearchLoading(true); + + try { + const response = await fetch('http://159.223.28.163:30002/stocks/search/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ + pattern: searchTerm, + limit: 10, + }), + }); + + if (!response.ok) { + throw new Error(`HTTP Error: ${response.status}`); + } + + const data = await response.json(); + const filteredData = data.filter((stock) => stock.price !== -1); + setSearchResults(filteredData); + } catch (error) { + console.error('Error searching stocks:', error); + Alert.alert('Error', 'Unable to search stocks. Please try again later.'); + } finally { + setSearchLoading(false); + } + }; + + const addStockToPortfolio = async () => { + // Validate inputs before proceeding + if (!selectedStock) { + Alert.alert('Error', 'Please select a stock.'); + return; + } + if (!priceBought || isNaN(priceBought) || parseFloat(priceBought) <= 0) { + Alert.alert('Error', 'Please enter a valid price bought.'); + return; + } + if (!quantity || isNaN(quantity) || parseInt(quantity, 10) <= 0) { + Alert.alert('Error', 'Please enter a valid quantity.'); + return; + } + + try { + const sanitizedPriceBought = priceBought; // Ensure proper formatting + const sanitizedQuantity = parseInt(quantity, 10); // Ensure proper integer + + const response = await fetch( + 'http://159.223.28.163:30002/portfolio-stocks/add_stock/', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ + portfolio_id: portfolio.id, + stock: selectedStock.id, + price_bought: sanitizedPriceBought, + quantity: sanitizedQuantity, + }), + } + ); + + if (!response.ok) { + throw new Error(`HTTP Error: ${response.status}`); + } + + let newStock = await response.json(); + const stockDetailsResponse = await fetch( + `http://159.223.28.163:30002/stocks/${selectedStock.id}/`, + { + method: 'GET', + headers: { + accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + } + ); + + if (!stockDetailsResponse.ok) { + throw new Error(`HTTP Error: ${stockDetailsResponse.status}`); + } + else{ + console.log("donedone") + } + + const stockDetails = await stockDetailsResponse.json(); + + newStock = { + stock: stockDetails.id, + name: stockDetails.name, + symbol: stockDetails.symbol, + price_bought: sanitizedPriceBought, + quantity: sanitizedQuantity, + currentPrice: parseFloat(stockDetails.price), // Ensure currentPrice is a number + currency: stockDetails.currency.code, + }; + console.log(JSON.stringify(newStock)) + setStocks((prevStocks) => [...prevStocks, newStock]); + Alert.alert('Success', 'Stock added successfully!'); + setFabActive(false); + resetInputFields(); + } catch (error) { + console.error('Error adding stock:', error); + Alert.alert('Error', 'Unable to add stock. Please try again later.'); + } + }; + + const resetInputFields = () => { + setSearchTerm(''); + setSearchResults([]); + setPriceBought(''); + setQuantity(''); + setSelectedStock(null); + }; + const cancelInput = () => { + resetInputFields(); + setFabActive(false); + }; + + const removeStockFromPortfolio = async (stock) => { + try { + const response = await fetch( + 'http://159.223.28.163:30002/portfolio-stocks/remove_stock/', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ + portfolio_id: portfolio.id, + stock: stock.stock, + price_bought: stock.price_bought.toString(), + quantity: stock.quantity, + }), + } + ); + + if (!response.ok) { + throw new Error(`HTTP Error: ${response.status}`); + } + + setStocks((prevStocks) => prevStocks.filter((s) => s.stock !== stock.stock)); + Alert.alert('Success', 'Stock removed successfully!'); + } catch (error) { + console.error('Error removing stock:', error); + Alert.alert('Error', 'Unable to remove stock. Please try again later.'); + } + }; + + const renderRightActions = (stock) => ( + removeStockFromPortfolio(stock)} + > + + Remove + + ); + const renderSelectedStockCard = () => { + if (!selectedStock) return null; + + return ( + + {selectedStock.name} + + Symbol: {selectedStock.symbol}, Current Price: {selectedStock.price} + + + ); + }; + const renderStockCard = (stock) => { + const investment = parseFloat(stock.price_bought) * stock.quantity; + const currentValue = stock.currentPrice * stock.quantity; + const profitLoss = currentValue - investment; + const profitLossPercentage = + investment === 0 ? 'N/A' : ((profitLoss / investment) * 100).toFixed(2); + const isProfit = profitLoss >= 0; + + return ( + renderRightActions(stock)} + > + navigation.navigate('StockDetails', { id: stock.stock })} + > + + {stock.name} + + Quantity: {stock.quantity}, Bought at: {parseFloat(stock.price_bought).toFixed(2)}{' '} + {stock.currency}, Current: {parseFloat(stock.currentPrice).toFixed(2)} {stock.currency} + + + {isProfit ? 'Profit' : 'Loss'}: {profitLoss.toFixed(2)} {stock.currency} ( + {profitLossPercentage}%) + + + + + ); + }; + return ( - {portfolio.name} + + {portfolio.name} + {}} style={styles.trashIcon}> + + + + {portfolio.description} Created on: {formattedDate} - {/* Portfolio Metrics */} - Total Investment: {totalInvestment.toFixed(2)} {portfolio.stocks[0]?.currency || 'N/A'} + Total Investment: {totalInvestment.toFixed(2)} {stocks[0]?.currency || 'N/A'} - Current Value: {currentValue.toFixed(2)} {portfolio.stocks[0]?.currency || 'N/A'} + Current Value: {currentValue.toFixed(2)} {stocks[0]?.currency || 'N/A'} { ]} > {isPortfolioProfit ? 'Total Profit' : 'Total Loss'}: {portfolioProfitLoss.toFixed(2)}{' '} - {portfolio.stocks[0]?.currency || 'N/A'} ({portfolioProfitLossPercentage}%) + {stocks[0]?.currency || 'N/A'} ({portfolioProfitLossPercentage}%) - {/* Pie Chart */} `rgba(0, 0, 0, ${opacity})`, @@ -83,11 +324,10 @@ const PortfolioDetails = ({ route, navigation }) => { accessor="val" backgroundColor="transparent" paddingLeft="15" - hasLegend={false} // Prevent default legends + hasLegend={false} /> - {/* Custom Legends Below Chart */} {chartData.map((item, index) => ( @@ -99,40 +339,82 @@ const PortfolioDetails = ({ route, navigation }) => { ))} - {/* Stocks Section (Vertically Stacked) */} Stocks - {portfolio.stocks.map((stock) => { - const investment = parseFloat(stock.price_bought) * stock.quantity; - const currentValue = stock.currentPrice * stock.quantity; - const profitLoss = currentValue - investment; - const profitLossPercentage = - investment === 0 ? 'N/A' : ((profitLoss / investment) * 100).toFixed(2); - const isProfit = profitLoss >= 0; - - return ( - navigation.navigate('StockDetails', { id: stock.stock })} - > - {stock.name} - - Quantity: {stock.quantity}, Bought at: {parseFloat(stock.price_bought).toFixed(2)}{' '} - {stock.currency}, Current: {stock.currentPrice.toFixed(2)} {stock.currency} - - - {isProfit ? 'Profit' : 'Loss'}: {profitLoss.toFixed(2)} {stock.currency} ( - {profitLossPercentage}%) - + {stocks.map(renderStockCard)} + + {fabActive && ( + + + + + - ); - })} + + {/* Show Loading Indicator */} + {searchLoading && ( + + )} + {searchResults.length > 0 && ( + item.id.toString()} + renderItem={({ item }) => ( + { + setSelectedStock(item); + setPriceBought(item.price.toString()); + setSearchResults([]); + }} + > + + {item.name} ({item.symbol}) + + + )} + /> + )} + {renderSelectedStockCard()} + + + + + )} + + (fabActive ? addStockToPortfolio() : setFabActive(true))} + > + + + + {fabActive && ( + + + + )} + ); @@ -144,11 +426,39 @@ const styles = StyleSheet.create({ padding: 16, backgroundColor: '#F5F5F5', }, + headerContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + searchContainer: { + flexDirection: 'row', + alignItems: 'center', + width: '100%', + + }, header: { fontSize: 22, fontWeight: 'bold', color: '#000', - marginBottom: 16, + }, + fab: { + alignSelf: 'center', + backgroundColor: '#0077B6', + width: 50, + height: 50, + borderRadius: 30, + alignItems: 'center', + justifyContent: 'center', + margin: 10, + }, + trashIcon: { + marginTop: 0, + padding: 8, + alignSelf: 'flex-end', + }, + flexInput: { + flex: 1, }, description: { fontSize: 16, @@ -201,10 +511,23 @@ const styles = StyleSheet.create({ }, stockCard: { backgroundColor: '#EFEFEF', - padding: 10, + padding: 5, marginVertical: 10, borderRadius: 8, }, + swipeDelete: { + backgroundColor: '#EF4444', + justifyContent: 'center', + alignItems: 'center', + height: '100%', + paddingHorizontal: 10, + borderRadius: 8, + }, + swipeDeleteText: { + color: '#FFF', + fontSize: 14, + fontWeight: 'bold', + }, stockName: { fontSize: 16, fontWeight: 'bold', @@ -218,8 +541,60 @@ const styles = StyleSheet.create({ fontSize: 14, fontWeight: 'bold', }, + inputSection: { + backgroundColor: '#FFF', + padding: 16, + borderRadius: 8, + marginVertical: 16, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 2, + }, + input: { + borderWidth: 1, + borderColor: '#CCC', + borderRadius: 8, + padding: 10, + margin: 5, + backgroundColor: '#F9F9F9', + fontSize: 14, + color: '#333', + }, + searchButton: { + backgroundColor: '#0077B6', + padding: 10, + borderRadius: 8, + + margin: 5, + }, + stockResult: { + padding: 12, + backgroundColor: '#EFEFEF', + borderRadius: 8, + marginVertical: 4, + }, + stockResultText: { + fontSize: 14, + color: '#000', + }, bottomSpace: { - height: 20, // Space at the bottom of the page + height: 30, + }, + cancelFab: { + backgroundColor: '#EF4444', + width: 50, + height: 50, + borderRadius: 30, + alignItems: 'center', + justifyContent: 'center', + margin: 10, + }, + actionButtons: { + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', }, });