diff --git a/Client/components/login.jsx b/Client/components/login.jsx
new file mode 100644
index 0000000..14f71be
--- /dev/null
+++ b/Client/components/login.jsx
@@ -0,0 +1,59 @@
+import React from 'react';
+import { useDispatch } from 'react-redux';
+import * as actions from '../actionCreator/actionCreator.js';
+
+const login = () => {
+ const dispatch = useDispatch();
+
+ const loginFunc = (event) => {
+ event.preventDefault();
+ const un = document.getElementById('usernameLogin').value;
+ const pw = document.getElementById('passwordLogin').value;
+
+ const loginObj = {
+ username: un,
+ password: pw,
+ };
+
+ fetch('http://localhost:3000/login', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(loginObj),
+ })
+ .then((data) => data.json())
+ .then((data) => {
+ if (!data.err) {
+ console.log(data);
+ // Do something
+ // change logged in user state with the returned userState
+ dispatch(actions.updateUSER_LOG_ON(data));
+
+ // disable opacity
+ const overlay = document.getElementById('overlay');
+ overlay.style.opacity = 0;
+ setTimeout(() => overlay.style.display = 'none', 1000);
+ } else {
+ alert(data.err);
+ }
+ })
+ .catch((error) => alert('Invalid Username/Password'));
+ };
+
+ return (
+
+ );
+};
+
+export default login;
diff --git a/Client/components/signup.jsx b/Client/components/signup.jsx
new file mode 100644
index 0000000..66a5145
--- /dev/null
+++ b/Client/components/signup.jsx
@@ -0,0 +1,60 @@
+import React from 'react';
+import { useDispatch } from 'react-redux';
+import * as actions from '../actionCreator/actionCreator.js';
+
+const signup = () => {
+ const dispatch = useDispatch();
+
+ const signupFunc = (event) => {
+ event.preventDefault();
+ const username = document.getElementById('usernameSignup').value;
+ const password1 = document.getElementById('passwordSignup').value;
+ const password2 = document.getElementById('passwordSignupConfirm').value;
+
+ // check if passwords match
+ if (password1 !== password2) {
+ return alert('Sign up passwords do not match');
+ }
+
+ fetch('http://localhost:3000/user', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ username, password: password1 }),
+ })
+ .then((data) => data.json())
+ .then((data) => {
+ // if there is an error object
+ if (data.err) alert(data.err);
+ // update logged in user
+ dispatch(actions.updateUSER_LOG_ON(data));
+ // disable opacity
+ const overlay = document.getElementById('overlay');
+ overlay.style.opacity = 0;
+ setTimeout(() => overlay.style.display = 'none', 1000);
+ })
+ .catch((error) => alert(error));
+ };
+
+ return (
+
+ );
+};
+
+export default signup;
diff --git a/Client/components/star-style.module.css b/Client/components/star-style.module.css
new file mode 100644
index 0000000..5f8ab1b
--- /dev/null
+++ b/Client/components/star-style.module.css
@@ -0,0 +1,14 @@
+.star_true {
+ /* represents on state */
+ color: #d4af37;
+}
+
+.star_false {
+ /* represents off state */
+ color: black;
+}
+
+.star-button {
+ background: none;
+ border: none;
+}
diff --git a/Client/constant/actionTypes.js b/Client/constant/actionTypes.js
new file mode 100644
index 0000000..7961ba1
--- /dev/null
+++ b/Client/constant/actionTypes.js
@@ -0,0 +1,17 @@
+/**
+ * ************************************
+ *
+ * @module
+ * @author Eivind Del Fierro, Morah Geist
+ * @date 07/2023
+ * @description actions
+ *
+ * ************************************
+ */
+
+export const UPDATE_FROM_API = "UPDATE_FROM_API"
+export const USER_LOG_ON = "USER_LOG_ON"
+export const USER_LOG_OFF = "USER_LOG_OFF"
+export const UPDATE_MUSCLE_DIFFICULTY = "UPDATE_MUSCLE_DIFFICULTY"
+export const ADD_FAVORITE = "ADD_FAVORITE"
+export const REMOVE_FAVORITE = "REMOVE_FAVORITE"
\ No newline at end of file
diff --git a/Client/containers/HeaderContainer.jsx b/Client/containers/HeaderContainer.jsx
index 022472b..d90d546 100644
--- a/Client/containers/HeaderContainer.jsx
+++ b/Client/containers/HeaderContainer.jsx
@@ -1,24 +1,34 @@
-import React from 'react';
-import '../styles.css';
+/**
+ * ************************************
+ *
+ * @module Header
+ * @author Eivind Del Fierro, Morah Geist
+ * @date 07/2023
+ * @description header feature on main page of app
+ *
+ * ************************************
+ */
-{
- /* This is the HeaderContainer in Client/containers/HeaderContainer.jsx */
-}
+import React from 'react';
+import * as actions from '../actionCreator/actionCreator.js';
+import { useDispatch } from 'react-redux';
const HeaderContainer = () => {
- // insert any logic for the HeaderContainer here
+ const dispatch = useDispatch();
+
+ const logoutHandler = () => {
+ dispatch(actions.updateUSER_LOG_OFF());
+ const overlay = document.getElementById('overlay');
+ overlay.style.opacity = 1;
+ overlay.style.display = 'block'
+ };
+
return (
-
-
- Ready to get your stretch on?
-
-
-
-
+
+
Stretch
+
+ Logout
+
);
};
diff --git a/Client/containers/MainContainer.jsx b/Client/containers/MainContainer.jsx
deleted file mode 100644
index 6fbaaec..0000000
--- a/Client/containers/MainContainer.jsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import React from 'react';
-import Stretch from '../components/Stretch.jsx';
-
-const MainContainer = () => {
- // stretchesFromAPI is an array that holds a series of objects, where each object is a stretch we have pulled from our query to our server
- let stretchesFromAPI = [{}, {}, {}];
- // stretchFetch is an async func that accepts as a param a muscle from user input in search bar
- const stretchFetch = async (muscle) => {
- try {
- // fetch request to server 3000
- const response = await fetch(
- `/api?muscle=${muscle}&type=stretching`
- );
- stretchesFromAPI = await response.json();
- redirect('/');
- return;
- } catch (err) {
- console.log('Error from stretchFetch in MainContainer.jsx');
- }
- };
-
- // init stretchComponents as empty arr, this will store Stretch components before rendering
- const stretchComponents = [];
- // if stretchesFromAPI is not undefined/null, iterate through stretchesFromAPI and push a new TaskRow component with id and key properties
- if (stretchesFromAPI) {
- stretchesFromAPI.forEach((task) => {
- stretchComponents.push(
-
- );
- });
- }
- // insert any logic for the MainContainer here, including (potentially):
- return (
-
- {/* this p tag just helps us see where this MainContainer is being rendered */}
-
This is the MainContainer in Client/containers/MainContainer.jsx
- {/* insert search bar here (input textbox and submit button) */}
-
- Search:
-
-
-
- {/* Stretch are individual search results from query to database/API */}
- {stretchComponents}
-
- );
-};
-export default MainContainer;
diff --git a/Client/containers/MenuContainer.jsx b/Client/containers/MenuContainer.jsx
new file mode 100644
index 0000000..ae232c5
--- /dev/null
+++ b/Client/containers/MenuContainer.jsx
@@ -0,0 +1,80 @@
+/**
+ * ************************************
+ *
+ * @module
+ * @author Eivind Del Fierro, Morah Geist
+ * @date 07/2023
+ * @description drop down menu item for selecting stretch target muscle group and difficulty
+ * question: can we pass the data from the drop down menus in menu container to the stretch container to render the stretch components? menu container and stretch container are siblings, do we need a parent to hold both containers?
+ *
+ * ************************************
+ */
+
+import React from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import * as actions from '../actionCreator/actionCreator.js';
+
+const MenuContainer = (prop) => {
+ const dispatch = useDispatch();
+ const state = useSelector( state => state.stretch);
+
+ const refreshExercises = async () => {
+ const muscle = document.getElementById('muscle').value;
+ const difficulty = document.getElementById('difficulty').value;
+ await fetch(
+ `http://localhost:3000/api?muscle=${muscle}&difficulty=${difficulty}&type=stretching`
+ )
+ .then((data) => data.json())
+ .then((data) => {
+ dispatch(actions.updateDifficultyAndMuscle([muscle, difficulty]));
+ return dispatch(actions.updateExercisesFromAPI(data));
+ })
+ .catch((error) => console.log('Error in MenuContainer.jsx fetch', error));
+ };
+
+ const showFavorites = () => {
+ return dispatch(actions.updateExercisesFromAPI(state.favorites))
+ }
+
+ return (
+
+
+ Select a muscle
+ Abdominals
+ Abductors
+ Adductors
+ Biceps
+ Calves
+ Chest
+ Forearms
+ Glutes
+ Hamstrings
+ Lats
+ Lower Back
+ Middle Back
+ Neck
+ Quadriceps
+ Traps
+ Triceps
+
+
+ Select a difficulty
+ Beginner
+ Intermediate
+ Expert
+
+
+ Favorites
+
+ );
+};
+
+export default MenuContainer;
\ No newline at end of file
diff --git a/Client/containers/StretchContainer.jsx b/Client/containers/StretchContainer.jsx
new file mode 100644
index 0000000..c80807c
--- /dev/null
+++ b/Client/containers/StretchContainer.jsx
@@ -0,0 +1,61 @@
+/**
+ * ************************************
+ *
+ * @module
+ * @author Eivind Del Fierro, Morah Geist
+ * @date 07/2023
+ * @description stretch container rendering stretch components based on search from drop down menus found in menu container
+ * question: can we pass the data from the drop down menus in menu container to the stretch container to render the stretch components? menu container and stretch container are siblings, do we need a parent to hold both containers?
+ *
+ * ************************************
+ */
+
+import React from 'react';
+import { useSelector } from 'react-redux';
+import Stretch from '../components/Stretch.jsx';
+
+// can pass in props and prop drill if you want
+const StretchContainer = () => {
+ const stretchList = useSelector((state) => state.stretch.exercisesFromAPI);
+ const muscle = useSelector((state) => state.stretch.muscle);
+ const difficulty = useSelector((state) => state.stretch.difficulty);
+ // const muscle = document.getElementById('muscle').value;
+ // const difficulty = document.getElementById('difficulty').value;
+
+ const stretchArr = [];
+
+ if (stretchList.length) {
+ for (let i = 0; i < stretchList.length; i++) {
+ const item = stretchList[i];
+ stretchArr.push(
);
+ }
+ return
{stretchArr}
;
+ }
+
+ if (!muscle === 'null' || !difficulty === 'null') {
+ return (
+
+
Please select a muscle group and difficulty
+
+ );
+ }
+
+ if (difficulty === 'null') {
+ return (
+
+
No {muscle} exercises found!
+
+ );
+ }
+
+ return (
+
+
+ No {difficulty !== 'null' ? difficulty : null}{' '}
+ {muscle !== 'null' ? muscle : null} exercises found!
+
+
+ );
+};
+
+export default StretchContainer;
diff --git a/Client/containers/TimerContainer.jsx b/Client/containers/TimerContainer.jsx
new file mode 100644
index 0000000..d3d4c14
--- /dev/null
+++ b/Client/containers/TimerContainer.jsx
@@ -0,0 +1,63 @@
+/**
+ * ************************************
+ *
+ * @module
+ * @author Eivind Del Fierro, Morah Geist
+ * @date 07/2023
+ * @description timer container rendering timer and start button
+ *
+ * ************************************
+ */
+
+import React from 'react';
+import { useTimer } from 'react-timer-hook';
+
+function MyTimer({ expiryTimestamp }) {
+ const { seconds, minutes, isRunning, start, pause, resume, restart, } =
+ useTimer({
+ expiryTimestamp,
+ autoStart: false,
+ onExpire: () => alert('Stretch complete! Let\'s stretch some more!'),
+ });
+
+ return (
+
+
+ {minutes} :{seconds}
+
+
+
+ Start
+
+
+ Pause
+
+
+ Resume
+
+ {
+ // Restarts to 5 minutes timer
+ const time = new Date();
+ time.setSeconds(time.getSeconds() + 60);
+ restart(time);
+ pause();
+ }}
+ >
+ Restart
+
+
+
+ );
+}
+
+export default function App() {
+ const time = new Date();
+ time.setSeconds(time.getSeconds() + 60);
+ return (
+
+
+
+ );
+}
diff --git a/Client/containers/WelcomeScreen.jsx b/Client/containers/WelcomeScreen.jsx
new file mode 100644
index 0000000..9fee6f5
--- /dev/null
+++ b/Client/containers/WelcomeScreen.jsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import Login from '../components/login.jsx';
+import Signup from '../components/signup.jsx';
+
+const WelcomeScreen = () => {
+ return (
+
+
+
+
Welcome to Flexios
+
+
+
+
+
+
+
+ );
+};
+
+export default WelcomeScreen;
diff --git a/Client/index.html b/Client/index.html
index a08748a..aee482b 100644
--- a/Client/index.html
+++ b/Client/index.html
@@ -1,15 +1,17 @@
-
-
+
Get stretching
-
+
+
+
+
diff --git a/Client/index.js b/Client/index.js
index a16b272..ae2352a 100644
--- a/Client/index.js
+++ b/Client/index.js
@@ -1,6 +1,30 @@
+/**
+ * ************************************
+ *
+ * @module App.jsx
+ * @author Eivind Del Fierro, Morah Geist
+ * @date 07/2023
+ * @description index file to render app
+ *
+ * ************************************
+ */
+
import React from 'react';
-import { render } from 'react-dom';
+import ReactDOM from 'react-dom/client';
+import { Provider } from 'react-redux';
+import store from './store.js';
import App from './App.jsx';
+// import './stylesheets/styles.css';
+import styles from './stylesheets/application.scss';
+import WelcomeScreen from './containers/WelcomeScreen.jsx';
// render app from App.jsx file on the html element with id of app in the index.html page
-render(
, document.getElementById('app'));
+const root = ReactDOM.createRoot(document.getElementById('app'));
+root.render(
+
+
+
+
+
+
+);
diff --git a/Client/login and signup/signup-login.css b/Client/login and signup/signup-login.css
deleted file mode 100644
index bd743c4..0000000
--- a/Client/login and signup/signup-login.css
+++ /dev/null
@@ -1,4 +0,0 @@
-body {
- background-color: whitesmoke;
-}
-
diff --git a/Client/login and signup/signup-login.html b/Client/login and signup/signup-login.html
deleted file mode 100644
index 8ab2098..0000000
--- a/Client/login and signup/signup-login.html
+++ /dev/null
@@ -1,52 +0,0 @@
-
-
-
-
-
-
-
-
Log In/Sign Up
-
-
-
-
-
-
-
-
-
- Ready to get your stretch on?
-
-
Don't have an account?
-
Sign up
-
-
-
-
-
Already have an account?
-
Log in
-
-
-
-
\ No newline at end of file
diff --git a/Client/reducers/index.js b/Client/reducers/index.js
new file mode 100644
index 0000000..935ff75
--- /dev/null
+++ b/Client/reducers/index.js
@@ -0,0 +1,19 @@
+/**
+ * ************************************
+ *
+ * @module stretchReducer
+ * @author Eivind Del Fierro, Morah Geist
+ * @date 07/2023
+ * @description combine reducers
+ *
+ * ************************************
+ */
+
+import { combineReducers } from 'redux';
+import stretchReducer from './stretchReducer';
+
+const reducers = combineReducers({
+ stretch: stretchReducer,
+});
+
+export default reducers;
diff --git a/Client/reducers/stretchReducer.js b/Client/reducers/stretchReducer.js
new file mode 100644
index 0000000..c86c85f
--- /dev/null
+++ b/Client/reducers/stretchReducer.js
@@ -0,0 +1,94 @@
+/**
+ * ************************************
+ *
+ * @module stretchReducer
+ * @author Eivind Del Fierro, Morah Geist
+ * @date 07/2023
+ * @description reducer for stretch
+ *
+ * ************************************
+ */
+import * as types from '../constant/actionTypes.js';
+
+const initialState = {
+ exercisesFromAPI: [],
+ loggedInUser: '',
+ favorites: [],
+ muscle: '',
+ difficulty: '',
+};
+
+const stretchReducer = (state = initialState, action) => {
+ switch (action.type) {
+ // Get's a list of exercises from the server and updates the array with objects of exercises
+ case types.UPDATE_FROM_API: {
+ return { ...state, exercisesFromAPI: action.payload };
+ }
+
+ // Logs user on
+ case types.USER_LOG_ON: {
+ // get authentication status of user
+ document.getElementById('usernameLogin').value = '';
+ document.getElementById('passwordLogin').value = '';
+ document.getElementById('usernameSignup').value = '';
+ document.getElementById('passwordSignup').value = '';
+ document.getElementById('passwordSignupConfirm').value = '';
+ return {
+ ...state,
+ loggedInUser: action.payload.username,
+ favorites: action.payload.favorites,
+ };
+ }
+
+ // Logs user off
+ case types.USER_LOG_OFF: {
+ // reset the state including the logged-in user
+ return { ...initialState };
+ }
+
+ case types.UPDATE_MUSCLE_DIFFICULTY: {
+ const [muscle, difficulty] = action.payload;
+ return { ...state, muscle: muscle, difficulty, difficulty };
+ }
+
+ case types.ADD_FAVORITE: {
+ // communicate with the server
+ fetch('http://localhost:3000/user/favorite', {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({username: state.loggedInUser, favorite: action.payload}),
+ })
+ .then((data) => data.json())
+ .then( data => {
+ return {
+ ...state,
+ favorites: data.favorites,
+ exercisesFromAPI: data.favorites
+ }
+ })
+ .catch( error => console.log('Error while adding favorites'))
+ }
+
+ case types.REMOVE_FAVORITE: {
+ fetch('http://localhost:3000/user/favorite', {
+ method: 'DELETE',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({username: state.loggedInUser, favorite: action.payload}),
+ })
+ .then((data) => data.json())
+ .then( data => {
+ return {
+ ...state,
+ favorites: data.favorites,
+ exercisesFromAPI: data.favorites
+ }
+ })
+ .catch( error => console.log('Error while removing favorites'))
+ }
+
+ default:
+ return state;
+ }
+};
+
+export default stretchReducer;
\ No newline at end of file
diff --git a/Client/store.js b/Client/store.js
new file mode 100644
index 0000000..cdf1891
--- /dev/null
+++ b/Client/store.js
@@ -0,0 +1,17 @@
+/**
+ * ************************************
+ *
+ * @module Store
+ * @author Eivind Del Fierro, Morah Geist
+ * @date 07/2023
+ * @description redux store
+ *
+ * ************************************
+ */
+
+import { configureStore } from '@reduxjs/toolkit';
+import reducers from './reducers';
+
+const store = configureStore({ reducer: reducers });
+
+export default store;
diff --git a/Client/styles.css b/Client/styles.css
deleted file mode 100644
index db04882..0000000
--- a/Client/styles.css
+++ /dev/null
@@ -1,21 +0,0 @@
-body {
- background-color: whitesmoke;
-}
-
-#navBar {
- display: flex;
- justify-content: space-between;
- background-color: lightblue;
- font-family: Arial, Helvetica, sans-serif;
- margin: 0px;
- padding: 5px;
-}
-
-#flex-item {
- display: flex;
- font-size: larger;
-}
-
-.stretchComp {
- background-color: pink;
-}
diff --git a/Client/stylesheets/_colorpalette.scss b/Client/stylesheets/_colorpalette.scss
new file mode 100644
index 0000000..169c9cc
--- /dev/null
+++ b/Client/stylesheets/_colorpalette.scss
@@ -0,0 +1,119 @@
+$lightest: #daebe4;
+$lighter: #a5caba;
+$light: #8faeba;
+$mid: #386994;
+$dark: #214457;
+$darker: #051827;
+
+// // initialize the default palette as a global variable
+// $base-palette: (
+// 'base': #a05b6d,
+// 'colors': #e8ccd3 #d8a8b4 #c9899a #a05b6d #864d5b #663a45 #40252b,
+// ) !default;
+
+// // color diff is a map of operations to apply to a Color A in order to come up with a Color B
+// // need a list of color diffs, one for each color of the palette
+// @function color-diff($a, $b) {
+// $sat: saturation($a) - saturation($b);
+// $lig: lightness($a) - lightness($b);
+// $fn-sat: if($sat > 0, 'desaturate', 'saturate');
+// $fn-lig: if($lig > 0, 'darken', 'lighten');
+// // returns a map of operations to apply to the first color a to obtain color b
+// @return (
+// adjust-hue: -(
+// hue($a) - hue($b),
+// ),
+// #{$fn-sat}: abs($sat),
+// #{$fn-lig}: abs($lig)
+// );
+// }
+
+// // need a function that runs color-diff on every color of the base palette ($base-palette), and returns a list of these diffs
+// @function palette-diff($palette) {
+// $base: map-get($palette, 'base');
+// $colors: map-get($palette, 'colors');
+// $diffs: ();
+
+// @each $color in $colors {
+// $diffs: append($diffs, color-diff($base, $color));
+// }
+
+// @return $diffs;
+// }
+
+// // run this once and then store it in a global variable
+// $palette-diff: palette-diff($base-palette);
+
+// // apply a diff (the return of color-diff) to a color, return new color
+// @function apply-diff($color, $diff) {
+// @each $function, $value in $diff {
+// $color: call($function, $color, $value);
+// }
+
+// @return $color;
+// }
+
+// // apply each function from the diff to the color, then get another color
+// // need a function that creates a palette from a base color
+// @function create-palette($base-color) {
+// $palette: ();
+
+// @each $diff in $palette-diff {
+// $palette: append($palette, apply-diff($base-color, $diff));
+// }
+
+// @return $palette;
+// }
+
+// // call the create-palette function with a base color, returns a list of 7 colors (the length of the $base-palette colors key), made from the same operations that were used for the base palette
+// $mauve-palette: create-palette(#ce95a3);
+// $lilac-palette: create-palette(#d1b1f9);
+// $latte-palette: create-palette(#c5a992);
+
+// // turn this list into a map with explicit keys: lightest, lighter, light, base, dark, darker, darkest
+// @function palette($base-color) {
+// $colors: create-palette($base-color);
+// $keys: 'lightest' 'lighter' 'light' 'base' 'dark' 'darker' 'darkest';
+// $palette: ();
+
+// @for $i from 1 through min(length($colors), length($keys)) {
+// $palette: map-merge(
+// $palette,
+// (
+// nth($keys, $i): nth($colors, $i),
+// )
+// );
+// }
+
+// @return $palette;
+// }
+
+// // build a series of helpers
+// @function lightest($palette) {
+// @if not map-has-key($palette, 'lightest') {
+// @warn "`#{inspect($palette)}` doesn't seem to have a key named `lightest`.";
+// }
+
+// @return map-get($palette, 'lightest');
+// }
+
+// @function lighter($palette) {
+// @if not map-has-key($palette, 'lighter') {
+// @warn "`#{inspect($palette)}` doesn't seem to have a key named `lighter`.";
+// }
+
+// @return map-get($palette, 'lighter');
+// }
+
+// @function light($palette) {
+// @if not map-has-key($palette, 'light') {
+// @warn "`#{inspect($palette)}` doesn't seem to have a key named `light`.";
+// }
+
+// @return map-get($palette, 'light');
+// }
+
+// /* then in CSS use .el {
+// color: light($green-palette);
+// }
+// */
diff --git a/Client/stylesheets/_flexios.scss b/Client/stylesheets/_flexios.scss
new file mode 100644
index 0000000..e0288bd
--- /dev/null
+++ b/Client/stylesheets/_flexios.scss
@@ -0,0 +1,220 @@
+@import url('https://fonts.cdnfonts.com/css/oak-sans');
+
+* {
+ margin: none;
+ padding: none;
+ font-family: 'Oak Sans', sans-serif;
+}
+strong {
+ font-weight: 600;
+}
+
+/* containers */
+.mainApp {
+ background-color: $lightest;
+ padding: 20px;
+}
+.stretchCont {
+ margin-bottom: 5px;
+}
+.appHeaderBox {
+ display: flex;
+ justify-content: space-between;
+}
+.cardHeadBox {
+ display: flex;
+ align-items: baseline;
+ margin-left: 20px;
+}
+.mainHeader {
+ font-size: 30px;
+ font-weight: 600;
+ color: $darker;
+}
+.overlay {
+ display: flex;
+ position: relative;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+ vertical-align: middle;
+ padding-top: 40%;
+}
+.overlayDiv {
+ display: flex;
+ position: relative;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ background-color: $darker;
+ padding: 25px;
+ border-radius: 10px;
+ max-height: 50%;
+ vertical-align: middle;
+}
+.loginDiv,
+.signupDiv {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ margin-top: 20px;
+}
+.welcomeHead {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: $lightest;
+}
+
+.stretchCard {
+ background-color: $lighter;
+ color: $darker;
+ border-radius: 10px;
+}
+.cardHeader {
+ margin-left: 5px;
+ padding-top: 10px;
+}
+.favIcon {
+ margin-left: 20px;
+ margin-right: 10px;
+}
+ul {
+ list-style-type: none;
+ margin-right: 5px;
+}
+li {
+ margin-bottom: 5px;
+}
+li:last-child {
+ padding-bottom: 10px;
+}
+
+/* dropdown menu */
+.muscle,
+.difficulty {
+ background-color: $light;
+ color: $darker;
+ padding: 10px;
+ font-size: 16px;
+ border: none;
+ border-radius: 8px;
+ cursor: pointer;
+ margin-right: 5px;
+ margin-bottom: 10px;
+}
+
+/* buttons */
+.logoutBtn {
+ background-color: rgba(51, 51, 51, 0.05);
+ border-radius: 8px;
+ border-width: 0;
+ color: #333333;
+ cursor: pointer;
+ display: inline-block;
+ font-size: 14px;
+ font-weight: 500;
+ line-height: 20px;
+ list-style: none;
+ margin: 0;
+ padding: 10px 12px;
+ text-align: center;
+ transition: all 200ms;
+ vertical-align: baseline;
+ white-space: nowrap;
+ user-select: none;
+ -webkit-user-select: none;
+ touch-action: manipulation;
+ max-height: 40px;
+}
+.favBtn,
+.loginBtn,
+.signupBtn {
+ background-color: $light;
+ color: $darker;
+ padding: 10px;
+ font-size: 16px;
+ border: none;
+ border-radius: 8px;
+ cursor: pointer;
+ margin-right: 5px;
+ margin-bottom: 10px;
+}
+.loginBtn,
+.signupBtn {
+ width: 100%;
+}
+
+#overlay {
+ transition: opacity 1s linear;
+ position: fixed;
+ display: block;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: $dark;
+ z-index: 2;
+}
+#usernameLogin,
+#passwordLogin,
+#usernameSignup,
+#passwordSignup,
+#passwordSignupConfirm {
+ margin-bottom: 5px;
+ display: flex;
+ background-color: $lightest;
+ height: 36px;
+ border-radius: 8px;
+ border: none;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+}
+/* timer */
+.timerDiv {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ margin-bottom: 10px;
+}
+.timeDisplay {
+ margin-bottom: 10px;
+ font-size: 25px;
+}
+
+.startBtn,
+.pauseBtn,
+.resumeBtn,
+.restartBtn {
+ background-color: rgba(51, 51, 51, 0.05);
+ border-radius: 8px;
+ border-width: 0;
+ color: #333333;
+ cursor: pointer;
+ display: inline-block;
+ font-size: 14px;
+ font-weight: 500;
+ line-height: 20px;
+ list-style: none;
+ margin: 0;
+ padding: 10px 12px;
+ text-align: center;
+ transition: all 200ms;
+ vertical-align: baseline;
+ white-space: nowrap;
+ user-select: none;
+ -webkit-user-select: none;
+ touch-action: manipulation;
+ max-height: 40px;
+}
+.startBtn,
+.pauseBtn,
+.resumeBtn {
+ margin-right: 5px;
+}
diff --git a/Client/stylesheets/application.scss b/Client/stylesheets/application.scss
new file mode 100644
index 0000000..3b86fb9
--- /dev/null
+++ b/Client/stylesheets/application.scss
@@ -0,0 +1,2 @@
+@import '_colorpalette';
+@import '_flexios';
diff --git a/Server/controller.js b/Server/controller.js
deleted file mode 100644
index d06c26a..0000000
--- a/Server/controller.js
+++ /dev/null
@@ -1,41 +0,0 @@
-// require('dotenv').config();
-// const apiKEY = process.env.API_KEY;
-
-// need to pass above object and the API key into the request to the API.
-// we added some .env stuff, not being used as this page is sufficient
- // having a .env folder for private info like an API key or database log in info is best practice
- // but we didn't have time to implement/didn't need to because of the scope of this project
-
- // init const StretchController, an object that stores the functionality
-const StretchController = {
- // The getStretches method is a function that accepts 3 params, req, res, next, and stores the result of a fetch request to the exercises api in our
- getStretches: async (req, res, next) => {
- try {
- // init const muscle as muscle prop of request query
- const { muscle } = req.query;
- // init const apiRes as output from api request
- const apiRes = await fetch(`https://api.api-ninjas.com/v1/exercises?muscle=${muscle}&type=stretching`, {
- method: 'GET',
- headers: { 'X-Api-Key': 'SReYt5aEyGMKzrdSe87wew==boZAObqiLCiQPGrb'},
- })
- // store apiRes in res.locals
- res.locals.apiRes = apiRes;
- // return the invocation of next to move to next middleware
- return next();
- } catch (error) {
- const errorObject = {
- // log to developer
- log: 'Error occurred in StretchController.GetExercise',
- // message to client
- message: { error: 'An error has occurred in getting an exericse'},
- status: 400
- };
- // pass error object to global error handler
- return next(errorObject);
- }
- }
-}
-
-StretchController.getStretches({muscle: 'Ilikemuscles'}, {locals: {}}, ()=>{});
-
-module.exports = StretchController;
diff --git a/Server/controller/ExerciseController b/Server/controller/ExerciseController
deleted file mode 100644
index e69de29..0000000
diff --git a/Server/controller/ExerciseController.js b/Server/controller/ExerciseController.js
new file mode 100644
index 0000000..5e9c69a
--- /dev/null
+++ b/Server/controller/ExerciseController.js
@@ -0,0 +1,68 @@
+// require('dotenv').config();
+const apiKEY = process.env.API_KEY;
+// need to pass above object and the API key into the request to the API.
+// we added some .env stuff, not being used as this page is sufficient
+// having a .env folder for private info like an API key or database log in info is best practice
+// but we didn't have time to implement/didn't need to because of the scope of this project
+
+// init const StretchController, an object that stores the functionality
+const StretchController = {
+ // The getStretches method is a function that accepts 3 params, req, res, next, and stores the result of a fetch request to the exercises api in our
+ getStretches: async (req, res, next) => {
+ try {
+ // init const muscle as muscle prop of request query
+ const { muscle, name, type, difficulty } = req.query;
+ //console.log(req.query);
+ // create API request string from query parameters
+ let apiString = 'https://api.api-ninjas.com/v1/exercises?';
+
+ // allow for muscle group input
+ if (muscle) apiString += `muscle=${muscle}`;
+ // allow for workout name input
+ if (name) apiString += `&name=${name}`;
+ // allow variation in workout type - since stretching is the default for the app it defaults to that
+ if (type) apiString += `&type=${type}`;
+ else apiString += `&type=stretching`;
+
+ if (difficulty) apiString += `&difficulty=${difficulty}`;
+
+ // init const apiRes as output from api request
+ const apiRes = await fetch(apiString, {
+ method: 'GET',
+ headers: { 'X-Api-Key': apiKEY },
+ }).then((response) => response.json());
+ // store apiRes in res.locals
+ //console.log(apiRes);
+ res.locals.apiRes = [];
+ const ex_names = {};
+
+ // filter out duplicate exercises in api response
+ apiRes.forEach((ex) => {
+ //console.log('checking: ', ex);
+ if (!(ex.name in ex_names)) {
+ //console.log('here', ex.name, ex_names, res.locals.apiRes);
+ res.locals.apiRes.push(ex);
+ ex_names[ex.name] = true;
+ }
+ });
+
+ // sort api response exercises alphabetically
+ res.locals.apiRes.sort((ex1, ex2) => (ex1.name > ex2.name ? 1 : -1));
+
+ // return the invocation of next to move to next middleware
+ return next();
+ } catch (error) {
+ const errorObject = {
+ // log to developer
+ log: 'Error occurred in StretchController.GetExercise',
+ // message to client
+ message: { error: 'An error has occurred in getting an exericse' },
+ status: 400,
+ };
+ // pass error object to global error handler
+ return next(errorObject);
+ }
+ },
+};
+
+module.exports = StretchController;
diff --git a/Server/controller/UserController.js b/Server/controller/UserController.js
new file mode 100644
index 0000000..2ad811e
--- /dev/null
+++ b/Server/controller/UserController.js
@@ -0,0 +1,127 @@
+const { restart } = require('nodemon');
+const User = require('../database/UserModel.js');
+const userController = {};
+const asyncHandler = require('express-async-handler');
+const bcrypt = require('bcrypt');
+
+// create user // username, password
+userController.registerUser = asyncHandler(async (req, res, next) => {
+ const { username, password } = req.body;
+
+ //if (User.find({ username })) throw new Error("User already exists");
+ const userCheck = await User.find({ username });
+ //console.log('username : ', username, ' : ', userCheck);
+ if (userCheck.length > 0) throw new Error('User already exists');
+ // if we find the user we need to throw an error
+ res.locals.registeredUser = await User.create({
+ username: username,
+ password: password,
+ });
+
+ return next();
+});
+
+userController.authUser = async (req, res, next) => {
+ const { username, password } = req.body;
+ // get user from db
+ const userCheck = await User.find({ username });
+ console.log('username : ', username, ' : ', userCheck);
+ // check if user is in db
+ if (userCheck.length <= 0) {
+ return next({ message: { err: 'user not found' } });
+ }
+ //compare password
+ const match = bcrypt.compare(
+ password,
+ userCheck[0].password,
+ function (err, result) {
+ if (result === true) {
+ //console.log('this is the log ', userCheck[0]);
+ res.locals.user = userCheck[0];
+ return next();
+ } else {
+ return next({ message: { err: 'user not found or wrong password' } });
+ }
+ }
+ );
+};
+
+// Delete a user
+// username and password must be passed in request body
+
+userController.deleteUser = async (req, res, next) => {
+ const { username, password } = req.body;
+ //console.log('entering deletion middleware ');
+
+ try {
+ // find the userand delete
+ await User.deleteOne({ username }).then(
+ (user) => (res.locals.deletedUser = user)
+ );
+ } catch (err) {
+ return next({
+ status: 401,
+ log: 'error in registerUser middleware',
+ error: err,
+ });
+ }
+ return next();
+};
+
+// Add a favorite
+// favorite is passed in through the req.body
+ //Find the user with that username.
+ //Push onto their favorites array the current favorites array.
+ //Update the current favorites array.
+userController.addFavorite = async (req, res, next) => {
+ const { username, favorite } = req.body;
+ console.log("entering add favorite middleware");
+ // console.log("favorite", favorite)
+
+ try {
+ await User.findOneAndUpdate(
+ { username: username },
+ //push the favorite onto the favorites array
+ { $push: { favorites: favorite } },
+ { new: true }
+ ).then((updatedUser) => {
+ console.log('in UserController.js', updatedUser);
+ res.locals.updatedUser = updatedUser;
+ });
+ } catch (err) {
+ return next({
+ status: 401,
+ log: "error in addFavorite middleware",
+ error: err,
+ });
+ }
+ return next();
+};
+
+//Remove a favorite
+userController.deleteFavorite = async (req, res, next) => {
+ const { username, favorite } = req.body;
+ console.log('entering delete favorite middleware');
+
+ try {
+ await User.findOneAndUpdate(
+ { username: username },
+ { $pull: { favorites: favorite } },
+ { new: true }
+ ).then((updatedUser) => {
+ res.locals.updatedUserDeletedFavorite = updatedUser;
+ })
+ } catch (err) {
+ return next({
+ status: 401,
+ log: "error in addFavorite middleware",
+ error: err,
+ });
+ }
+ return next();
+};
+
+// find a user
+// mostly for testing deletion
+// username must be passed in request body
+module.exports = userController;
diff --git a/Server/database/UserModel.js b/Server/database/UserModel.js
index 3ffb7af..80d6d99 100644
--- a/Server/database/UserModel.js
+++ b/Server/database/UserModel.js
@@ -1,13 +1,37 @@
-const mongoose = require('mongoose');
-// init const Schema as Schema constructor
-const Schema = mongoose.Schema;
-
-const userSchema = new Schema({
- username: { type: String, required: true },
- email: { type: String, required: true },
- password: { type: String, required: true },
+const mongoose = require("mongoose");
+const bcrypt = require("bcrypt");
+
+const userSchema = mongoose.Schema({
+ username: {
+ type: String,
+ required: true,
+ },
+ password: {
+ type: String,
+ required: true,
+ },
+ favorites: {
+ type: Array,
+ default: [],
+ },
});
-// Export user model through module.exports
-// The collection name should be 'user'
-module.exports = mongoose.model('user', userSchema);
+userSchema.methods.matchPassword = async function (enteredPassword) {
+ return await bcrypt.compare(enteredPassword, this.password);
+};
+
+userSchema.pre("save", async function (next) {
+ // only run if the password was modified
+ // this allows for a username change / password change to be separate
+ if (!this.isModified("password")) {
+ next();
+ }
+ // generate salt for encryption
+ const salt = await bcrypt.genSalt(10);
+ // encrypt password
+ this.password = await bcrypt.hash(this.password, salt);
+});
+
+const User = mongoose.model("users", userSchema);
+
+module.exports = User;
\ No newline at end of file
diff --git a/Server/database/dbConnection.js b/Server/database/dbConnection.js
new file mode 100644
index 0000000..2f2b78f
--- /dev/null
+++ b/Server/database/dbConnection.js
@@ -0,0 +1,14 @@
+const mongoose = require("mongoose");
+const dotenv = require("dotenv");
+const uri = process.env.MONGO_URI;
+dotenv.config();
+
+async function startServer() {
+ mongoose.set("strictQuery", false);
+ const connection = await mongoose.connect(uri, {
+ useNewUrlParser: true,
+ useUnifiedTopology: true,
+ });
+ console.log(`MongoDB is connected to: ${connection.connection.host}`);
+}
+module.exports = startServer;
diff --git a/Server/server.js b/Server/server.js
index f246ec8..3e4de83 100644
--- a/Server/server.js
+++ b/Server/server.js
@@ -1,54 +1,114 @@
// goal: set up express routing
-
+// enviroment variable configuration
+require('dotenv').config();
// steps
// create a controller.js
- // this will contain routers
+// this will contain routers
// ensure exports/imports are handled
// declare port
// define endpoints and actions
-// add listener for PORT
+// add listener for PORT
const express = require('express');
const app = express();
const path = require('path');
-const controller = require('./controller.js');
+const controller = require('./controller/ExerciseController.js');
+const startServer = require('./database/dbConnection.js');
+const userController = require('./controller/UserController.js');
+const cors = require('cors');
+
const PORT = 3000;
app.use(express.json());
+app.use(cors());
// if you ever have a form on your frontend, express.urlencoded
app.use(express.urlencoded({ extended: true })); // this will be helpful for stringifying a form req from an .html file
// want to send get request on 'submit' from the dropdown selection on home page
// dropdown body be the req.body that gets sent to controller.js
- // the req.body is used to create and make the API call
+// the req.body is used to create and make the API call
// get response from res.locals.varName and res.status().json(stretch array)
-
// new instance of router
// const stretchRouter = express.Router();
+startServer();
+
+// respond to get request ot root wiht html for welcome screen
+app.get('/', (req, res) => {
+ return res
+ .status(200)
+ .sendFile(
+ path.resolve(__dirname, '../Client/login and signup/signup-login.html')
+ );
+});
+
+app.get('/landingpage', (req, res) => {
+ return res
+ .status(200)
+ .sendFile(
+ path.resolve(__dirname, '../Client/login and signup/WelcomeScreen.html')
+ );
+});
+
+// to create user into database // takes in body // username, password
+app.post('/user', userController.registerUser, (req, res) => {
+ console.log();
+ return res.status(201).json(res.locals.registeredUser);
+});
+
+// delete user from database
+app.delete('/user', userController.deleteUser, (req, res) => {
+ return res.status(202).json(res.locals.deletedUser);
+});
+
+// TODO:
+// get user from database
+
+// to authenticate user based on input username and password
+app.post('/login', userController.authUser, (req, res) => {
+ return res.status(202).json(res.locals.user);
+});
// /API/exercises?muscle=${muscle}&type=stretching
app.get('/api', controller.getStretches, (req, res) => {
- return res.status(200).json(res.locals.apiRes);
+ return res.status(203).json(res.locals.apiRes);
+});
+
+// add a favorite
+app.patch('/user/favorite', userController.addFavorite, (req, res) => {
+ console.log('in server.js, res.locals.updatedUser: ', res.locals.updatedUser);
+ return res.status(202).json(res.locals.updatedUser);
+});
+
+// delete a favorite
+app.delete('/user/favorite', userController.deleteFavorite, (req, res) => {
+ return res.status(202).json(res.locals.updatedUserDeletedFavorite);
});
// app.get('/api', controller.getExercise, (req, res) => {
// return res.status(200).json(res.locals.apiRes);
// });
+// error if route not found
+app.use(() =>
+ next({
+ status: 404,
+ log: 'Route not found',
+ })
+);
+
// global error handler
app.use((err, req, res, next) => {
- const defaultErr = {
- log: 'Express error handler caught unknown middleware error',
- status: 400,
- message: { err: 'An error occurred'},
- };
- const errorObj = Object.assign({}, defaultErr, err);
- console.log(errorObj.log);
- return res.status(errorObj.status).json(errorObj.message);
-})
-
+ const defaultErr = {
+ log: 'Express error handler caught unknown middleware error',
+ status: 400,
+ message: { err: 'An error occurred' },
+ };
+ const errorObj = Object.assign({}, defaultErr, err);
+ console.log(errorObj.log);
+ return res.status(errorObj.status).json(errorObj.message);
+});
// listener
-app.listen(PORT, () => console.log(`listening on ${PORT}`));
+app.listen(PORT, () => console.log(`listening on port ${PORT}`));
diff --git a/__mocks__/mock.js b/__mocks__/mock.js
new file mode 100644
index 0000000..f053ebf
--- /dev/null
+++ b/__mocks__/mock.js
@@ -0,0 +1 @@
+module.exports = {};
diff --git a/__mocks__/mockImage.js b/__mocks__/mockImage.js
new file mode 100644
index 0000000..9dc5fc1
--- /dev/null
+++ b/__mocks__/mockImage.js
@@ -0,0 +1 @@
+module.exports = '';
diff --git a/__tests__/__snapshots__/menuContainerTest.js.snap b/__tests__/__snapshots__/menuContainerTest.js.snap
new file mode 100644
index 0000000..b0d9857
--- /dev/null
+++ b/__tests__/__snapshots__/menuContainerTest.js.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`test for menuContainer.jsx menuContainer.js renders on the page 1`] = `
`;
diff --git a/__tests__/appTest.js b/__tests__/appTest.js
new file mode 100644
index 0000000..90c2608
--- /dev/null
+++ b/__tests__/appTest.js
@@ -0,0 +1,28 @@
+// // inport for testing frontend
+// // const server = 'http://localhost:3000';
+// // const request = require('supertest');
+// // const fs = require('fs');
+
+// import React from 'react';
+// import App from '../Client/App.jsx';
+// import { Store } from '../Client/store.js';
+// import { Provider } from 'react-redux';
+
+// import { render, screen, waitFor } from '@testing-library/react';
+
+// // const React = require('react');
+// // const App = require('../Client/App.jsx');
+// // const { Store } = require('../Client/store.js');
+// // const { render, screen, waitFor } = require('@testing-library/react');
+
+// describe('test for app.jsx', () => {
+// describe('LabeledText', () => {
+// let text;
+// beforeAll(() => {
+// text = render(
);
+// });
+// test('app div should contain mainApp div', async () => {
+// console.log(text);
+// });
+// });
+// });
diff --git a/__tests__/menuContainerTest.js b/__tests__/menuContainerTest.js
new file mode 100644
index 0000000..4f07459
--- /dev/null
+++ b/__tests__/menuContainerTest.js
@@ -0,0 +1,22 @@
+const server = 'http://localhost:3000';
+const request = require('supertest');
+const fs = require('fs');
+import React from 'react';
+import App from '../Client/App.jsx';
+import { Store } from '../Client/store.js';
+import { Provider } from 'react-redux';
+import { render, screen, waitFor } from '@testing-library/react';
+import renderer from 'react-test-renderer';
+
+
+describe('test for menuContainer.jsx', () => {
+ // beforeAll(() => {
+ // const renderedMenuContainer = render(
);
+ // })
+ it('menuContainer.js renders on the page', () => {
+ const tree = renderer
+ .create(
)
+ .toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+});
\ No newline at end of file
diff --git a/__tests__/stretchComponentTest.js b/__tests__/stretchComponentTest.js
new file mode 100644
index 0000000..e1f5e5c
--- /dev/null
+++ b/__tests__/stretchComponentTest.js
@@ -0,0 +1,4 @@
+import React from 'React';
+import { render, screen, waitFor } from '@testing-library/react';
+
+import StretchContainer from '../Client/containers/StretchContainer';
diff --git a/__tests__/userControllerTest.js b/__tests__/userControllerTest.js
new file mode 100644
index 0000000..6669698
--- /dev/null
+++ b/__tests__/userControllerTest.js
@@ -0,0 +1,128 @@
+const server = 'http://localhost:3000';
+const request = require('supertest');
+const fs = require('fs');
+//const { describe, expect, test } = require("@jest/globals");
+// we've commented out the line above ^ because the global variables are already available once we install jest
+
+// test user creation
+
+// TODO: make sure tests that create a user also delete them so not to clutter?
+// not super important but iwll make our DB easier to visually inspect
+xdescribe('User Creation', () => {
+ // first let's check that it returns an object
+ xit('responds with 200 status and object', () => {
+ const username = `Paul${Date.now()}`;
+
+ const password = 'Paul';
+ return request(server)
+ .post('/user')
+ .send({ username, password })
+ .expect(201)
+ .expect('Content-Type', /application\/json/);
+ });
+
+ xit('password is not the same as the original (should be hashed)', () => {
+ const username = `Paul${Date.now()}`;
+
+ const password = 'Paul';
+ return request(server)
+ .post('/user')
+ .send({ username, password })
+ .then((response) => {
+ expect(response.body.password).not.toEqual(password);
+ });
+ });
+
+ xit('attempt to create a duplicate user returns a 400 status code and error object', () => {
+ const username = 'Paul';
+ const password = 'Paul';
+ return request(server)
+ .post('/user')
+ .send({ username, password })
+ .then((response) => {
+ expect(response.status).toEqual(400); // expect a 400 status code
+ expect(response).toHaveProperty('error');
+ });
+ });
+});
+
+xdescribe('User Deletion', () => {
+ it('deletes user from database', () => {
+ const username = `Paul${Date.now()}`;
+ const password = 'Paul';
+
+ return request(server)
+ .post('/user')
+ .send({ username, password })
+ .then(() => {
+ request(server)
+ .delete('/user')
+ .send({ username, password })
+ .then((response) => {
+ expect(response.body.deletedCount).toEqual(1);
+ });
+ });
+ });
+});
+
+xdescribe('User Authentication', () => {
+ const username = 'Paul';
+ const password = 'Paul';
+ it('should return user if user password is correct', () => {
+ // login route request
+ return request(server)
+ .get('/login')
+ .send({ username, password })
+ .then((response) => {
+ expect(response.body.username).toEqual(username);
+ });
+ });
+});
+
+xdescribe('User favorite creation', () => {
+ const username = 'Paul';
+ const password = 'Paul';
+ const favorite = {
+ name: 'HM Running Man Crunch',
+ type: 'cardio',
+ muscle: 'abdominals',
+ equipment: 'body_only',
+ difficulty: 'intermediate',
+ instructions: '',
+ };
+ it('should return the updated user if favorite was added', () => {
+ return request(server)
+ .patch('/user/favorite')
+ .send({ username, favorite })
+ .then((response) => {
+ expect(response.body.favorites[0].name).toEqual(favorite.name);
+ });
+ });
+});
+
+describe('User favorite deletion', () => {
+ const username = 'Paul';
+ const password = 'Paul';
+ //fake favorite to delete
+ const favorite = {
+ name: 'HM Running Man Crunch',
+ type: 'cardio',
+ muscle: 'abdominals',
+ equipment: 'body_only',
+ difficulty: 'intermediate',
+ instructions: '',
+ };
+ it('should return the updated user if favorite was deleted', () => {
+ return request(server)
+ .patch('/user/favorite')
+ .send({ username, favorite })
+ .then(() => {
+ return request(server)
+ .delete('/user/favorite')
+ .send({ username, favorite })
+ .then((response) => {
+ expect(response.body.favorites).toEqual([]);
+ });
+ });
+ });
+});
\ No newline at end of file
diff --git a/package.json b/package.json
index fc32da4..f76b14f 100644
--- a/package.json
+++ b/package.json
@@ -4,36 +4,61 @@
"description": "Stretches!",
"main": "index.js",
"scripts": {
- "test": "echo \"Error: no test specified\" && exit 1",
+ "test": "jest",
"start": "NODE_ENV=production node server/server.js",
"build": "NODE_ENV=production webpack",
- "dev": "concurrently \"nodemon server/server.js\" \"NODE_ENV=development webpack serve --open\""
+ "dev": "concurrently --kill-others \"nodemon server/server.js\" \"NODE_ENV=development webpack serve --open\""
+ },
+ "jest": {
+ "transform": {
+ "^.+\\.jsx?$": "babel-jest"
+ },
+ "moduleNameMapper": {
+ "\\.(css|less)$": "
/__mocks__/mock.js"
+ }
},
"author": "Alana Herlands, Serena Romano, Diane Moon, Josh Hall, Rodrigo Samour Calderon",
"license": "ISC",
"dependencies": {
+ "@fortawesome/fontawesome-svg-core": "^6.4.0",
+ "@fortawesome/free-regular-svg-icons": "^6.4.0",
+ "@fortawesome/free-solid-svg-icons": "^6.4.0",
+ "@fortawesome/react-fontawesome": "^0.2.0",
+ "@reduxjs/toolkit": "^1.9.5",
+ "bcrypt": "^5.1.0",
+ "cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.16.3",
+ "express-async-handler": "^1.2.0",
+ "font-awesome": "^4.7.0",
"mongodb": "^5.6.0",
"mongoose": "^6.8.0",
- "react": "^16.5.2",
- "react-dom": "^16.5.2",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
"react-hot-loader": "^4.6.3",
+ "react-redux": "^8.1.1",
"react-router": "^4.3.1",
- "react-router-dom": "^6.14.0"
+ "react-router-dom": "^6.14.0",
+ "react-timer-hook": "^3.0.6",
+ "redux": "^4.2.1",
+ "sass": "^1.63.6"
},
"devDependencies": {
- "@babel/core": "^7.22.5",
- "@babel/preset-env": "^7.22.5",
+ "@babel/core": "^7.22.6",
+ "@babel/preset-env": "^7.22.6",
"@babel/preset-react": "^7.22.5",
+ "@testing-library/react": "^14.0.0",
+ "babel-jest": "^29.6.0",
"babel-loader": "^9.1.2",
"concurrently": "^8.2.0",
"css-loader": "^6.8.1",
"dotenv-webpack": "^8.0.1",
"html-webpack-plugin": "^5.5.3",
+ "jest": "^29.6.0",
"nodemon": "^2.0.22",
"sass-loader": "^13.3.2",
"style-loader": "^3.3.3",
+ "supertest": "^6.3.3",
"webpack": "^5.88.1",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1"
diff --git a/webpack.config.js b/webpack.config.js
index 8481973..27695ad 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,60 +1,65 @@
// require the path
const path = require('path');
// require html webpack plugin
-const HtmlWebpackPlugin = require("html-webpack-plugin");
-
+const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
- entry: './client/index.js',
- output: {
- path: path.resolve(__dirname, 'build'),
- filename: 'bundle.js',
- },
- mode: process.env.NODE_ENV,
- module: {
- rules: [
- {
- test: /\.jsx?/,
- exclude: /node_modules/,
- //at this point install these: npm install -D babel-loader @babel/core @babel/preset-env @babel/preset-react
- use: {
- loader: 'babel-loader',
- options: {
- presets: ['@babel/preset-env', '@babel/preset-react']
- }
- }
- },
- //at this point install these: npm install -D sass style-loader css-loader sass-loader
- {
- test: /\.s?css/,
- use: [
- 'style-loader',
- 'css-loader',
- // 'sass-loader'
- ]
+ entry: './client/index.js',
+ output: {
+ path: path.resolve(__dirname, 'build'),
+ filename: 'bundle.js',
+ },
+ mode: process.env.NODE_ENV,
+ module: {
+ rules: [
+ {
+ // test: /\.jsx?/,
+ test: /\.(js|jsx)$/,
+ exclude: /node_modules/,
+ //at this point install these: npm install -D babel-loader @babel/core @babel/preset-env @babel/preset-react
+ use: {
+ loader: 'babel-loader',
+ options: {
+ presets: ['@babel/preset-env', '@babel/preset-react'],
+ plugins: ['@babel/plugin-syntax-jsx'],
+ },
},
- ]
- },
- //at this point, npm install webpack-dev-server --save-dev
- //at this point, npm install -D webpack-cli
- //at this point, npm install --save-dev html-webpack-plugin
- //also, ensure to require in HtmlWebpackPlugin at the top of this file
- // npm install nodemon
- //maybe npm install webpack ! we got an error that webpack command not found
- //npm install
- plugins: [
- new HtmlWebpackPlugin({
- template: './client/index.html',
- filename: 'index.html',
- //template: path.resolve(__dirname, './index.html'),//ANOTHER WAY
- }),
- // new Dotenv(),
+ },
+ //at this point install these: npm install -D sass style-loader css-loader sass-loader
+ {
+ test: /\.s?css/,
+ use: ['style-loader', 'css-loader', 'sass-loader'],
+ },
],
- //declare devServer
- devServer : {
- static : {
- directory : path.resolve(__dirname, 'build')
- },
- port: 3000,
- }
-}
+ },
+ //at this point, npm install webpack-dev-server --save-dev
+ //at this point, npm install -D webpack-cli
+ //at this point, npm install --save-dev html-webpack-plugin
+ //also, ensure to require in HtmlWebpackPlugin at the top of this file
+ // npm install nodemon
+ //maybe npm install webpack ! we got an error that webpack command not found
+ //npm install
+ plugins: [
+ new HtmlWebpackPlugin({
+ template: './client/index.html',
+ filename: 'index.html',
+ //template: path.resolve(__dirname, './index.html'),//ANOTHER WAY
+ }),
+ // new Dotenv(),
+ ],
+ //declare devServer
+ devServer: {
+ static: {
+ directory: path.resolve(__dirname, 'build'),
+ },
+ proxy: {
+ // context: ['/character','/characters'],
+ // target: 'http://localhost:3000'
+ '/api': 'http://localhost:3000',
+ },
+ headers: {
+ 'Access-Control-Allow-Origin': '*',
+ },
+ port: 8080,
+ },
+};