diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
new file mode 100644
index 0000000..60083d1
--- /dev/null
+++ b/.github/CONTRIBUTING.md
@@ -0,0 +1,101 @@
+# Team Norms
+
+Welcome to our Bill-Splitting App! We aim to create a collabortaive and respectful environemt here. Please take a look at our norms:
+
+- We'll have regular meetings to share updates and listen to each other's thoughts and concerns.
+- In cases where physcial co-location isn't practical, we emphasize everyone making an effort to show to meetings via video call.
+
+## Team Values
+
+- Each team member will be assigned specific tasks and we will make sure to complete it before the next meeting
+- In the event of encountering challenges or needing help, we will consult each other first before notifying a supervisor.
+- Conflicts will be resolved through constant communication with one another.
+ - In the event of encountering a disagreement on project direction, we will host a duscission for everyone to have the chance to input their opinions and hear each other out.
+ - In the event where one or multiple team memebers fail to deliver their obligations, please inform the team about your challenges and we will make necessary adjsutments to keep everything on track.
+ - Members are expected to respond to messages by the end of the work day. Communication is key!
+
+## Sprint Cadence
+
+For our project, we have decided that a sprint should last 2 weeks. It's enough time to be able to complete meaningful work while also being short enough to maintain a sense of urgency and focus.
+
+## Daily Standups
+
+Our daily standups will happen on every Tuesday and Thursday at 1:45pm. Each standup will last 15 minutes and we expect all team memebers to be present synchronously, ready to share their progress and or any blockers. Active participation is key so members will not cover for other members who do not participate. If someone consistently doesn't contribute for two consecutive standups, we'll report this concern to management.
+
+## Coding Standards
+
+- We will be using Visual Studio Code.
+- Always pull from master before working.
+- Always push working code, if you break the pipeline/build then fix it.
+- Write self documenting code. Use descriptive variable and function names. Avoid unnecessary name shortening.
+- Provide specific commit messages.
+
+## Concluding Statement
+
+Our Group Bill Splitting App, makes splitting bills in a group super easy. In today's world, we often share costs for things like meals, trips, or living expenses. Our app helps manage these shared expenses in a way that's simple and fair for everyone involved. The Group Bill Splitting App is here to simplify this process by giving you a straightforward and user-friendly tool to handle shared expenses effortlessly.
+
+## Git Workflow
+
+- Always pull from master branch before working
+- Push each commit with a meaningful message
+- Before we incorporate changes into our main branch, a pull request must be made and must be approved by another member.
+
+## Rules of Contribution
+
+Before you start contributing, please keep the following considerations in mind:
+
+- **Adhere to Team Values**: All contributors must adhere to the team values. Any form of inappropriate behavior will not be tolerated.
+
+- **Pull Requests**: Ensure that your pull requests(PRs) are small enough to be reviewed easily. Always provide a concise and informative description.
+
+- **Issues**: If you're working on a particular issue, please comment on it to prevent multiple contributors working on the same problem.
+
+- **Commit Messages**: Write meaningful commit messages that clearly explain the changes made. This helps in understanding the history and purpose of changes.
+
+- **Coding Standards**: Adhere to the coding standards established in this project for consistency.
+
+#### What to Contribute?
+
+- **Bug Fixes**: If you've discovered a bug and know how to fix it, please submit a PR. Make sure to describe the problem and solution in the PR's description.
+
+- **Features**: New features can be proposed by opening an issue. Once it's discussed and approved, you or someone else can start working on it.
+
+- **Documentation**: Improving or correcting the project's documentation is always appreciated.
+
+## Setting up a Local Development Environment
+
+To set up a local development environment, follow these steps:
+
+(Note: Now, we assume that we will have a 'package.json' and the project will use separate npm scripts to launch the back-end and front-end servers. This may change later on. )
+
+1. **Install Dependencies**:
+
+ After cloning the repository and navigating to the project directory, run the following command to install all the necessary dependencies:
+
+ ```bash
+ npm install
+ ```
+
+2. **Launch the Back-End Server**:
+
+ To start the back-end server, run:
+
+ ```bash
+ npm run start-backend
+ ```
+
+3. **Launch the Front-End (React.js) Server**:
+
+ To start the front-end server, use:
+
+ ```bash
+ npm run start-frontend
+ ```
+
+---
+
+More to add: More instructions will be added as we develop further.
+
+## Building and Testing the Project
+
+Please refer to [README.md](README.md) for details on how to build and test the project.
diff --git a/.github/README.md b/.github/README.md
index 7768631..96214b9 100644
--- a/.github/README.md
+++ b/.github/README.md
@@ -22,10 +22,101 @@ Please refer to [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contrib
## Instructions for Building and Testing
-To be updated
+### Step 1. Clone the [Group Bill Split Repo](https://github.com/agiledev-students-fall2023/4-final-project-group-bill-splitting-app) by
+
+```bash
+git clone https://github.com/agiledev-students-fall2023/4-final-project-group-bill-splitting-app.git
+```
+
+### Step 2. Make sure you have Node installed.
+
+Both the back-end and front-end should be running.
+
+### Step 3. Building and Running the Backend
+
+#### Navigate to the backend directory:
+
+```bash
+cd back-end
+```
+
+#### Install Dependencies
+
+Install the necessary Node.js packages for the backend:
+
+```bash
+npm install
+```
+
+#### Start the Server
+
+Run the following command to start the Express.js server:
+
+```bash
+nodemon server
+```
+
+You can open `http://localhost:3001` to see back end.
+
+### Step 4. Building and Running the Frontend
+
+#### Navigate to the front-end directory
+
+```bash
+cd front-end
+```
+
+#### Install Dependencies
+
+Install the required packages using npm. These packages are essential for the app to function correctly:
+
+```bash
+npm install
+```
+
+#### Start the Application
+
+With all the dependencies in place, start the application by running:
+
+```bash
+npm start
+```
+
+### Step 5. Accessing the App
+
+Once the app is running, it will be available in development mode. Open your web browser and go to [http://localhost:3000](http://localhost:3000) to view the app.
+
+### Step 6. Unit tests
+
+#### Navigate to the back-end directory
+
+```bash
+cd back-end
+```
+
+#### Running Unit Tests
+
+To run the unit tests, execute the following command:
+
+```bash
+npm test
+```
+
+### Step 6. Deployment to Digital Ocean Droplet
+
+Front end address is `http://165.22.42.62:3000/`
+
+Back end address is `http://165.22.42.62:3001/`
## Additional Links
- [User Experience Design](UX-DESIGN.md)
-## Notes
+## Project Roles
+
+| Week | Product Owner | Scrum Master |
+| -------- | ------------- | ------------ |
+| Sprint 1 | Laura Zhao | Elaine Zhang |
+| Sprint 2 | Joy Chen | Cindy Liang |
+| Sprint 3 | Allison Ji | Joy Chen |
+| Sprint 4 | Cindy Liang | Allison Ji |
diff --git a/README.md b/README.md
index 7768631..335b3e3 100644
--- a/README.md
+++ b/README.md
@@ -22,10 +22,109 @@ Please refer to [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contrib
## Instructions for Building and Testing
-To be updated
+### Step 1. Clone the [Group Bill Split Repo](https://github.com/agiledev-students-fall2023/4-final-project-group-bill-splitting-app) by
+
+```bash
+git clone https://github.com/agiledev-students-fall2023/4-final-project-group-bill-splitting-app.git
+```
+
+### Step 2. Make sure you have Node installed.
+
+Both the back-end and front-end should be running.
+
+### Step 3. Building and Running the Backend
+
+#### Navigate to the backend directory:
+
+```bash
+cd back-end
+```
+
+#### Install Dependencies
+
+Install the necessary Node.js packages for the backend:
+
+```bash
+npm install
+```
+
+#### Start the Server
+
+Run the following command to start the Express.js server:
+
+```bash
+nodemon server
+```
+
+You can open `http://localhost:3001` to see back end.
+
+### Step 4. Building and Running the Frontend
+
+#### Navigate to the front-end directory
+
+```bash
+cd front-end
+```
+
+#### Install Dependencies
+
+Install the required packages using npm. These packages are essential for the app to function correctly:
+
+```bash
+npm install
+```
+
+#### Start the Application
+
+With all the dependencies in place, start the application by running:
+
+```bash
+npm start
+```
+
+### Step 5. Accessing the App
+
+Once the app is running, it will be available in development mode. Open your web browser and go to [http://localhost:3000](http://localhost:3000) to view the app.
+
+### Step 6. Unit tests
+
+#### Navigate to the back-end directory
+
+```bash
+cd back-end
+```
+
+#### Running Unit Tests
+
+To run the unit tests, execute the following command:
+
+```bash
+npm test
+```
+
+### Step 7. CI/CD using GitHub Actions
+
+We have set up Continuous Integration and Continuous Deployment (CI/CD) using GitHub Actions. Every push to the main branch triggers automatic testing and deployment to our Digital Ocean Droplet.
+
+### Step 8. Docker Integration
+
+We have containerized our application using Docker. You can build and run the Docker containers for both the front end and back end with the provided Dockerfiles.
+
+## Deployment to Digital Ocean Droplet
+
+Front end address is `http://165.22.42.62:3000/`
+
+Back end address is `http://165.22.42.62:3001/`
## Additional Links
- [User Experience Design](UX-DESIGN.md)
-## Notes
+## Project Roles
+
+| Week | Product Owner | Scrum Master |
+| -------- | ------------- | ------------ |
+| Sprint 1 | Laura Zhao | Elaine Zhang |
+| Sprint 2 | Joy Chen | Cindy Liang |
+| Sprint 3 | Allison Ji | Joy Chen |
+| Sprint 4 | Cindy Liang | Allison Ji |
diff --git a/back-end/routes/addEventMemberRoute.js b/back-end/routes/addEventMemberRoute.js
index 718c364..8d7e270 100644
--- a/back-end/routes/addEventMemberRoute.js
+++ b/back-end/routes/addEventMemberRoute.js
@@ -1,19 +1,17 @@
const express = require("express");
const router = express.Router();
const { User } = require("../models/User.js");
-const { Event } = require("../models/Event.js");
+const Event = require("../models/Event.js");
const { body, validationResult } = require("express-validator");
-router.get('/friendsList/:userId', async (req, res) => {
+router.get("/friendsList/:userId", async (req, res) => {
try {
//fetch all data
const userId = req.params.userId;
- console.log("userId:", userId);
const userWithFriends = await User.findById(userId).populate({
path: "friends",
model: "User",
});
- console.log(userWithFriends);
if (!userWithFriends) {
return res.status(404).send("User not found");
}
diff --git a/back-end/routes/addEventRoute.js b/back-end/routes/addEventRoute.js
index c211917..598651c 100644
--- a/back-end/routes/addEventRoute.js
+++ b/back-end/routes/addEventRoute.js
@@ -44,6 +44,13 @@ router.post(
}
user.events.push(savedEvent._id); // Add the event ID to the user's events list
+
+ for (const friendId of req.body.Members) {
+ if (friendId !== userId && !user.friends.includes(friendId)) {
+ user.friends.push(friendId);
+ }
+ }
+
await user.save();
console.log(`Event added to user ${userId}`);
}
diff --git a/front-end/src/App.js b/front-end/src/App.js
index 7789a64..814432d 100644
--- a/front-end/src/App.js
+++ b/front-end/src/App.js
@@ -1,5 +1,10 @@
import React, { useState } from "react";
-import { useLocation, BrowserRouter as Router, Routes, Route } from "react-router-dom";
+import {
+ useLocation,
+ BrowserRouter as Router,
+ Routes,
+ Route,
+} from "react-router-dom";
import Event from "./components/Event";
import "./index.css";
import "./App.css";
@@ -34,14 +39,17 @@ function App() {
};
return (
-
-
+
+ } />
} />
} />
- } />
+ }
+ />
-
-
+
+
);
}
@@ -118,9 +126,11 @@ function AppContainer({ isDarkMode, children }) {
const isLoginPage = location.pathname === "/";
const isForgotPasswordPage = location.pathname === "/forgot-password";
// disable dark mode on Login and ForgotPassword page
- const containerClass = isDarkMode && !isLoginPage && !isForgotPasswordPage ? "container dark-mode" : "container";
+ const containerClass =
+ isDarkMode && !isLoginPage && !isForgotPasswordPage
+ ? "container dark-mode"
+ : "container";
return
{children}
;
}
-
export default App;
diff --git a/front-end/src/components/AddEvent.jsx b/front-end/src/components/AddEvent.jsx
index 88a159a..024c144 100644
--- a/front-end/src/components/AddEvent.jsx
+++ b/front-end/src/components/AddEvent.jsx
@@ -1,290 +1,291 @@
import React, { useEffect, useState } from "react";
import axios from "axios";
-import addMemberButton from "../images/addMember.png";
-import { jwtDecode } from 'jwt-decode';
+import addMemberButton from "../images/addMember.png";
+import { jwtDecode } from "jwt-decode";
-function AddEvent({addEvent, onClose}){
- const[friendsList, setfriendsList] = useState([])
- const[selectedFriends, setselectedFriends] = useState(false)
- const[searchQuery, setSearchQuery] = useState('');
- const[searchPerformed, setSearchPerformed] = useState(false);
- const[selectedMember, setselectedMember] = useState([])
- const[loading, setLoading] = useState(false)
- const [currentUser, setCurrentUser] = useState(null);
- const[errors, setErrors] = useState({
- eventName: false,
- Date: false,
- Description: false,
- Members: false
- });
- const[eventData, seteventData] = useState({
- eventName: '',
- Date: '',
- Description: '',
- Members: [],
- })
+function AddEvent({ addEvent, onClose }) {
+ const [friendsList, setfriendsList] = useState([]);
+ const [selectedFriends, setselectedFriends] = useState(false);
+ const [searchQuery, setSearchQuery] = useState("");
+ const [searchPerformed, setSearchPerformed] = useState(false);
+ const [selectedMember, setselectedMember] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [errors, setErrors] = useState({
+ eventName: false,
+ Date: false,
+ Description: false,
+ Members: false,
+ });
+ const [eventData, seteventData] = useState({
+ eventName: "",
+ Date: "",
+ Description: "",
+ Members: [],
+ });
- const backupData_friends = [{
- "id":"507f1f77bcf86cd799439011",
- "username":"Gaby Coupar",
- "avatar":"https://robohash.org/temporererumomnis.png?size=50x50\u0026set=set1",
- "phone":"435-715-2899",
- "email":"gcoupar0@rakuten.co.jp"
- }, {
- "id": "507f1f77bcf86cd799439012",
- "username": "Andy Gaber",
- "avatar": "https://robohash.org/quaeetcorrupti.png?size=50x50&set=set1",
- "phone":"425-712-2309",
- "email":"gmember0@rakuten.co.jp"
- }]
-
- const handleInputChange =(e) =>{
- const{name, value} = e.target;
- seteventData(prev => ({...prev,[name]:value}))
+ const handleInputChange = (e) => {
+ const { name, value } = e.target;
+ seteventData((prev) => ({ ...prev, [name]: value }));
+ };
+
+ const validateForm = () => {
+ const newErrors = {};
+ let isValid = true;
+
+ // Validate Event Name
+ if (!eventData.eventName) {
+ newErrors.eventName = true;
+ isValid = false;
}
- const validateForm = () => {
- const newErrors = {};
- let isValid = true;
+ // Validate Date
+ if (!eventData.Date) {
+ newErrors.Date = true;
+ isValid = false;
+ }
- // Validate Event Name
- if (!eventData.eventName) {
- newErrors.eventName = true;
- isValid = false;
- }
+ // Validate Description
+ if (!eventData.Description) {
+ newErrors.Description = true;
+ isValid = false;
+ }
+ if (selectedMember.length === 0) {
+ alert("Please select at least one member.");
+ newErrors.Members = true;
+ isValid = false;
+ }
- // Validate Date
- if (!eventData.Date) {
- newErrors.Date = true;
- isValid = false;
- }
+ setErrors(newErrors);
+ return isValid;
+ };
- // Validate Description
- if (!eventData.Description) {
- newErrors.Description = true;
- isValid = false;
- }
- if (selectedMember.length === 0) {
- alert('Please select at least one member.');
- newErrors.Members = true;
- isValid = false;
- }
+ const handleAddEvent = async () => {
+ if (!validateForm()) {
+ return; // Stop the function if validation fails
+ }
- setErrors(newErrors);
- return isValid;
+ const submitData = {
+ eventName: eventData.eventName,
+ Date: eventData.Date,
+ Description: eventData.Description,
+ Members: selectedMember,
};
- const handleAddEvent = async () => {
- if (!validateForm()) {
- return; // Stop the function if validation fails
- }
-
- const submitData = {
- eventName: eventData.eventName,
- Date: eventData.Date,
- Description: eventData.Description,
- Members: selectedMember
- };
-
- try {
- console.log('Submitting data:', submitData);
- const response = await axios.post(`${process.env.REACT_APP_BACKEND}/addEvent`, submitData);
- console.log(`Event added:`, response.data);
- onClose(); // Optionally close the form upon successful submission
- } catch (error) {
- console.error('Failed to submit event:', error.response);
- }
+ try {
+ const response = await axios.post(
+ `${process.env.REACT_APP_BACKEND}/addEvent`,
+ submitData
+ );
+ onClose(); // Optionally close the form upon successful submission
+ } catch (error) {
+ console.error("Failed to submit event:", error.response);
}
+ };
- //fetch friends' data for the member list
- async function friendsCL(userID){
- setLoading(true);
- setSearchPerformed(true);
- console.log('userID in friendsCL:', userID, 'Type:', typeof userID);
- try{
-
- // Make the API call
- const response = await axios.get(`${process.env.REACT_APP_BACKEND}/addEventMember/friendsList/${userID}`);
- //return the data
- console.log(response)
- setfriendsList(Array.isArray(response.data.friends) ? response.data.friends : [response.data.friends]);
- //setfriendsList(response.data.flatMap(user => user.friends));
+ //fetch friends' data for the member list
+ async function friendsCL(userID) {
+ setLoading(true);
+ setSearchPerformed(true);
+ try {
+ // Make the API call
+ const response = await axios.get(
+ `${process.env.REACT_APP_BACKEND}/addEventMember/friendsList/${userID}`
+ );
+ //return the data
+ setfriendsList(
+ Array.isArray(response.data.friends)
+ ? response.data.friends
+ : [response.data.friends]
+ );
+ } catch (error) {
+ console.error("There was an error fetching the data:", error.response);
+ } finally {
+ setLoading(false);
+ }
+ }
- }catch(error){
- console.error("There was an error fetching the data:", error.response);
- setfriendsList(backupData_friends)
- }
- finally{
- setLoading(false);
+ const handleSearch = () => {
+ const token = localStorage.getItem("token");
+ const decodedUser = jwtDecode(token);
+ if (decodedUser && decodedUser.id) {
+ friendsCL(decodedUser.id);
+ setselectedMember((prevMembers) => {
+ // Check if the current user's ID is already in the selected members
+ if (prevMembers.some((member) => member.id === decodedUser.id)) {
+ return prevMembers;
}
+ // Add the current user's ID to the selected members
+ return [...prevMembers, decodedUser.id];
+ });
+ } else {
}
+ };
-/*
- useEffect(() => {
- // Retrieve the current user from local storage
- const token = localStorage.getItem("token");
- const decodedUser = jwtDecode(token);
- if (decodedUser && decodedUser.id) {
- console.log(decodedUser.id)
- setselectedMember(prevMembers => {
- // Check if the current user's ID is already in the selected members
- if (prevMembers.some(member => member.id === decodedUser.id)) {
- return prevMembers;
- }
- // Add the current user's ID to the selected members
- return [...prevMembers, decodedUser.id];
- });
- } else {
- console.error("No valid user found in local storage.");
- }
- }, []);
- */
+ const handleSearchChange = (e) => {
+ setSearchQuery(e.target.value);
+ };
- /*
- useEffect(() => {
- console.log('Selected Members:', selectedMember);
- }, [selectedMember]);
- */
+ const filteredFriends = searchQuery
+ ? friendsList.filter((friend) =>
+ friend?.username?.toLowerCase().includes(searchQuery.toLowerCase())
+ )
+ : [];
- const handleSearch = () => {
- const token = localStorage.getItem("token");
- const decodedUser = jwtDecode(token);
- if (decodedUser && decodedUser.id) {
- console.log(decodedUser.id)
- setCurrentUser(decodedUser.id)
- friendsCL(decodedUser.id)
- setselectedMember(prevMembers => {
- // Check if the current user's ID is already in the selected members
- if (prevMembers.some(member => member.id === decodedUser.id)) {
- return prevMembers;
- }
- // Add the current user's ID to the selected members
- return [...prevMembers, decodedUser.id];
- });
- } else {
- console.error("No valid user found in local storage.");
- }
- };
-
-
- const handleSearchChange = (e) =>{
- setSearchQuery(e.target.value)
+ const handleSelectedMember = (memberId) => {
+ if (selectedMember.includes(memberId)) {
+ return;
}
-
- const filteredFriends = searchQuery ? friendsList.filter(friend => friend?.username?.toLowerCase().includes(searchQuery.toLowerCase())) : [];
-
- const handleSelectedMember = (memberId) =>{
- if(selectedMember.includes(memberId)){
- return;
- }
- const member = friendsList.find(p => p._id === memberId)
- if(member){
- setselectedMember([...selectedMember, memberId]);
- console.log(`Member Successfuly Added!`)
- }
-
+ const member = friendsList.find((p) => p._id === memberId);
+ if (member) {
+ setselectedMember([...selectedMember, memberId]);
}
+ };
- useEffect(()=>{
- seteventData(prev => ({
- ...prev,
- Members:selectedMember
- }));
- },[selectedMember])
+ useEffect(() => {
+ seteventData((prev) => ({
+ ...prev,
+ Members: selectedMember,
+ }));
+ }, [selectedMember]);
- return(
-
+ function calculateAmounts(expenses, currentUserId) {
+ let amountOwed = 0; // Amount the current user owes to others
+ let amountOwedBy = 0; // Amount owed to the current user by others
- setSearchTerm(e.target.value)}
- className="mt-4 search-input"
- />
-
-
+ expenses.forEach((expense) => {
+ if (expense.settleTo._id !== currentUserId && !expense.status) {
+ // If the current user is not the one who paid and the status is false (unsettled)
+ amountOwed += expense.amount;
+ }
+ if (expense.settleTo._id === currentUserId && !expense.status) {
+ // If the current user is the one who paid and the status is false (unsettled)
+ amountOwedBy += expense.amount;
+ }
+ });
+ return { amountOwed, amountOwedBy };
+ }
- {addEvent && (
- {setaddEvent(false); window.location.reload();}} />
- )}
-
-