diff --git a/README.md b/README.md
index 15ca32e1bc..94406b3a57 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,24 @@
# Advanced Web Applications Sprint Challenge
+- [Advanced Web Applications Sprint Challenge](#advanced-web-applications-sprint-challenge)
+ - [Overview](#overview)
+ - [Introduction](#introduction)
+ - [Instructions](#instructions)
+ - [Task 1: Project Setup](#task-1-project-setup)
+ - [Task 2: CodeGrade Setup](#task-2-codegrade-setup)
+ - [Task 3: Project Requirements](#task-3-project-requirements)
+ - [Authentication](#authentication)
+ - [Consuming the API](#consuming-the-api)
+ - [Testing](#testing)
+ - [Reference Materials](#reference-materials)
+ - [API Documentation](#api-documentation)
+ - [Hex Color Examples](#hex-color-examples)
+ - [Submission format](#submission-format)
+ - [Interview Questions](#interview-questions)
+ - [Rick Mansfield's push/pull trail for Advanced Web Apps Sprint](#rick-mansfields-pushpull-trail-for-advanced-web-apps-sprint)
+
+## Overview
+
**Read these instructions carefully. Understand exactly what is expected _before_ starting this Sprint Challenge.**
This challenge allows you to practice the concepts and techniques learned over the past sprint and apply them in a concrete project. This sprint explored **advanced web applications**. During this sprint, you studied **React testing, client-side auth, HTTP methods, and Vercel**.
@@ -48,21 +67,24 @@ Your finished project must include all of the following requirements. **Unlike o
* [ ] Build in functionality that would allow an error to be displayed in the provided p tag if either the username or password is incorrect.
* [ ] **Make sure your error p tag has an id="error" attribute attached. Codegrade autotests will fail without them.**
* [ ] Construct an http request that retrieves an auth token from the server when the username `Lambda` and the password `School` is passed into the request.
-* [ ] Save the token to localStorage.
-* [ ] Build a `axiosWithAuth` module within the helpers folder to create an instance of axios with the authentication header.
-* [ ] Build a `PrivateRoute` component within the components folder and use it to protect the route that renders the `BubblesPage` component.
-* [ ] In `App.js`, build the backend to the logout button. When pressed, send an http request to the logout endpoint and remove the authentication token from localStorage. Use window.location.href to redirect to the login page.
+* [x] Save the token to localStorage.
+* [x] Build a `axiosWithAuth` module within the helpers folder to create an instance of axios with the authentication header.
+* [x] Build a `PrivateRoute` component within the components folder and use it to protect the route that renders the `BubblesPage` component.
+* [x] In `App.js`, build the backend to the logout button. When pressed, send an http request to the logout endpoint and remove the authentication token from localStorage. Use window.location.href to redirect to the login page.
#### Consuming the API
> *Add in the http requests and state changes needed to connect our api to the web application. Consider the effect of authentication on your api requests.*
-* [ ] In `services/fetchColorServices.js`, build out fetchColorService function to make a GET request to fetch the color data for your bubbles.
-* [ ] When `BubblePages` mounts, call fetchColorServices and save it's result in state.
-* [ ] In `BubblePage.js`, complete `saveEdit`, and `deleteColor` functions to make API requests for to editing and delete data.
-* [ ] Watch and enjoy as your app responds to updates in the data. Check out `Bubbles.js` to see how this is built.
+* [x] In `services/fetchColorServices.js`, build out fetchColorService function to make a GET request to fetch the color data for your bubbles.
+* [x] When `BubblePage` mounts, call fetchColorServices and save it's result in state.
+* [x] In `BubblePage.js`, complete `saveEdit`, and `deleteColor` functions to make API requests for to editing and delete data.
+* [x] Watch and enjoy as your app responds to updates in the data. Check out `Bubbles.js` to see how this is built.
#### Testing
* [ ] Finish the test in `Color.test.js`, `ColorList.test.js`, `BubblePage.test.js`. You will need to use rerendering, function mocking and spies in order to complete.
+* [ ] * [ ] Note to self ... I used this data
+ ![res.data for colors](src\assets\Capture1.JPG)
+ view in readme.md preview mode to see pic
**Notes:**
* You are welcome to create additional files but **do not move or rename existing files** or folders.
@@ -100,4 +122,8 @@ Be prepared to demonstrate your understanding of this week's concepts by answeri
1. Explain what a token is used for.
2. What steps can you take in your web apps to keep your data secure?
3. Describe how web servers work.
-4. Which HTTP methods can be mapped to the CRUD acronym that we use when interfacing with APIs/Servers.
\ No newline at end of file
+4. Which HTTP methods can be mapped to the CRUD acronym that we use when interfacing with APIs/Servers.
+
+## Rick Mansfield's push/pull trail for Advanced Web Apps Sprint
+
+- [Link for convenience](https://github.com/LambdaSchool/web-sprint-challenge-advanced-web-applications/pull/141)
\ No newline at end of file
diff --git a/src/App.js b/src/App.js
index 4054971a97..fbd60f8db6 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,17 +1,32 @@
-import React, { useState } from "react";
-import { BrowserRouter as Router, Route } from "react-router-dom";
+import React from "react";
+import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
+import PrivateRoute from './components/PrivateRoute';
+import BubblePage from './components/BubblePage';
import Login from "./components/Login";
import "./styles.scss";
+//3A Build the logout button to call the logout endpoint, remove the localStorage Item and redirect to the login page.
+const logout = () => {
+ localStorage.removeItem("token");
+ window.location.href = "login";
+}
function App() {
return (
Color Picker Sprint Challenge
- logout
+ {/* //3b. Build the logout button to call the logout endpoint, remove the localStorage Item and redirect to the login page. */}
+ logout
+
+ {/* //2. Render BubblePage as a PrivateRoute */}
+
+ {/* //1. Add in two routes that link to the Login Component, one for the default path '/' and one for the '/login'. */}
+
+
+
);
diff --git a/src/assets/Capture1.JPG b/src/assets/Capture1.JPG
new file mode 100644
index 0000000000..525f542d3c
Binary files /dev/null and b/src/assets/Capture1.JPG differ
diff --git a/src/assets/cg1.JPG b/src/assets/cg1.JPG
new file mode 100644
index 0000000000..897d8629f0
Binary files /dev/null and b/src/assets/cg1.JPG differ
diff --git a/src/assets/cg2.JPG b/src/assets/cg2.JPG
new file mode 100644
index 0000000000..63f1347eb6
Binary files /dev/null and b/src/assets/cg2.JPG differ
diff --git a/src/assets/cg3.JPG b/src/assets/cg3.JPG
new file mode 100644
index 0000000000..ace76a1692
Binary files /dev/null and b/src/assets/cg3.JPG differ
diff --git a/src/assets/cg4.JPG b/src/assets/cg4.JPG
new file mode 100644
index 0000000000..9461d46e5c
Binary files /dev/null and b/src/assets/cg4.JPG differ
diff --git a/src/assets/terminal-1.JPG b/src/assets/terminal-1.JPG
new file mode 100644
index 0000000000..b59ad651e2
Binary files /dev/null and b/src/assets/terminal-1.JPG differ
diff --git a/src/components/BubblePage.js b/src/components/BubblePage.js
index 47a88f2291..cda85f0fe6 100644
--- a/src/components/BubblePage.js
+++ b/src/components/BubblePage.js
@@ -3,19 +3,37 @@ import React, { useEffect, useState } from "react";
import Bubbles from "./Bubbles";
import ColorList from "./ColorList";
import fetchColorService from '../services/fetchColorService';
+import axiosWithAuth from "../helpers/axiosWithAuth";
+import editColorService from "../services/saveEditService";
+import deleteColorService from "../services/deleteColorService";
-const BubblePage = () => {
+const BubblePage = (props) => {
const [colors, setColors] = useState([]);
const [editing, setEditing] = useState(false);
+ //1. When the component mounts, make an axios call to retrieve all color data and push to state.useEffect
+ useEffect(()=>{
+ axiosWithAuth()
+ fetchColorService(setColors);
+ },[editing]);//inserted "editing to prevent memory leak"
+
const toggleEdit = (value) => {
setEditing(value);
};
+
+//2. Complete toggleEdit, saveEdit, deleteColor and functions
const saveEdit = (editColor) => {
+ editColorService(editColor);
+ fetchColorService(setColors);
+ props.history.push('/bubbles');
+ toggleEdit(false)
};
const deleteColor = (colorToDelete) => {
+ console.log('colorToDelete Obj in BubblePage.js', colorToDelete);
+ deleteColorService(colorToDelete.id);
+ fetchColorService(setColors);
};
return (
diff --git a/src/components/BubblePage.test.js b/src/components/BubblePage.test.js
index eeaef0c197..feb17a7cb4 100644
--- a/src/components/BubblePage.test.js
+++ b/src/components/BubblePage.test.js
@@ -1,13 +1,31 @@
import React from 'react';
-import MutationObserver from 'mutationobserver-shim';
-
-import { render, screen} from "@testing-library/react";
+import { render, screen, waitFor} from "@testing-library/react";
import BubblePage from './BubblePage';
+import ColorList from './ColorList';
+
+const testColors = [
+ {code: {hex: "##ffebcd"},
+ color: "blanchedalmond",
+ id: 8},
+ {code: {hex: "#6093ca"},
+ color: "blue",
+ id: 9},
+ {code: {hex: "#ff000"},
+ color: "red",
+ id: 10}
+];
test("Renders without errors", ()=> {
-
+ render()
});
test("Renders appropriate number of colors passed in through mock", async ()=> {
//Keep in mind that our service is called on mount for this component.
-});
\ No newline at end of file
+ const mockColorsFunc = jest.fn(() => { return (testColors) });
+ render();
+
+ await waitFor(()=>{
+ const correctColorsReturned = screen.getAllByTestId(/color/i);
+ expect(correctColorsReturned).toHaveLength(3);
+ })
+});
diff --git a/src/components/Color.test.js b/src/components/Color.test.js
index def97c5555..0e8d0f7204 100644
--- a/src/components/Color.test.js
+++ b/src/components/Color.test.js
@@ -5,15 +5,90 @@ import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Color from './Color';
+//I inpspected the res.data to see the needed syntax/structure.See my pic of this data in readme.md preview mode under testing. I created a TOC Table of Contents for easy reference.
test("Renders without errors with blank color passed into component", () => {
+ render(
+
+ );
});
-
+
test("Renders the color passed into component", () => {
-});
+ //Arrange - Usually Render/const/screen to prep for Action step
+ render(
+
+ );
+ //Act - Usually const/screen for something
+ let testColor = screen.getByText(/limegreen/i)
+ //Assert - What's "expected"
+ expect(testColor).toBeInTheDocument();
+});
test("Executes handleDelete and toggleEdit property when the 'x' icon is clicked", () => {
+ //Arrange - Usually Render/const/screen to prep for Action step (review Day 1 (Monday) testing Display.test.js)
+ const mockToggleFunc = jest.fn();
+ const mockDeleteFunc = jest.fn();
+ render(
+
+ );
+ let theXToDelete = screen.getByText(/x/i);//it says "icon" on line 39 but it's a text letter "x".
+
+ //Act - Usually doing something to what you just const/screened/arranged
+ userEvent.click(theXToDelete);
+
+ //Assert - What's "expected"
+ expect(mockToggleFunc).toHaveBeenCalled();
+ expect(mockDeleteFunc).toHaveBeenCalled();
+ expect(mockDeleteFunc.mock.calls).toHaveLength(1);
+ expect(mockToggleFunc.mock.calls).toHaveLength(1);
});
test("Executes setEditColor and toggleEdit property when color div is clicked", () => {
+ //Arrange - Usually Render/const/screen to prep for Action step
+ const mocksetEditColorFunc = jest.fn();
+ const mockToggleEditFunc = jest.fn();
+
+ render(
+
+ );
+ let theColorDiv = screen.getByTestId(/color/i);
+ // let theEditMenu = screen.findByTestId(/edit_menu/i)
+
+ //Act - Usually doing something to what you just const/screened/arranged
+ userEvent.click(theColorDiv);
+
+ // //Assert - What's "expected"
+ expect(mockToggleEditFunc).toHaveBeenCalled();
+ // // expect(mockToggleEditFunc).toHaveLength(1);
+ // // expect(theEditMenu).toBeTruthy();
+ expect(mocksetEditColorFunc).toHaveBeenCalled();
+ // expect(mocksetEditColorFunc).toHaveLength(1);
});
\ No newline at end of file
diff --git a/src/components/ColorList.test.js b/src/components/ColorList.test.js
index 115b169331..66a40e5a6b 100644
--- a/src/components/ColorList.test.js
+++ b/src/components/ColorList.test.js
@@ -1,14 +1,50 @@
import React from 'react';
-import MutationObserver from 'mutationobserver-shim';
-
import { render, screen} from "@testing-library/react";
-import ColorList from './ColorList';
+import ColorList from './ColorList'
+
+const testColors = [
+ {code: {hex: "##ffebcd"},
+ color: "blanchedalmond",
+ id: 8},
+ {code: {hex: "#6093ca"},
+ color: "blue",
+ id: 9},
+ {code: {hex: "#ff000"},
+ color: "red",
+ id: 10}
+];
test("Renders an empty list of colors without errors", () => {
+ render()
});
test("Renders a list of colors without errors", () => {
+ render()
});
+
test("Renders the EditForm when editing = true and does not render EditForm when editing = false", () => {
-});
+ // //Arrange - Usually Render/const/screen to prep for Action step
+ const { rerender } = render()
+ // //See/inspect DOM for variable name choices.
+ const editColorLegend = screen.getByText(/edit color/i);
+ const labelForColorName = screen.getByText(/color name/i);
+ const labelForColorHex = screen.getByText(/hex code/i);
+
+ // //Act - Usually doing something to what you just const/screened/arranged
+ // //in this case the form should be true and therefore all the above would be present.So not additional actions are needed after editing = true which I've set above in line
+
+
+ // //Assert - What's "expected" from the actions taken
+ expect(editColorLegend).toBeInTheDocument()
+ expect(labelForColorName).toBeInTheDocument();
+ expect(labelForColorHex ).toBeInTheDocument();
+
+ // //ReArrange for "falsey" conditions- Usually Render/const/screen to prep for Action step
+ rerender();
+ // // //Act - Usually doing something to what you just const/screened/arranged
+ // // //Assert - What's "expected" from the actions taken
+ expect(editColorLegend).not.toBeInTheDocument();
+ expect(labelForColorName).not.toBeInTheDocument();
+ expect(labelForColorHex).not.toBeInTheDocument();
+ });
\ No newline at end of file
diff --git a/src/components/Login.js b/src/components/Login.js
index 8df9390633..2cc36cc0f8 100644
--- a/src/components/Login.js
+++ b/src/components/Login.js
@@ -1,20 +1,78 @@
-import React from "react";
+import React, { useState } from "react";
+import axios from 'axios';
-const Login = () => {
- // make a post request to retrieve a token from the api
- // when you have handled the token, navigate to the BubblePage route
- const error = "";
- //replace with error state
+const Login = (props) => {
+ //2. Add whatever state necessary for form functioning.
+ const [credentials, setCredentials] = useState({ username: "", password: "" })
+
+ // const error = ""; //replace with error state
+ const [error, setError] = useState("");
+
+ const handleChange = e => {
+ setCredentials({
+ ...credentials,
+ [e.target.name]: e.target.value
+ });
+ };
+
+ const submit = e => {
+ e.preventDefault();
+ //4. If either the username or password is not entered, display the following words with the p tag provided: Username or Password not valid.
+ if (!credentials.username || !credentials.password) {
+ return (setError("Username or Password not valid."));
+ }
+
+ // make a post request to retrieve a token from the api
+ axios.post('http://localhost:5000/api/login', credentials)
+ .then(res => {
+ //5. If the username / password is equal to "Lambda" / "School", save that token to localStorage and redirect to a BubblePage route.
+ localStorage.setItem("token", res.data.payload);
+ // when you have handled the token, navigate to the BubblePage route
+ props.history.push('/bubbles');
+ })
+ .catch(err => {
+ setError("Error logging in");
+ })
+ };
return (
Welcome to the Bubble App!
-
Build login form here
-
+ {/*
Build login form here
*/}
+ {/* //1. Build a form containing a username and password field. */}
+
+
+ {/* //8. MAKE SURE YOUR ERROR p tag contains the id="error" */}
+ {error}
);
};
@@ -22,10 +80,7 @@ const Login = () => {
export default Login;
//Task List:
-//1. Build a form containing a username and password field.
-//2. Add whatever state necessary for form functioning.
-//4. If either the username or password is not entered, display the following words with the p tag provided: Username or Password not valid.
-//5. If the username / password is equal to "Lambda" / "School", save that token to localStorage and redirect to a BubblePage route.
-//6. MAKE SURE YOUR USERNAME AND PASSWORD INPUTS INCLUDE id="username" and id="password"
-//7. MAKE SURE YOUR SUBMIT BUTTON INCLUDES id="submit"
-//8. MAKE SURE YOUR ERROR p tag contains the id="error"
\ No newline at end of file
+
+//3??? missing
+
+
diff --git a/src/components/PrivateRoute.js b/src/components/PrivateRoute.js
index e718313bdf..ca8b875fe8 100644
--- a/src/components/PrivateRoute.js
+++ b/src/components/PrivateRoute.js
@@ -1,2 +1,15 @@
//Task List:
-//1. Build a PrivateRoute component that redirects if user is not logged in
\ No newline at end of file
+//1. Build a PrivateRoute com//Task List:
+//1. Build a PrivateRoute component that redirects if user is not logged in
+import React from 'react';
+import { Route, Redirect } from 'react-router-dom';
+
+const PrivateRoute = ({component:Component, ...rest}) => {
+ if (localStorage.getItem("token")) {
+ return
+ } else {
+ return
+ }
+}
+
+export default PrivateRoute;
\ No newline at end of file
diff --git a/src/helpers/axiosWithAuth.js b/src/helpers/axiosWithAuth.js
index 1da418ff86..29c07554bc 100644
--- a/src/helpers/axiosWithAuth.js
+++ b/src/helpers/axiosWithAuth.js
@@ -1,4 +1,16 @@
import axios from "axios";
//Task List:
-//Build and export a function used to send in our authorization token
\ No newline at end of file
+//Build and export a function used to send in our authorization token
+const axiosWithAuth = () => {
+ const token = localStorage.getItem('token');
+
+ return axios.create({
+ headers: {
+ Authorization: token
+ },
+ baseURL: 'http://localhost:5000'
+ })
+}
+
+export default axiosWithAuth;
\ No newline at end of file
diff --git a/src/services/deleteColorService.js b/src/services/deleteColorService.js
new file mode 100644
index 0000000000..76b0f0f8c2
--- /dev/null
+++ b/src/services/deleteColorService.js
@@ -0,0 +1,9 @@
+import axiosWithAuth from "../helpers/axiosWithAuth";
+
+const deleteColorService = (colorToDeleteID) =>{
+ console.log('colorToDeleteID is:', colorToDeleteID);
+ axiosWithAuth()
+ .delete(`api/colors/${colorToDeleteID}`)
+}
+
+export default deleteColorService;
\ No newline at end of file
diff --git a/src/services/fetchColorService.js b/src/services/fetchColorService.js
index a914deb477..34f86b7022 100644
--- a/src/services/fetchColorService.js
+++ b/src/services/fetchColorService.js
@@ -1,7 +1,17 @@
import axiosWithAuth from '../helpers/axiosWithAuth';
-const fetchColorService = () => {
+const fetchColorService = (setColors) => {
+ axiosWithAuth()
+ .get(`/api/colors`)
+ .then((res)=>{
+ // console.log('fetchColorsServices res:', res);
+ setColors(res.data)
+ // console.log('this is the setColors res.data', res.data);
+ })
+ .catch(err => {
+ console.log(err);
+ })
}
-export default fetchColorService;
\ No newline at end of file
+export default fetchColorService;
diff --git a/src/services/saveEditService.js b/src/services/saveEditService.js
new file mode 100644
index 0000000000..fdd657ed4f
--- /dev/null
+++ b/src/services/saveEditService.js
@@ -0,0 +1,9 @@
+import axiosWithAuth from "../helpers/axiosWithAuth";
+
+const editColorService = (saveEditedColor) =>{
+ const {id} = saveEditedColor;
+ axiosWithAuth()
+ .put(`api/colors/${id}`, saveEditedColor)
+}
+
+export default editColorService;
\ No newline at end of file