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'
+
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.
+
+
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 4f2fad02..0f370fab 100644
--- a/mobile/src/pages/App.js
+++ b/mobile/src/pages/App.js
@@ -18,6 +18,11 @@ import Markets from './Markets';
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';
@@ -26,12 +31,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');
}
@@ -71,8 +76,49 @@ const PostStack = () => {
)
}
+
+const MarketsStack = () => (
+
+ }}
+ />
+
+
+);
+
+const PortfolioStack = () => (
+
+ }}
+ />
+
+
+
+
+);
+
const DrawerNavigator = () => {
- const { user } = useAuth();
+ const { username, userId } = useAuth();
@@ -89,7 +135,7 @@ const DrawerNavigator = () => {
component={Home}
/>
- { user ? (
+ { userId ? (
{
)}
{
+ { userId ? (
+
+ ) : (
+
+ )}
);
diff --git a/mobile/src/pages/Community.js b/mobile/src/pages/Community.js
index c879ecfd..cb4932fd 100644
--- a/mobile/src/pages/Community.js
+++ b/mobile/src/pages/Community.js
@@ -1,54 +1,63 @@
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 { userId } = useAuth();
+ const { baseURL } = config;
const [posts, setPosts] = useState([]);
+ const [users, setUsers] = useState([]);
+ const [userMap, setUserMap] = useState([]);
const fetchPosts = async () => {
- const baseURL = 'http://159.223.28.163:30002';
- 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);
+ }
+ };
+
+
+ const fetchUsers = async () => {
+ const postURL = baseURL + '/users/';
+
+ try {
+ const response = await fetch(postURL, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ if (response.ok) {
+ const jsonResponse = await response.json();
+ setUsers(jsonResponse);
+ const map = {};
+ jsonResponse.forEach((user) => {
+ map[user.id] = user;
+ });
+ setUserMap(map);
+
} else {
const errorResponse = await response.json();
@@ -58,38 +67,51 @@ const Community = ({navigation}) => {
}
} catch (error) {
-
console.error('Error:', error);
}
-
-
-
};
- useFocusEffect(
- React.useCallback(() => {
- fetchPosts();
- }, [])
- );
+ useFocusEffect(
+ React.useCallback(() => {
+ fetchPosts();
+ fetchUsers();
+ }, [])
+ );
+
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 = () => {
- navigation.navigate('CreatePost');
+ if(!userId){
+ Alert.alert('Please login to create a post');
+ navigation.navigate('Login&Register');
+ }else{
+ navigation.navigate('CreatePost');
+ }
+
+ }
+
+ const renderUsername = (post) => {
+ if(userMap[post.author]){
+ return userMap[post.author].username;
+ }else{
+ return post.author;
+ }
}
const renderItem = ({ item: post }) => (
+ console.log("post", post),
{post.title}
- Published on: {post.date} by {post.author}
+ Published on: {new Date(post.created_at).toLocaleDateString()} by {renderUsername(post)}
{post.content}
{post.tags.map((tag) => (
- {tag}
+ {tag.name}
))}
{post.graph && (
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;
diff --git a/mobile/src/pages/CreatePost.js b/mobile/src/pages/CreatePost.js
index ada545e3..19e43391 100644
--- a/mobile/src/pages/CreatePost.js
+++ b/mobile/src/pages/CreatePost.js
@@ -1,99 +1,150 @@
-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 { user, accessToken, refreshToken } = useAuth();
+const CreatePost = ({ navigation }) => {
+ const { baseURL } = config;
+ const { accessToken, userId } = 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 [availableTags, setAvailableTags] = useState([]);
+ const [selectedTags, setSelectedTags] = useState([]);
+ const [newTag, setNewTag] = useState("");
- const handleCreation = async () => {
- const baseURL = 'http://159.223.28.163:30002';
- const postData = {
- title: postTitle,
- content: postContent,
- liked_by:[],
- 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/';
+ 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('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");
+ fetchTags();
+ } 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 (
-
- {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}
))}
-
- + Tag Ekle
+
+
+
+
+ + Add Tag
- handleCreation()}
- >
+
Post
@@ -114,22 +165,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,
diff --git a/mobile/src/pages/Home.js b/mobile/src/pages/Home.js
index ebd0dfb0..95a879bd 100644
--- a/mobile/src/pages/Home.js
+++ b/mobile/src/pages/Home.js
@@ -10,25 +10,23 @@ import gubrfLogo from "../../assets/stock-logos/gubrf.png";
import bist100 from "../../assets/stock-logos/bist-100.png";
import bist30 from "../../assets/stock-logos/bist-30.png";
import bist from "../../assets/stock-logos/bist.png";
-
+import config from './config/config';
const Home = () => {
+ 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 {
- setLoading(false);
}
};
@@ -51,9 +49,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/Login.js b/mobile/src/pages/Login.js
index 951e3e30..7402b0bd 100644
--- a/mobile/src/pages/Login.js
+++ b/mobile/src/pages/Login.js
@@ -10,9 +10,11 @@ import {
ActivityIndicator,
} 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;
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
@@ -21,9 +23,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 +31,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,9 +50,9 @@ const Login = ({ navigation }) => {
// Check for a successful login
if (response.ok) {
// Handle successful login
-
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/Markets.js b/mobile/src/pages/Markets.js
index 2d55cfb4..36a9deb1 100644
--- a/mobile/src/pages/Markets.js
+++ b/mobile/src/pages/Markets.js
@@ -1,149 +1,187 @@
-import React, { useState } from 'react';
-import { View, Text, FlatList, TouchableOpacity, StyleSheet, ScrollView } from 'react-native';
+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 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 }],
- };
+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
-const Markets = () => {
- const [selectedStock, setSelectedStock] = useState(null);
- const [activeCategory, setActiveCategory] = useState('BIST30');
+ 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();
+
+ // 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;
+ }
+
+ 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.');
+ } finally {
+ setLoading(false);
+ 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
+ }, []);
+
+ const loadMoreStocks = () => {
+ if (loadingMore || !hasMore) return;
+
+ setLoadingMore(true);
+ setPage((prevPage) => {
+ const nextPage = prevPage + 1;
+ fetchStocks(nextPage);
+ return nextPage;
+ });
+ };
const renderStockItem = ({ item }) => (
setSelectedStock(item)}
+ style={styles.stockCard}
+ onPress={() => navigation.navigate('StockDetails', { id: item.id })}
>
- {item.code}
- {item.name}
- ${item.price.toFixed(2)}
+
+ {item.symbol || 'N/A'}
+ {item.name || 'No Name'}
+
+
+ {parseFloat(item.price || 0).toFixed(2)} {item.currency?.code || ''}
+
);
- return (
-
- {/* Header with Categories */}
-
- {Object.keys(mockStocks).map((category) => (
- {
- setActiveCategory(category);
- setSelectedStock(null);
- }}
- >
- {category}
-
- ))}
+ if (loading) {
+ return (
+
+
+ );
+ }
- {/* Stock List */}
-
- item.code}
+ return (
+
+ {/* Search Bar */}
+
+
+ {
+ setSearchQuery(text);
+ searchStocks(text); // Trigger search on input change
+ }}
/>
- {/* Stock Details */}
- {selectedStock && (
-
- {selectedStock.code}
- {selectedStock.name}
- Price: ${selectedStock.price.toFixed(2)}
- {selectedStock.about}
-
+ {/* Loading Indicator for Search */}
+ {searchLoading && (
+
)}
+
+ {/* Stock List */}
+ item.id.toString()} // Ensure unique keys
+ 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 && !searchQuery && (
+
+
+
+ )
+ }
+ />
);
};
@@ -151,75 +189,70 @@ const Markets = () => {
const styles = StyleSheet.create({
container: {
flex: 1,
- backgroundColor: '#F5F5F5',
+ backgroundColor: '#F4F4F4',
+ padding: 10,
},
- header: {
+ searchContainer: {
flexDirection: 'row',
- justifyContent: 'space-around',
- paddingVertical: 10,
+ alignItems: 'center',
backgroundColor: '#FFFFFF',
- borderBottomWidth: 1,
- borderBottomColor: '#DDDDDD',
+ borderRadius: 8,
+ paddingHorizontal: 10,
+ marginBottom: 10,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 2,
},
- categoryButton: {
- padding: 10,
- borderRadius: 20,
- backgroundColor: '#E0E0E0',
+ searchIcon: {
+ marginRight: 10,
},
- activeCategory: {
- backgroundColor: '#007AFF',
+ searchInput: {
+ fontSize: 16,
+ color: '#333',
+ flex: 1,
},
- categoryText: {
- color: '#FFFFFF',
- fontWeight: 'bold',
+ searchLoader: {
+ marginBottom: 10,
},
- listContainer: {
+ loaderContainer: {
flex: 1,
- backgroundColor: '#FFFFFF',
+ 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: {
- fontWeight: 'bold',
- color: '#333333',
- },
- stockName: {
- color: '#555555',
- },
- stockPrice: {
- color: '#007AFF',
- fontWeight: 'bold',
- },
- detailsContainer: {
- padding: 20,
- backgroundColor: '#FFFFFF',
- borderTopWidth: 1,
- borderTopColor: '#DDDDDD',
- },
- detailsTitle: {
fontSize: 18,
fontWeight: 'bold',
- marginBottom: 10,
color: '#007AFF',
},
- detailsName: {
- fontSize: 16,
+ stockName: {
+ fontSize: 14,
color: '#333333',
+ marginTop: 4,
},
- detailsPrice: {
- fontSize: 14,
- marginTop: 5,
- color: '#555555',
+ stockPrice: {
+ fontSize: 16,
+ fontWeight: '600',
+ color: '#4CAF50',
},
- detailsAbout: {
- fontSize: 14,
- marginTop: 10,
- color: '#666666',
+ footer: {
+ padding: 10,
+ alignItems: 'center',
},
});
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/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/PortfolioDetails.js b/mobile/src/pages/PortfolioDetails.js
new file mode 100644
index 00000000..a01f35fe
--- /dev/null
+++ b/mobile/src/pages/PortfolioDetails.js
@@ -0,0 +1,601 @@
+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);
+
+ const predefinedColors = ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF'];
+
+ const generateColor = () => {
+ let color;
+ do {
+ color = `#${Math.floor(Math.random() * 16777215).toString(16)}`;
+ } while (color === '#000000' || color === '#ffffff');
+ return color;
+ };
+
+ const chartData = stocks.map((stock, index) => ({
+ name: stock.name,
+ val: stock.currentPrice * stock.quantity,
+ color: index < predefinedColors.length ? predefinedColors[index] : generateColor(),
+ legendFontColor: '#000',
+ legendFontSize: 12,
+ }));
+
+ const totalInvestment = stocks.reduce(
+ (total, stock) => total + parseFloat(stock.price_bought) * stock.quantity,
+ 0
+ );
+ const currentValue = 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;
+
+ 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}
+ {}} style={styles.trashIcon}>
+
+
+
+
+ {portfolio.description}
+ Created on: {formattedDate}
+
+
+
+ Total Investment: {totalInvestment.toFixed(2)} {stocks[0]?.currency || 'N/A'}
+
+
+ Current Value: {currentValue.toFixed(2)} {stocks[0]?.currency || 'N/A'}
+
+
+ {isPortfolioProfit ? 'Total Profit' : 'Total Loss'}: {portfolioProfitLoss.toFixed(2)}{' '}
+ {stocks[0]?.currency || 'N/A'} ({portfolioProfitLossPercentage}%)
+
+
+
+
+ `rgba(0, 0, 0, ${opacity})`,
+ }}
+ accessor="val"
+ backgroundColor="transparent"
+ paddingLeft="15"
+ hasLegend={false}
+ />
+
+
+
+ {chartData.map((item, index) => (
+
+
+
+ {item.val.toFixed(2)} - {item.name}
+
+
+ ))}
+
+
+ Stocks
+ {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 && (
+
+
+
+ )}
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ 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',
+ },
+ 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,
+ 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: 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',
+ color: '#000',
+ },
+ stockDetails: {
+ fontSize: 14,
+ color: '#333',
+ },
+ stockProfitLoss: {
+ 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: 30,
+ },
+ cancelFab: {
+ backgroundColor: '#EF4444',
+ width: 50,
+ height: 50,
+ borderRadius: 30,
+ alignItems: 'center',
+ justifyContent: 'center',
+ margin: 10,
+ },
+ actionButtons: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+});
+
+export default PortfolioDetails;
diff --git a/mobile/src/pages/Post.js b/mobile/src/pages/Post.js
index 4cdc5288..318769c9 100644
--- a/mobile/src/pages/Post.js
+++ b/mobile/src/pages/Post.js
@@ -1,188 +1,208 @@
import React, { useState, useEffect } from 'react';
-import { ScrollView, Text, StyleSheet, View, Dimensions } 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 [post, setPost] = useState(null);
- const [user, setUser] = useState({});
-
+const Post = ({ route }) => {
+ const { postId, author } = route.params;
+ const { baseURL } = config;
+ const [post, setPost] = useState(null);
+ const [likes, setLikes] = useState(0);
+ const [tooltip, setTooltip] = useState(null);
+ //const [author, setAuthor] = useState(null);
+ useEffect(() => {
+ fetchPost();
+ }, [postId]);
+
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',
- },
- });
+ const response = await fetch(postURL);
if (response.ok) {
const postData = await response.json();
+
+ // Use postData directly to fetch the author
setPost(postData);
+ setLikes(postData.liked_by.length || 0);
- // Kullanıcı bilgisi çekme
- const authorId = postData.author;
- fetchUser(authorId);
+ // 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 fetchUser = async (authorId) => {
- const baseURL = 'http://159.223.28.163:30002';
- const userURL = `${baseURL}/users/${authorId}/`;
-
+
+
+
+ const fetchAuthor = async (authorId) => {
+ const authorURL = `${baseURL}/users/${authorId}/`; // Replace with your API's author endpoint
try {
- const response = await fetch(userURL, {
- method: 'GET',
- headers: {
- 'Content-Type': 'application/json',
- 'X-CSRFToken': 'WTyfHMRCB4yI4D5IhdreWdnFDe6skYPyBbenY9Z5F5VWc7lyii9zV0qXKjtEDGRN',
- },
- });
+ const response = await fetch(authorURL);
if (response.ok) {
- const userData = await response.json();
- setUser(userData);
+ const authorData = await response.json();
+ setAuthor(authorData);
} else {
- console.log('Error fetching user:', response);
- console.error('Error fetching user:', response.status);
+ console.error(`Failed to fetch author: ${response.status}`);
}
} catch (error) {
- console.error('Error fetching user:', error.message, error.stack);
+ console.error('Error fetching author:', error);
}
};
-
- useEffect(() => {
- fetchPost();
-
- }, []);
-
+ const handleLike = () => {
+ setLikes((prevLikes) => prevLikes + 1);
+ };
+
+ const stockData = [142, 145, 143, 141, 144, 140, 138, 139];
+
if (!post) {
return Loading...;
}
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}
-
- );
-
- // 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
+ {/* Buttons */}
+
+
+ 👍 Like ({likes})
+
+
+ 💬 Add Comment
+
-
- NVIDIA stocks have recently seen a sharp decline, raising concerns among investors. Here's the latest trend for better understanding.
-
- 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',
+ {/* Chart Section */}
+ Stock Price Chart
+ `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,
- 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,
+ color: '#999999',
+ marginBottom: 20,
+ },
+ content: {
+ fontSize: 16,
+ lineHeight: 24,
+ color: '#444444',
+ marginBottom: 20,
+ },
+ buttonContainer: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ marginTop: 20,
+ },
+ likeButton: {
+ backgroundColor: '#0073e6',
+ paddingVertical: 10,
+ paddingHorizontal: 20,
+ borderRadius: 8,
+ },
+ commentButton: {
+ backgroundColor: '#28a745',
+ paddingVertical: 10,
+ paddingHorizontal: 20,
+ borderRadius: 8,
+ },
+ buttonText: {
+ color: '#ffffff',
+ fontSize: 16,
+ fontWeight: 'bold',
+ textAlign: 'center',
},
tagsContainer: {
flexDirection: 'row',
+ flexWrap: 'wrap',
marginBottom: 10,
},
tag: {
@@ -192,30 +212,11 @@ const styles = StyleSheet.create({
paddingVertical: 5,
borderRadius: 15,
marginRight: 5,
+ marginBottom: 5,
},
- content: {
- fontSize: 16,
- marginBottom: 20,
- },
- graphTitle: {
- fontSize: 18,
- fontWeight: 'bold',
- marginBottom: 10,
- },
- chart: {
- marginVertical: 8,
- borderRadius: 16,
- },
- tooltip: {
- position: 'absolute',
- backgroundColor: '#000',
- padding: 5,
- borderRadius: 5,
- alignItems: 'center',
- },
- tooltipText: {
- color: '#fff',
- fontSize: 12,
+ noTags: {
+ fontSize: 14,
+ color: '#aaa',
},
});
diff --git a/mobile/src/pages/ProfilePage.js b/mobile/src/pages/ProfilePage.js
index 3db392bd..d459b0aa 100644
--- a/mobile/src/pages/ProfilePage.js
+++ b/mobile/src/pages/ProfilePage.js
@@ -5,7 +5,7 @@ import { useAuth } from './context/AuthContext'; // Import AuthContext
const Profile = ({ navigation }) => {
- 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/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/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;
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
diff --git a/mobile/src/pages/context/AuthContext.js b/mobile/src/pages/context/AuthContext.js
index 4456cc38..ef582f5d 100644
--- a/mobile/src/pages/context/AuthContext.js
+++ b/mobile/src/pages/context/AuthContext.js
@@ -4,29 +4,35 @@
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(0);
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);
-
+
};
const logout = async () => {
if (!refreshToken) {
console.warn('No refresh token available for logout.');
- setUser(null);
+ setUsername(null);
+ setUserId(null);
setRefreshToken(null);
+ setAccessToken(null);
return;
}else{
- setUser(null);
+ setUsername(null);
+ setUserId(null);
setRefreshToken(null);
+ setAccessToken(null);
return;
}
@@ -35,7 +41,7 @@
return (
-
+
{children}
);