diff --git a/CNAME b/CNAME
new file mode 100644
index 0000000..d4faa2d
--- /dev/null
+++ b/CNAME
@@ -0,0 +1 @@
+camping.sebtota.com
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..d401a91
--- /dev/null
+++ b/README.md
@@ -0,0 +1,93 @@
+# Camping Trip Planner
+## by Ryan O'Connor, Sebastian Tota, Shravanth Surapaneni, Marcus Anestad and Matthew Day
+
+### Reference Links
+[Project Board](https://trello.com/b/uclUHtIM/camping-trip-planner)
+
+[GitHub Repo](https://github.com/SebTota/Camping-Trip-Planner-Backend)
+
+### User Features
+* Create account
+* Create new trips
+* Invite friends to your trip planners
+* Create multiple lists in each trip
+* Edit/delete items on list
+* Mobile friendly
+
+### Security Features
+* Hashing and encrypting user passwords
+* ReCaptcha for bot prevention
+* Protected endpoints using sessions, only allowing signed in users to access data
+
+### Project Overview
+_The goal of the project is to create an application for camping/outdoor trips. The project
+functionality includes planning, responsibility sharing, and photo uploading. Users of the app
+will be able to create a trip and invite friends. The focus of the planning section includes gear lists,
+food menus, directions, and maps. Responsibility sharing includes signing up for bringing food/gear, keeping track of
+how much each person spends, and a system for reimbursement after the trip. Finally, after the trip members can upload and
+make comments on photos._
+
+### Vision Statement
+_For a group of adventure seekers who are planning an outdoors activity together the Camping
+Trip Planner is a web app that allows groups to plan a trip together in a more intuitive and seamless
+way unlike Google Sheets which takes longer to create._
+
+### Personas
+
+#### Julia, a college student
+
+_Julia, age 21, is a senior CS major at Temple University in Philadelphia.
+She grew up in Montgomery County, where she would enjoy hikes around the Wissahickon
+with her family growing up. This extended to a true passion for outdoor activities and
+does them to relax and escape the stresses of her daily life._
+
+_With an increase in course difficulty on top of living in the age of COVID,
+Julia relies heavily on weekly social distanced outings with her friends to
+maintain her sanity. On a recent trip to the Poconos for some camping, her group
+realized they forgot to bring anything substantial for dinner. Being out in the middle of
+nowhere they had to drive 50 minutes to a supermarket to remedy the situation, cutting heavily
+into their time with each other and nature. Being a person that is highly involved in the world of
+new software technology, Julia wondered if there was an app that could help prevent an incident like
+this in the future. Luckily, she stumbled across the Camping Trip Planner app. Since then,
+every trip goes off without a hitch._
+
+#### Juan, a father
+
+_Juan is a parent of two. His family always looks forward to going on a yearly summer
+canoe trips with the Smith’s. Juan is a doctor and pretty inexperienced when it comes
+to new technology. However he loves learning new things. He hears about Camping Trip
+Planner and excitedly calls Mr. Smith. They are easily able to set up profiles and
+create a trip._
+
+_Now they are easily able to organize and plan the trip. They are also able to get
+their children (Smith, John, Smitty, and Johnny) involved and begin to teach them
+everything that goes into organizing such a trip. They have a great time and plan
+on using Camping Trip Planner for planning their trip next year._
+
+#### Tracy, a middle school teacher
+
+_Tracy, a middle school English teacher, is getting ready to take her class on a camping trip. Every year, the 8th-grade class takes a 3-day trip to the local campground as a celebration for graduating from middle school. This is Tracys first year taking her class, and she is worried about helping her students plan for the trip._
+
+_With the Camping Trip Planner, she can sign all her students up into a single trip list to make sure enough supplies are brought, without over packing the busses. Rather than speaking to each student individually in the hopes they remember to bring what they are asked to bring, she makes a list of all the necessities, and has the student assign themselves to certain items. This way, Tracy can keep track of what has been tracked, and what is still missing._
+
+#### Tyler, a boy scout troop leader
+_Tyler is is the troop leader for his son's boy scout troop. He wants to take his troop camping this weeknd
+to teach them outdoor skills. However, he can't expect a bunch of middle schoolers to remember to bring everything, he needed
+to find an effecient way to organize everything in preperation for the trip. As we all know the boy scout motto is: "Be prepared",
+so clearly had to think of everything._
+
+_So, to deal with this he asks the parents of the kids in his troop for ideas and one of them suggests, Camping Trip Planner.
+Tyler, looked into it and found that Camping Trip Planner not only lets him create a list for his trip, but also allows him to
+add people to share the list with so that they could also collaborate and use it as a checklist. Tyler had found his solution,
+he was able to add all the parents of the kids in his troop, and this way he has a way to keep track of everything and no longer
+has to rely on the kids to make sure they have everything they will need._
+
+### Testing
+We focused mainly on manual testing for out application. We started with testing database functions in Python
+Then, we created API route that used the database function and we would manually check to make sure that the database was updating as we expected for each database function/api route.
+
+The API routes were tested with Postman by calling certain routes and manually checking the database. Once we tested it with postman, we would integrate the endpoint with out website and continue manual checking of the database and results being shown.
+
+To make teseting easier, we added code to the Python API web service that would check if we were running on localhost. If we were, that meant we were in testing mode and we could bypass the reCaptcha for authentication making testing a bit easier.
+
+![](postman.png)
diff --git a/group.html b/group.html
new file mode 100644
index 0000000..43427fe
--- /dev/null
+++ b/group.html
@@ -0,0 +1,308 @@
+
+
+
+
+ Pitched
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Invite a new user to the group:
+
+
+
Send Invite
+
+
+
+ Create New List
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Are you sure you wish to delete this list (data will be purged)?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Assigned
+
Assign to myself
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/group.js b/group.js
new file mode 100644
index 0000000..369705f
--- /dev/null
+++ b/group.js
@@ -0,0 +1,443 @@
+//DataTable = dt;
+const urlParams = new URLSearchParams(window.location.search);
+const groupName = decodeURIComponent(urlParams.get('group-name'));
+const groupUuid = urlParams.get('group-uuid');
+
+
+let listUuid;
+let listCount;
+let selectedItem;
+let elementUuid;
+
+if (getCookie('active') === 'true') {
+ // Signed in
+ nav_signOut.style.display = "inline";
+ nav_profile.style.display = "inline";
+ nav_signIn.style.display = "none";
+} else {
+ window.location.href="login.html";
+}
+
+function addUserToGroupList() {
+ const groupUserList = document.getElementById('group-user-display');
+
+ createRequest('/getUsersInGroup?group_uuid=' + groupUuid, 'GET').then(ret => {
+ if (ret['status'] === 200) {
+ // ST
+ for (let i = 0; i < ret['users'].length; i++) {
+ let user = ret['users'][i];
+ let listItem = document.createElement('li');
+ listItem.classList.add('group-user');
+ listItem.textContent = user['first-name'][0] + user['last-name'][0];
+ listItem.title = user['first-name'] + ' ' + user['last-name'] + " : " + user['email'];
+ listItem.setAttribute('data-toggle', 'tooltip');
+ listItem.setAttribute('data-placement', 'top');
+ listItem.onclick = function() {
+ displayUserModal(user['first-name'] + ' ' + user['last-name'], user['email']);
+ }
+
+ groupUserList.appendChild(listItem);
+ }
+ }
+ })
+}
+
+function displayUserModal(name, email) {
+ $('#modal_group-user').modal('toggle');
+ $('#group-user-modal-name').text(name);
+ $('#group-user-modal-email').text(email);
+}
+
+async function sendGroupInvitePost(data) {
+ const response = await fetch(serverAddress + '/inviteUser', {
+ method: 'POST',
+ credentials: 'include',
+ redirect: 'follow',
+ body: JSON.stringify(data)
+ });
+ return await response.json();
+}
+
+function sendGroupInvite() {
+ const inviteEmail = document.getElementById('input_inviteEmail').value;
+
+ sendGroupInvitePost({
+ 'group-uuid': groupUuid,
+ 'invite-user-email': inviteEmail
+ }).then(ret => {
+ console.log(ret);
+ location.reload(true);
+ })
+}
+
+function expandTextarea(element) {
+ element.style.height = "";
+ element.style.height = element.scrollHeight + "px";
+}
+
+function renameGroup(newName) {
+ createRequest('/renameGroup','POST', {
+ 'group-uuid': groupUuid,
+ 'new-name': newName
+ }).then(ret => {
+ console.log("Changed name");
+ console.log(ret);
+ })
+}
+
+addUserToGroupList();
+document.getElementById('group-name-display').value = groupName;
+
+async function newListPost(data) {
+ const response = await fetch(serverAddress + '/createList?group-uuid=\' +groupUuid', {
+ method: 'POST',
+ credentials: 'include',
+ redirect: 'follow',
+ body: JSON.stringify(data)
+ });
+ return await response.json();
+}
+
+function createNewList() {
+
+ if(listCount === 5){
+ alert( 'Please consider a pro membership if you would like to have more than 5 lists :)');
+ return;
+ }
+
+ newListPost({
+ "name": document.getElementById('input_user-list-name').value,
+ "group-uuid": groupUuid
+ }).then(ret => {
+ if (ret['status'] === 400) {
+ }
+ else{
+ $('#modal_new-list').modal('hide');
+ window.location.reload(true);
+ }
+ });
+}
+
+document.getElementById('group-name-display').textContent = groupName;
+
+async function getLists() {
+ const response = await fetch(serverAddress + '/getListsByGroup?group-uuid=' +groupUuid, {
+ method: 'GET',
+ credentials: 'include',
+ redirect: 'follow'
+ });
+ return await response.json();
+}
+
+//return list elements from db
+async function getElements() {
+
+ const response = await fetch(serverAddress + '/getElementsByList?list-uuid=' +listUuid, {
+ method: 'GET',
+ credentials: 'include',
+ redirect: 'follow'
+ });
+ return await response.json();
+}
+
+//create list item
+function createListItem(item) {
+ const listItem = document.createElement('li');
+ listItem.classList.add('list-item');
+ listItem.textContent = item;
+
+ const listTitle = document.createElement('p');
+ listTitle.classList.add('md1');
+ listTitle.style.marginBottom = '0';
+
+ listItem.appendChild(listTitle);
+
+ return listItem;
+}
+
+//get the div id for the table we are currently rendering
+function getTableDivId(num){
+ switch(num) {
+ case num = 0:
+ return "#data-table1"
+ case num = 1:
+ return "#data-table2"
+ case num = 2:
+ return "#data-table3"
+ case num = 3:
+ return "#data-table4"
+ case num = 4:
+ return "#data-table5"
+ }
+}
+
+//db add new item
+async function newItemPost(data) {
+ console.log(data);
+ const response = await fetch(serverAddress + '/addElementToList', {
+ method: 'POST',
+ credentials: 'include',
+ redirect: 'follow',
+ body: JSON.stringify(data)
+ });
+ return await response.json();
+}
+
+//adding new item
+function addNewItem(){
+ newItemPost({
+ 'list-id' : document.getElementById('list-id').value,
+
+ 'element-name' : document.getElementById('input_user-item-name').value,
+ 'element-description' : document.getElementById('input_user-item-description').value,
+ 'element-quantity' : document.getElementById('input_user-item-quantity').value,
+ 'element_cost': document.getElementById('input_user-item-cost').value,
+ 'element-user-id' : 100,
+ 'element_status': document.getElementById('input_user-item-status').value
+
+ }).then(ret => {
+ if (ret['status'] === 400) {
+ }
+ else{
+ $('#modal_new-item').modal('hide');
+ //let temp = [];
+ //temp.push(['element-name'],['element-quantity' ],['element_cost'], ['element-description'], ['element_status'], ]);
+ //return temp;
+ window.location.reload(true);
+ }
+ });
+}
+
+//adding item modal
+function addItemModal(listId){
+ document.getElementById('list-id').value = listId;
+ $('#modal_add-item').modal('show');
+}
+
+//db delete list
+async function deleteListPost(data) {
+ console.log(data);
+ const response = await fetch(serverAddress + '/deleteList', {
+ method: 'POST',
+ credentials: 'include',
+ redirect: 'follow',
+ body: JSON.stringify(data)
+ });
+ return await response.json();
+}
+
+
+function deleteList(){
+ deleteListPost({
+ 'list-uuid' : document.getElementById('list-uuid').value,
+ }).then(ret => {
+ if (ret['status'] === 400) {
+ }
+ else{
+ $('#modal_new-item').modal('hide');
+ window.location.reload(true);
+ }
+ });
+}
+
+//db delete Item
+async function deleteItemPost() {
+ console.log("250 " + elementUuid);
+ const response = await fetch(serverAddress + '/deleteElementFromList?element-uuid=' +elementUuid, {
+ method: 'POST',
+ credentials: 'include',
+ redirect: 'follow',
+ });
+ // Wait for response from server, then parse the body of the response in json format
+ return await response.json();
+}
+
+//deleteItem
+function deleteItem(elementId){
+ deleteItemPost({
+ "element-uuid" : elementId
+ }).then(ret => {
+ if (ret['status'] === 400) {
+ }
+ else{
+ }
+ });
+}
+
+//db edit item
+async function editItemPost(data) {
+ console.log(data);
+ const response = await fetch(serverAddress + '/addElementToList', {
+ method: 'POST',
+ credentials: 'include',
+ redirect: 'follow',
+ body: JSON.stringify(data)
+ });
+ // Wait for response from server, then parse the body of the response in json format
+ return await response.json();
+}
+
+//editing item
+function editItem(){
+ console.log(291);
+ newItemPost({
+ 'list-id' : document.getElementById('edit-list-id').value,
+
+ 'element-name' : document.getElementById("edit-item-name").value,
+ 'element-description' : document.getElementById('edit-item-description').value,
+ 'element-quantity' : document.getElementById("edit-item-quantity").value,
+ 'element_cost': document.getElementById('edit-item-cost').value,
+ 'element-user-id' : 100,
+ 'element_status': document.getElementById('edit-item-status').value
+
+ }).then(ret => {
+ if (ret['status'] === 400) {
+ }
+ else{
+ deleteItem(selectedItem[5]);
+ window.location.reload(true);
+ }
+ });
+}
+
+
+
+//show modal to edit selected item
+function editItemModal(listId, elementId){
+ document.getElementById('edit-list-id').value = listId;
+
+ document.getElementById("edit-item-name").value = selectedItem[0];
+ document.getElementById("edit-item-quantity").value = selectedItem[1];
+ document.getElementById("edit-item-cost").value = selectedItem[2];
+ document.getElementById("edit-item-description").value = selectedItem[3];
+ document.getElementById("edit-item-status").value = selectedItem[4];
+
+ document.getElementById( "edit-element-id").value = selectedItem[5];
+ $("#edit-item-modal").modal('show');
+}
+
+//show modal to confirm list deletion
+function deleteListModal(listId){
+ document.getElementById('list-uuid').value = listId;
+ $('#modal_delete_list').modal('show');
+}
+
+//render a single table
+function renderTable(tableDivId, temp, listId, listUuid){
+ getElements().then(ret => {
+ console.log(ret);
+ console.log(ret['status']);
+ if (ret['status'] === 200) {
+ console.log("Creating list display");
+
+ const elements = ret['elements'];
+ console.log(elements);
+
+ let dataSet = [];
+
+ for (let i = 0; i < elements.length; i++) {
+ dataSet.push([elements[i]['item-name'], elements[i]['item-quantity'], elements[i]['item-cost'], elements[i]['item-description'], elements[i]['item-status'], elements[i]["item-uuid"]]);
+ console.log(dataSet);
+ }
+
+ let headerDiv = tableDivId.concat("Header");
+ headerDiv = headerDiv.substr(1, headerDiv.length);
+ document.getElementById(headerDiv).textContent = temp;
+
+ $(document).ready(function () {
+
+ $(tableDivId).DataTable({
+ data: dataSet,
+ select: {
+ style: 'single'
+ },
+ 'columnDefs': [{
+ order: [[1, 'asc']]
+ }],
+ columns: [
+ {title: "Item Name", width: '120px'},
+ {title: "Quantity", width: '100px'},
+ {title: "Price", width: '100px'},
+ {title: "Description", width: '200px'},
+ {title: "Bought (T/F)", width: '70px'},
+ { "visible": false, "targets": 6 }
+ ],
+ /* "initComplete": function(oSettings) {
+ $(this).on('click', "i.fa.fa-minus-square", function(e) {
+ table.row( $(this).closest('tr') ).remove().draw();});},*/
+ dom: 'Bfrtip',
+ buttons: [
+ {
+ text: 'Add item',
+ action: function ( e, dt, node, config ) {
+ addItemModal(listId);
+ }
+ },
+ {
+ text: 'Edit item',
+ action: function ( e, dt, node, config ) {
+ selectedItem = $.map(this.row('.selected').data(), function (item) {
+ console.log("Item " + item);
+ return item
+ });
+ elementUuid = selectedItem[5];
+ if(this.rows( '.selected' ).any()) {
+ editItemModal(listId, selectedItem[5], this);
+ this.row('.selected').remove().draw(false);
+ }
+ }
+ },
+ {
+ text: 'Delete item',
+ action: function ( e, dt, node, config ) {
+ selectedItem = $.map(this.row('.selected').data(), function (item) {
+ console.log("Item " + item);
+ return item
+ });
+ elementUuid = selectedItem[5];
+ deleteItem(selectedItem[5]);
+ this.row('.selected').remove().draw( false );
+ }
+ },
+ {
+ text: 'Delete List',
+ action: function ( e, dt, node, config ) {
+ //alert( 'Button activated' );
+ deleteListModal(listUuid);
+ }
+ }
+ ]
+ });
+ });
+ }
+ });
+}
+
+
+$( document ).ready(function() {
+ //get the lists belonging to this group
+ getLists().then(ret => {
+ console.log(ret);
+ console.log(ret['status']);
+ if (ret['status'] === 200) {
+ console.log("Creating group page lists");
+ const lists = ret['lists'];
+ console.log(lists);
+ listCount = lists.length;
+
+ //populate individual list
+ for(let j = 0; j < lists.length; j++)
+ {
+ listUuid = lists[j]['list-uuid'];
+
+ console.log(lists[j]['list-name']);
+ let listId = lists[j]['list-id'];
+ let listName = lists[j]['list-name'];
+ renderTable(getTableDivId(j), listName , listId, listUuid);
+ }
+ }
+ });
+
+
+});
+
diff --git a/img/Pitched.png b/img/Pitched.png
new file mode 100644
index 0000000..b862fd7
Binary files /dev/null and b/img/Pitched.png differ
diff --git a/img/code.png b/img/code.png
new file mode 100644
index 0000000..070b562
Binary files /dev/null and b/img/code.png differ
diff --git a/img/profile.png b/img/profile.png
new file mode 100644
index 0000000..95f0168
Binary files /dev/null and b/img/profile.png differ
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..928d0c9
--- /dev/null
+++ b/index.html
@@ -0,0 +1,91 @@
+
+
+
+
+ Camping Trip Planner
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Pitched helps groups organize trips in an intuitive way.
+
+
+
+
+
+
Sign up, it's free!
+
+
+
+
+
+
+
+
+
+
+
+
+
We love the open source community, so this entire project is open source!
+
Checkout our
+ website and
+ webservice repositories on GitHub!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..f4c2def
--- /dev/null
+++ b/index.js
@@ -0,0 +1,6 @@
+if (getCookie('active') === 'true') {
+ // Signed in
+ nav_signOut.style.display = "inline";
+ nav_profile.style.display = "inline";
+ nav_signIn.style.display = "none";
+}
\ No newline at end of file
diff --git a/login.html b/login.html
new file mode 100644
index 0000000..2c19957
--- /dev/null
+++ b/login.html
@@ -0,0 +1,140 @@
+
+
+
+
+ Sign In
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/login.js b/login.js
new file mode 100644
index 0000000..74ec96e
--- /dev/null
+++ b/login.js
@@ -0,0 +1,164 @@
+const urlParams = new URLSearchParams(window.location.search);
+const signupCheck = urlParams.get('signup');
+
+//--- Sign In Process ---//
+async function signInPost(data) {
+ // Make POST request submitting
+ // Add CORS header to allow cross origin resource sharing
+ const response = await fetch(serverAddress + '/login', {
+ method: 'POST',
+ credentials: 'include',
+ redirect: 'follow',
+ body: JSON.stringify(data)
+ });
+ // Wait for response from server, then parse the body of the response in json format
+ return await response.json();
+}
+
+function signIn() {
+ // Reset sign in warnings
+ warning_incorrectPass.style.display = 'none';
+ warning_recaptchaErrorSignIn.style.display = 'none';
+
+ // Get recaptcha response
+ const g_recaptcha_response = document.getElementById('g-recaptcha-response').value;
+
+ // Don't process request unless recaptcha is complete
+ if (g_recaptcha_response === '') {
+ warning_recaptchaErrorSignIn.style.display = 'inline';
+ return;
+ }
+
+ signInPost({
+ "email": document.getElementById('input_email').value,
+ "password": document.getElementById('input_password').value,
+ "recap_response": g_recaptcha_response
+ }).then(ret => {
+ if (ret['status'] === 401) {
+ // Unauthorized error
+ if (ret['error'] === 'incorrect-password') {
+ // Incorrect password
+ // Show incorrect password indicator
+ warning_incorrectPass.style.display = 'inline';
+ }
+
+ if(ret['error'] === 'failed-recaptcha') {
+ // recaptcha verification failed
+ warning_recaptchaErrorSignIn.style.display = 'inline';
+ }
+ grecaptcha.reset(); // Refresh captcha
+ } else {
+ // Logged in successful
+ // setCookie('active', 'true');
+ // sessionStorage.setItem('status','logged-in');
+ window.location.href="profile.html";
+ }
+ });
+}
+//--- End of sign in process ---//
+
+
+//--- Sign Up Process ---//
+function confirmPass() {
+ if (input_signupPass.value !== input_signupPassConf.value) {
+ warning_passwordConfirmation.style.display = 'inline';
+ } else {
+ warning_passwordConfirmation.style.display = 'none';
+ }
+}
+
+async function signUpPost(data) {
+ // Make POST request submitting new account
+ // Add CORS header to allow cross origin resource sharing
+ const response = await fetch(serverAddress + '/signup', {
+ method: 'POST',
+ credentials: 'include',
+ redirect: 'follow',
+ body: JSON.stringify(data)
+ });
+ // Wait for response from server, then parse the body of the response in json format
+ return await response.json();
+}
+
+function signUp() {
+ validateForm();
+
+ // Reset sign in warnings
+ warning_recaptchaErrorSignUp.style.display = 'none';
+
+ // Get recaptcha response
+ const g_recaptcha_response = document.getElementById('g-recaptcha-response-1').value;
+
+ // Don't process request unless recaptcha is complete
+ if (g_recaptcha_response === '') {
+ warning_recaptchaErrorSignUp.style.display = 'inline';
+ return;
+ }
+
+ signUpPost({
+ "first_name": document.getElementById('input_user-first-name').value,
+ "last_name": document.getElementById('input_user-last-name').value,
+ "email": document.getElementById('input_user-email').value,
+ "password": input_signupPass.value,
+ "password_conf": input_signupPassConf.value,
+ "recap_response": g_recaptcha_response
+ }).then(ret => {
+ if (ret['status'] === 401) {
+ // Unauthorized error
+ if (ret['error'] === 'incorrect-password') {
+ // Incorrect password
+ // Show incorrect password indicator
+ warning_incorrectPass.style.display = 'inline';
+ } else if(ret['error'] === 'failed-recaptcha') {
+ // recaptcha verification failed
+ warning_recaptchaErrorSignIn.style.display = 'inline';
+ }
+ grecaptcha.reset(); // Refresh captcha
+ } else {
+ // Logged in successful
+ window.location.href="profile.html";
+ }
+ });
+}
+
+//check if forms are valid
+function validateForm() {
+ let firstName = document.getElementById("input_user-first-name").value;
+ let lastName = document.getElementById("input_user-last-name").value;
+ let email = document.getElementById("input_user-email").value;
+
+ if (firstName == "") {
+ alert("First name must be filled out");
+ return false;
+ }
+ if (lastName == "") {
+ alert("Last name must be filled out");
+ return false;
+ }
+ if (email == "") {
+ alert("Email must be filled out");
+ return false;
+ }
+ if (document.getElementById("input_user-pass").value == "") {
+ alert("Password must be filled out");
+ return false;
+ }
+ if (document.getElementById("input_user-confirm-pass").value == "") {
+ alert("Please confirm password");
+ return false;
+ }
+
+ var reg = /^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/;
+
+ if (reg.test(email) == false)
+ {
+ alert('Invalid Email Address');
+ return false;
+ }
+}
+
+//--- END SIGN UP PROCESS ---//
+
+if (signupCheck === 'true') {
+ $('#modal_sign-up').modal('toggle');
+}
\ No newline at end of file
diff --git a/postman.png b/postman.png
new file mode 100644
index 0000000..456548e
Binary files /dev/null and b/postman.png differ
diff --git a/profile.html b/profile.html
new file mode 100644
index 0000000..d885931
--- /dev/null
+++ b/profile.html
@@ -0,0 +1,113 @@
+
+
+
+
+ Camping Trip Planner - Profile
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Create Group
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/profile.js b/profile.js
new file mode 100644
index 0000000..5fefcd9
--- /dev/null
+++ b/profile.js
@@ -0,0 +1,230 @@
+// Check if user is signed in before displaying a profile page
+if (getCookie('active') === 'true') {
+ // Signed in
+ nav_signOut.style.display = "inline";
+ nav_profile.style.display = "inline";
+ nav_signIn.style.display = "none";
+} else {
+ // Redirect to login page
+ window.location.href="login.html";
+}
+
+function createGroupInviteListItem(groupName, inviteFrom, requestUuid) {
+ const newListItem = document.createElement('li');
+ newListItem.classList.add('list-group-item');
+
+ const textDiv = document.createElement('div');
+ textDiv.style.width = '70%';
+ textDiv.style.float = 'left';
+
+ const groupNameText = document.createElement('p');
+ groupNameText.classList.add('md1');
+ groupNameText.innerText = groupName;
+
+ const groupInvitedByText = document.createElement('p');
+ groupInvitedByText.classList.add('small');
+ groupInvitedByText.style.marginBottom = '0';
+ groupInvitedByText.innerText= inviteFrom;
+
+ const buttonDiv = document.createElement('div');
+ buttonDiv.style.width = '30%';
+ buttonDiv.style.float = 'right';
+
+ const acceptButton = document.createElement('button');
+ acceptButton.onclick = function() { acceptGroupInvite(requestUuid, newListItem) };
+ acceptButton.innerText = 'Accept';
+ acceptButton.classList.add('group-accept-button');
+ acceptButton.classList.add('btn');
+ acceptButton.classList.add('btn-success');
+ acceptButton.classList.add('btn-sm');
+ acceptButton.type = 'button';
+
+ const declineButton = document.createElement('button');
+ declineButton.onclick = function() { declineGroupInvite(requestUuid, newListItem) };
+ declineButton.innerText = 'Decline';
+ declineButton.classList.add('group-decline-button');
+ declineButton.classList.add('btn');
+ declineButton.classList.add('btn-danger');
+ declineButton.classList.add('btn-sm');
+ declineButton.type = 'button';
+
+ newListItem.appendChild(textDiv);
+ textDiv.appendChild(groupNameText);
+ textDiv.appendChild(groupInvitedByText);
+ newListItem.appendChild(buttonDiv);
+ buttonDiv.appendChild(acceptButton);
+ buttonDiv.appendChild(declineButton);
+
+ return newListItem;
+}
+
+function createGroupListItem(groupName, groupUuid) {
+ const listItem = document.createElement('li');
+ listItem.classList.add('list-group-item');
+
+ const listTitle = document.createElement('p');
+ listTitle.classList.add('md1');
+ listTitle.style.marginBottom = '0';
+
+ const listLink = document.createElement('a');
+ listLink.href = 'group.html?group-uuid=' + groupUuid + '&group-name=' + groupName;
+ listLink.textContent = groupName;
+
+ listTitle.appendChild(listLink);
+ listItem.appendChild(listTitle);
+
+ return listItem;
+}
+
+async function getGroupRequests() {
+ const response = await fetch(serverAddress + '/getGroupInvites', {
+ method: 'GET',
+ credentials: 'include',
+ redirect: 'follow'
+ });
+ return await response.json();
+}
+
+async function getGroupList() {
+ const response = await fetch(serverAddress + '/getGroupsByUser', {
+ method: 'GET',
+ credentials: 'include',
+ redirect: 'follow'
+ });
+ return await response.json();
+}
+
+async function acceptGroupInviteRequest(requestUuid) {
+ const response = await fetch(serverAddress + '/acceptGroupInvite', {
+ method: 'POST',
+ credentials: 'include',
+ body: JSON.stringify({'request-uuid': requestUuid})
+ });
+ return await response.json();
+}
+
+async function declineGroupInviteRequest(requestUuid) {
+ const response = await fetch(serverAddress + '/declineGroupInvite', {
+ method: 'POST',
+ credentials: 'include',
+ redirect: 'follow',
+ body: JSON.stringify({'request-uuid': requestUuid})
+ });
+ return await response.json();
+}
+
+function checkGroupInvite() {
+ if ($('#group-invite-list').children().length === 0) {
+ $("#new-group-requests").hide();
+ }
+}
+
+function declineGroupInvite(requestUuid, buttonElement) {
+ $("#loader").show();
+ declineGroupInviteRequest(requestUuid).then(ret => {
+ if (ret['status'] === 200) {
+ buttonElement.remove();
+ }
+ console.log(ret);
+ checkGroupInvite();
+ $("#loader").hide();
+ });
+}
+
+function acceptGroupInvite(requestUuid, buttonElement) {
+ $("#loader").show();
+ acceptGroupInviteRequest(requestUuid).then(ret => {
+ if (ret['status'] === 200) {
+ buttonElement.remove();
+ }
+ console.log(ret);
+ checkGroupInvite();
+ generateGroupList();
+ $("#loader").hide();
+ });
+}
+
+async function newGroupPost(data) {
+ console.log(data);
+ const response = await fetch(serverAddress + '/createGroup', {
+ method: 'POST',
+ credentials: 'include',
+ redirect: 'follow',
+ body: JSON.stringify(data)
+ });
+ return await response.json();
+}
+
+function newGroup() {
+ $("#loader").show();
+ newGroupPost({
+ 'group-name': document.getElementById('input_user-group-name').value,
+ }).then(ret => {
+ if (ret['status'] === 200) {
+ $('#modal_new-group').modal('hide');
+ window.location.reload(true);
+ }
+ console.log(ret);
+ checkGroupInvite();
+ $("#loader").hide();
+ });
+}
+
+
+function generateGroupList() {
+ const groupList = document.getElementById('group-list')
+ while (groupList.firstChild) {
+ groupList.removeChild(groupList.firstChild);
+ }
+
+ getGroupList().then(ret => {
+ console.log(ret);
+ console.log(ret['status']);
+ if (ret['status'] === 200) {
+ console.log("Creating group list");
+ const groups = ret['groups'];
+ console.log(groups);
+
+ for (let i = 0; i < groups.length; i++) {
+ document.getElementById('group-list').appendChild(
+ createGroupListItem(groups[i]['group-name'], groups[i]['group-uuid'])
+ )
+ }
+ }
+ });
+}
+
+
+$( document ).ready(function() {
+
+
+ getGroupRequests().then(ret => {
+ if (ret['status'] === 200) {
+ const invites = ret['invites'];
+ console.log(invites);
+
+ if (invites.length > 0) {
+ $("#new-group-requests").show();
+ }
+
+ for (let i = 0; i < invites.length; i++) {
+ document.getElementById('group-invite-list').appendChild(
+ createGroupInviteListItem(invites[i]['group-name'], invites[i]['invite-from'], invites[i]['request-uuid'])
+ )
+ }
+ }
+ });
+
+ generateGroupList();
+
+ const name = getCookie("full_name");
+ if (name != null) {
+ $("#profile-name").text(name);
+ // document.getElementById('profile-name').value = name;
+ }
+
+ $("#loader").hide();
+
+});
+
+
diff --git a/script.js b/script.js
new file mode 100644
index 0000000..b4c9836
--- /dev/null
+++ b/script.js
@@ -0,0 +1,84 @@
+const serverAddress = "https://camping.sebtota.com:5000"
+
+
+const nav_signIn = document.getElementById('nav_sign-in')
+const nav_signOut = document.getElementById('nav_sign-out')
+const nav_profile = document.getElementById('nav_profile')
+
+const warning_incorrectPass = document.getElementById('label_incorrect-password');
+const warning_recaptchaErrorSignIn = document.getElementById('label_recaptcha-error-signin');
+const warning_recaptchaErrorSignUp = document.getElementById('label_recaptcha-error-signup');
+
+const warning_passwordConfirmation = document.getElementById('label__pass_conf');
+
+const input_signupPass = document.getElementById("input_user-pass");
+const input_signupPassConf = document.getElementById("input_user-confirm-pass");
+
+
+async function createRequest(route, method='POST', data = undefined) {
+ let response;
+
+ if (data === undefined) {
+ response = await fetch(serverAddress + route, {
+ method: method,
+ redirect: 'follow',
+ credentials: 'include'
+ });
+ } else {
+ response = await fetch(serverAddress + route, {
+ method: method,
+ redirect: 'follow',
+ credentials: 'include',
+ body: JSON.stringify(data)
+ });
+ }
+
+ return await response.json();
+}
+
+// Split and iterate over the cookie string, returning the cookie value if found
+function getCookie(cookieName) {
+ const cookies = document.cookie.split("; ");
+
+ // Iterate over every cookie in the cookie string
+ for (let i = 0; i < cookies.length; i++) {
+ let cookieObj = cookies[i].split("=");
+ if (cookieObj[0] === cookieName) {
+ // Remove double quotes from string if they exist
+ let retCookie = cookieObj[1];
+ if (retCookie[0] === '"') {
+ return retCookie.substring(1, retCookie.length - 1);
+ } else {
+ return retCookie;
+ }
+ }
+ }
+
+ // No cookie found with that name
+ return null;
+}
+
+//--- Sign Out Process ---//
+async function signOutPost() {
+ const response = await fetch(serverAddress + '/logout', {
+ method: 'POST',
+ redirect: 'follow',
+ credentials: 'include'
+ });
+ return await response.json();
+}
+
+function signOut() {
+ signOutPost().then(ret => {
+ console.log(ret);
+ console.log("signed out");
+ $("#loader").hide();
+
+ window.location.href="index.html";
+ })
+}
+
+$('#nav_sign-out').on('click',function (e){
+ e.preventDefault();
+ signOut();
+});
diff --git a/style.css b/style.css
new file mode 100644
index 0000000..ff89297
--- /dev/null
+++ b/style.css
@@ -0,0 +1,333 @@
+body {
+ padding-bottom: 50px;
+}
+
+.logo {
+ margin-bottom: 50px;
+}
+
+#form_signin {
+ width: 300px;
+ margin: auto;
+}
+#header {
+ margin-bottom: 10px;
+}
+
+#intro-sign-up-btn {
+ width: 250px;
+}
+
+@media(max-width: 400px) {
+ #intro-sign-up-btn {
+ width: 150px;
+ }
+}
+
+#intro-div {
+ font-family: 'Montserrat', sans-serif;
+ width: 100%;
+ height: 100vh;
+}
+
+#form_signup {
+ width: 300px;
+ margin: auto;
+}
+
+.btn_sign-in {
+ width: 100% !important;
+ margin-bottom: 10px;
+}
+
+.warning-text {
+ color: #ff0000;
+}
+
+.center {
+ text-align: center;
+}
+
+.group-header-center {
+ text-align: center;
+ margin-top:116px;
+}
+
+.center-div {
+ margin: auto;
+ width: 100%;
+ padding: 10px;
+}
+
+.center-vert {
+ width: 100%;
+ position: relative;
+ top: 50%;
+ transform: translateY(-50%);
+}
+
+.hidden {
+ display: none;
+}
+
+.group-accept-button {
+ width: 100%;
+ margin-bottom: 3px;
+}
+
+.group-decline-button {
+ width: 100%;
+}
+
+.loader-div {
+ width: 100%;
+ height: 100vh;
+ position: fixed;
+}
+
+.loader {
+ width: 4em !important;
+ height: 4em !important;
+ left: 50%;
+ top: 50%;
+ margin-left: -2em;
+ margin-top: -2em;
+ position: fixed !important;
+}
+
+#group-data-container {
+ max-width: 500px;
+ margin: auto;
+}
+
+#group-name-display {
+ width: 100%;
+ text-align: center;
+ font-size: 30px;
+ height: 50px;
+ resize: none;
+ overflow: hidden;
+ border: none;
+ overflow-x: scroll;
+}
+
+/* Change element modal */
+#element-change-header {
+ padding-left: 20px;
+ padding-top: 20px;
+ padding-bottom: 0;
+ border-bottom: 0;
+}
+
+#element-change-modal-body {
+ padding-left: 20px;
+ padding-top: 0;
+}
+
+#element-change-name {
+ width: 100%;
+ background: transparent;
+ font-size: 20px;
+ font-weight: 600;
+ line-height: 25px;
+ height: 30px;
+ resize: none;
+ overflow: hidden;
+ border: none;
+}
+
+#element-change-list-display {
+ font-size: 14px;
+}
+
+#element-change-description {
+ min-height: 100px;
+ max-height: 350px;
+ width: 100%;
+ resize: none;
+ overflow-y: scroll;
+}
+
+.element-change-modal-label {
+ padding-bottom: 5px;
+ margin-bottom: 0;
+}
+
+.circle {
+ height: 25px;
+ width: 25px;
+ background-color: #8d8686;
+ border-radius: 50%;
+ display: inline-block;
+}
+
+/* Display users belonging to a group */
+#group-user-display {
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+ overflow: hidden;
+}
+
+.group-user {
+ margin-right: 5px;
+ width: 40px;
+ height: 40px;
+ font-size: 15px;
+ line-height: 40px;
+ text-align: center;
+ background-color: lightgray;
+ border-radius: 40px;
+ float: left;
+ display: block;
+ text-decoration: none;
+}
+.ui {
+ height: 100vh;
+ display: grid;
+}
+
+.listHorizontal {
+ color: #b6d8cf;
+ font-size:30px;
+ padding:0;
+ text-transform: uppercase;
+ list-style-type: none;
+ display:flex;
+ justify-content:space-evenly;
+}
+
+.list-inline > div{
+ float: left;
+ display: inline;
+}
+
+table.dataTable {
+ margin: 0 auto;
+ clear: both;
+ width: 100%;
+}
+
+table.dataTable thead th {
+ padding: 3px 18px 3px 10px;
+ border-bottom: 1px solid black;
+ font-weight: bold;
+ cursor: pointer;
+ *cursor: hand;
+}
+
+table.dataTable tfoot th {
+ padding: 3px 18px 3px 10px;
+ border-top: 1px solid black;
+ font-weight: bold;
+}
+
+table.dataTable td {
+ padding: 3px 10px;
+}
+
+table.dataTable td.center,
+table.dataTable td.dataTables_empty {
+ text-align: center;
+}
+
+table.dataTable tr.odd { background-color: #E2E4FF; }
+table.dataTable tr.even { background-color: white; }
+
+table.dataTable tr.odd td.sorting_1 { background-color: #D3D6FF; }
+table.dataTable tr.odd td.sorting_2 { background-color: #DADCFF; }
+table.dataTable tr.odd td.sorting_3 { background-color: #E0E2FF; }
+table.dataTable tr.even td.sorting_1 { background-color: #EAEBFF; }
+table.dataTable tr.even td.sorting_2 { background-color: #F2F3FF; }
+table.dataTable tr.even td.sorting_3 { background-color: #F9F9FF; }
+
+
+/*
+ * Table wrapper
+ */
+.dataTables_wrapper {
+ position: relative;
+ clear: both;
+ *zoom: 1;
+}
+
+
+/*
+ * Page length menu
+ */
+.dataTables_length {
+ float: left;
+}
+
+
+/*
+ * Filter
+ */
+.dataTables_filter {
+ float: right;
+ text-align: right;
+}
+
+
+/*
+ * Table information
+ */
+.dataTables_info {
+ clear: both;
+ float: left;
+}
+
+/*
+ * Processing indicator
+ */
+.dataTables_processing {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 250px;
+ height: 30px;
+ margin-left: -125px;
+ margin-top: -15px;
+ padding: 14px 0 2px 0;
+ border: 1px solid #ddd;
+ text-align: center;
+ color: #999;
+ font-size: 14px;
+ background-color: white;
+}
+
+table.dataTable th:active {
+ outline: none;
+}
+
+
+/*
+ * Scrolling
+ */
+.dataTables_scroll {
+ clear: both;
+}
+
+.dataTables_scrollBody {
+ *margin-top: -1px;
+}
+
+#new_list {
+ width: 300px;
+ margin: auto;
+}
+
+#new_item {
+ width: 300px;
+ margin: auto;
+}
+
+#delete_list {
+ width: 300px;
+ margin: auto;
+}
+
+#new_group{
+ width: 300px;
+ margin: auto;
+}