Compare commits
26 Commits
master
...
10b0b7544c
| Author | SHA1 | Date | |
|---|---|---|---|
|
10b0b7544c
|
|||
|
fd4983b2dd
|
|||
|
a74d17b315
|
|||
|
3aa3d41b84
|
|||
|
021cfe24f2
|
|||
|
e5e123670c
|
|||
|
d53388c9ae
|
|||
|
0743fa3576
|
|||
|
ee7bcbcdfe
|
|||
|
d182657b44
|
|||
|
12d399511c
|
|||
|
d6211d91e6
|
|||
|
dc1b459c56
|
|||
|
8a088b718a
|
|||
|
35777252d0
|
|||
|
a4b4cddb36
|
|||
|
aa89bf9ca6
|
|||
|
ec673515b8
|
|||
|
93eea91ae6
|
|||
|
55a8b26a7d
|
|||
|
4810a6ed74
|
|||
|
6da0c2b84a
|
|||
|
ea710c3845
|
|||
|
3998d43041
|
|||
|
d680d16a76
|
|||
|
b78929b566
|
@@ -9,6 +9,7 @@ extends:
|
|||||||
- plugin:jest/style
|
- plugin:jest/style
|
||||||
- prettier
|
- prettier
|
||||||
- prettier/standard
|
- prettier/standard
|
||||||
|
- plugin:react-hooks/recommended
|
||||||
parserOptions:
|
parserOptions:
|
||||||
ecmaFeatures:
|
ecmaFeatures:
|
||||||
jsx: true
|
jsx: true
|
||||||
@@ -17,6 +18,7 @@ parserOptions:
|
|||||||
plugins:
|
plugins:
|
||||||
- react
|
- react
|
||||||
- jest
|
- jest
|
||||||
|
- testing-library
|
||||||
rules:
|
rules:
|
||||||
no-tabs: 0
|
no-tabs: 0
|
||||||
indent:
|
indent:
|
||||||
|
|||||||
3
.stylelintrc.json
Normal file
3
.stylelintrc.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": ["stylelint-prettier/recommended"]
|
||||||
|
}
|
||||||
6
Jenkinsfile
vendored
6
Jenkinsfile
vendored
@@ -26,6 +26,12 @@ pipeline {
|
|||||||
sh './do.sh _lint'
|
sh './do.sh _lint'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
stage('stylelint') {
|
||||||
|
steps {
|
||||||
|
echo 'Running stylelint'
|
||||||
|
sh './do.sh _stylelint'
|
||||||
|
}
|
||||||
|
}
|
||||||
stage('test') {
|
stage('test') {
|
||||||
steps {
|
steps {
|
||||||
echo 'Running test'
|
echo 'Running test'
|
||||||
|
|||||||
3
__mocks__/styleMock.js
Normal file
3
__mocks__/styleMock.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// __mocks__/styleMock.js
|
||||||
|
|
||||||
|
module.exports = {};
|
||||||
12
do.sh
12
do.sh
@@ -11,7 +11,7 @@ build() {
|
|||||||
|
|
||||||
run() {
|
run() {
|
||||||
echo "I am ${FUNCNAME[0]}ning"
|
echo "I am ${FUNCNAME[0]}ning"
|
||||||
npx webpack serve
|
API_ROOT=http://localhost:3000/api npx webpack serve
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt() {
|
fmt() {
|
||||||
@@ -22,13 +22,21 @@ _lint() {
|
|||||||
npx eslint src --ext .js,.jsx
|
npx eslint src --ext .js,.jsx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_stylelint() {
|
||||||
|
npx stylelint "src/**/*.{css,scss,sass}"
|
||||||
|
}
|
||||||
|
|
||||||
_jest() {
|
_jest() {
|
||||||
npx jest --ci --coverage
|
npx jest --ci --coverage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_jestUpdateSnaps() {
|
||||||
|
npx jest -u
|
||||||
|
}
|
||||||
|
|
||||||
test() {
|
test() {
|
||||||
echo "I am ${FUNCNAME[0]}ing"
|
echo "I am ${FUNCNAME[0]}ing"
|
||||||
_lint && _jest
|
_lint && _stylelint && _jest
|
||||||
}
|
}
|
||||||
|
|
||||||
"$@" # <- execute the task
|
"$@" # <- execute the task
|
||||||
|
|||||||
1
jest/setEnvVars.js
Normal file
1
jest/setEnvVars.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
process.env.API_ROOT = "http://localhost:8080/";
|
||||||
2649
package-lock.json
generated
2649
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
39
package.json
39
package.json
@@ -10,6 +10,9 @@
|
|||||||
"name": "Deepak Mallubhotla"
|
"name": "Deepak Mallubhotla"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
|
"setupFiles": [
|
||||||
|
"<rootDir>/jest/setEnvVars.js"
|
||||||
|
],
|
||||||
"reporters": [
|
"reporters": [
|
||||||
"default",
|
"default",
|
||||||
"jest-junit"
|
"jest-junit"
|
||||||
@@ -17,7 +20,16 @@
|
|||||||
"coverageReporters": [
|
"coverageReporters": [
|
||||||
"text",
|
"text",
|
||||||
"cobertura"
|
"cobertura"
|
||||||
]
|
],
|
||||||
|
"collectCoverage": true,
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"<rootDir>/src/**/*.jsx",
|
||||||
|
"!<rootDir>/src/**/*.test.jsx"
|
||||||
|
],
|
||||||
|
"coverageProvider": "babel",
|
||||||
|
"moduleNameMapper": {
|
||||||
|
"\\.(css|less|scss)$": "<rootDir>/__mocks__/styleMock.js"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"private": "true",
|
"private": "true",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
@@ -26,6 +38,9 @@
|
|||||||
"@babel/core": "^7.12.10",
|
"@babel/core": "^7.12.10",
|
||||||
"@babel/preset-env": "^7.12.11",
|
"@babel/preset-env": "^7.12.11",
|
||||||
"@babel/preset-react": "^7.12.10",
|
"@babel/preset-react": "^7.12.10",
|
||||||
|
"@testing-library/jest-dom": "^5.11.9",
|
||||||
|
"@testing-library/react": "^11.2.3",
|
||||||
|
"@testing-library/user-event": "^12.6.2",
|
||||||
"babel-loader": "^8.2.2",
|
"babel-loader": "^8.2.2",
|
||||||
"css-loader": "^5.0.1",
|
"css-loader": "^5.0.1",
|
||||||
"eslint": "^7.17.0",
|
"eslint": "^7.17.0",
|
||||||
@@ -36,12 +51,22 @@
|
|||||||
"eslint-plugin-node": "^11.1.0",
|
"eslint-plugin-node": "^11.1.0",
|
||||||
"eslint-plugin-promise": "^4.2.1",
|
"eslint-plugin-promise": "^4.2.1",
|
||||||
"eslint-plugin-react": "^7.22.0",
|
"eslint-plugin-react": "^7.22.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.2.0",
|
||||||
|
"eslint-plugin-testing-library": "^3.10.1",
|
||||||
|
"file-loader": "^6.2.0",
|
||||||
"jest": "^26.6.3",
|
"jest": "^26.6.3",
|
||||||
"jest-junit": "^12.0.0",
|
"jest-junit": "^12.0.0",
|
||||||
"prettier": "2.2.1",
|
"prettier": "^2.2.1",
|
||||||
"react-test-renderer": "^17.0.1",
|
"react-test-renderer": "^17.0.1",
|
||||||
|
"resolve-url-loader": "^3.1.2",
|
||||||
|
"sass": "^1.32.5",
|
||||||
|
"sass-loader": "^10.1.1",
|
||||||
"style-loader": "^2.0.0",
|
"style-loader": "^2.0.0",
|
||||||
"webpack": "^5.11.1",
|
"stylelint": "^13.9.0",
|
||||||
|
"stylelint-config-prettier": "^8.0.2",
|
||||||
|
"stylelint-config-sass-guidelines": "^7.1.0",
|
||||||
|
"stylelint-prettier": "^1.1.2",
|
||||||
|
"webpack": "^5.19.0",
|
||||||
"webpack-cli": "^4.3.1",
|
"webpack-cli": "^4.3.1",
|
||||||
"webpack-dev-server": "^3.11.1"
|
"webpack-dev-server": "^3.11.1"
|
||||||
},
|
},
|
||||||
@@ -49,6 +74,10 @@
|
|||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
"react-hot-loader": "^4.13.0"
|
"react-hot-loader": "^4.13.0",
|
||||||
}
|
"react-router-dom": "^5.2.0"
|
||||||
|
},
|
||||||
|
"browserslist": [
|
||||||
|
"since 2017-06"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
14
src/App.css
14
src/App.css
@@ -1,14 +0,0 @@
|
|||||||
.App {
|
|
||||||
font: 14px "Century Gothic", Futura, sans-serif;
|
|
||||||
margin: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mainContent {
|
|
||||||
width: 80%;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: rgb(240, 240, 240);
|
|
||||||
color: rgb(50, 50, 50);
|
|
||||||
}
|
|
||||||
25
src/App.jsx
25
src/App.jsx
@@ -1,19 +1,18 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { hot } from "react-hot-loader";
|
import { hot } from "react-hot-loader";
|
||||||
import "./App.css";
|
import "./App.scss";
|
||||||
import AllPlansComponent from "./components/AllPlansComponent.jsx";
|
import { useAuth } from "./context/AuthContext";
|
||||||
|
import UnauthenticatedApp from "./screens/UnauthenticatedApp";
|
||||||
|
import AuthenticatedApp from "./screens/AuthenticatedApp";
|
||||||
|
|
||||||
class App extends React.Component {
|
function App() {
|
||||||
render() {
|
const { user } = useAuth();
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<div className="app">
|
||||||
<h1> Hello, World! </h1>
|
<h1 className="main-title">GOG</h1>
|
||||||
<div className="mainContent">
|
{user ? <AuthenticatedApp /> : <UnauthenticatedApp />}
|
||||||
<AllPlansComponent />
|
</div>
|
||||||
</div>
|
);
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default hot(module)(App);
|
export default hot(module)(App);
|
||||||
|
|||||||
17
src/App.scss
Normal file
17
src/App.scss
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
@use './common_styles/typography' as t;
|
||||||
|
@use './common_styles/colours';
|
||||||
|
|
||||||
|
.app {
|
||||||
|
font: t.$stack;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: colours.$background-color;
|
||||||
|
color: colours.$text-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-title {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
6
src/common_styles/colours.scss
Normal file
6
src/common_styles/colours.scss
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
$text-color: rgb(50, 50, 50);
|
||||||
|
$inverted-text-color: rgb(245, 235, 235);
|
||||||
|
$background-color: rgb(250, 245, 242);
|
||||||
|
|
||||||
|
$accent-color: rgb(70, 90, 200);
|
||||||
|
$dimmed-color: rgb(220, 220, 245);
|
||||||
BIN
src/common_styles/fonts/TrilbyVariable-VF-Testing.woff
Normal file
BIN
src/common_styles/fonts/TrilbyVariable-VF-Testing.woff
Normal file
Binary file not shown.
BIN
src/common_styles/fonts/TrilbyVariable-VF-Testing.woff2
Normal file
BIN
src/common_styles/fonts/TrilbyVariable-VF-Testing.woff2
Normal file
Binary file not shown.
BIN
src/common_styles/fonts/TrilbyVariable-VF_Italic-Testing.woff
Normal file
BIN
src/common_styles/fonts/TrilbyVariable-VF_Italic-Testing.woff
Normal file
Binary file not shown.
BIN
src/common_styles/fonts/TrilbyVariable-VF_Italic-Testing.woff2
Normal file
BIN
src/common_styles/fonts/TrilbyVariable-VF_Italic-Testing.woff2
Normal file
Binary file not shown.
33
src/common_styles/typography.scss
Normal file
33
src/common_styles/typography.scss
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
// @font-face {
|
||||||
|
// src: url('../fonts/FernMicro-Regular.woff2') format("woff2"),
|
||||||
|
// url('../fonts/FernMicro-Regular.woff') format("woff");
|
||||||
|
// font-family: 'FernMicro';
|
||||||
|
// font-style: normal;
|
||||||
|
// font-weight: normal;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// @font-face {
|
||||||
|
// src: url('../fonts/FernMicro-Italic.woff2') format("woff2"),
|
||||||
|
// url('../fonts/FernMicro-Italic.woff') format("woff");
|
||||||
|
// font-family: 'FernMicro';
|
||||||
|
// font-style: italic;
|
||||||
|
// font-weight: normal;
|
||||||
|
// }
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Trilby";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400 800;
|
||||||
|
src: url("fonts/TrilbyVariable-VF-Testing.woff2") format("woff2"),
|
||||||
|
url("fonts/TrilbyVariable-VF-Testing.woff") format("woff");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Trilby";
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400 800;
|
||||||
|
src: url("fonts/TrilbyVariable-VF_Italic-Testing.woff2") format("woff2"),
|
||||||
|
url("fonts/TrilbyVariable-VF_Italic-Testing.woff") format("woff");
|
||||||
|
}
|
||||||
|
|
||||||
|
$stack: 18px "Trilby", "Century Gothic", Futura, sans-serif;
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
.actionID,
|
|
||||||
.actionDescription,
|
|
||||||
.actionChunks {
|
|
||||||
margin-left: 1rem;
|
|
||||||
margin-right: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actionCompleted {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actionChunks {
|
|
||||||
flex: 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actionID {
|
|
||||||
flex: 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actionDescription {
|
|
||||||
flex: 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actionWrapper {
|
|
||||||
align-items: flex-end;
|
|
||||||
display: flex;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
padding-top: 1rem;
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { action } from "../types";
|
|
||||||
import "./Action.css";
|
|
||||||
|
|
||||||
class Action extends React.Component {
|
|
||||||
render() {
|
|
||||||
const action = this.props.action;
|
|
||||||
const completed = "completed_on" in action;
|
|
||||||
console.log([action.completed_on, completed]);
|
|
||||||
return (
|
|
||||||
<div className="actionWrapper">
|
|
||||||
<div className="actionCompleted">{completed ? "X" : " "}</div>
|
|
||||||
<div className="actionID">
|
|
||||||
ID: {action.action_id} | {action.plan_id}
|
|
||||||
</div>
|
|
||||||
<div className="actionDescription">{action.action_description}</div>
|
|
||||||
<div className="actionChunks">
|
|
||||||
{action.completed_chunks}/{action.estimated_chunks}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Action.propTypes = {
|
|
||||||
action: action,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Action;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
test("trying it out", () => {
|
|
||||||
expect(true).toEqual(true);
|
|
||||||
});
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import Action from "./Action.jsx";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import { action } from "../types";
|
|
||||||
|
|
||||||
class ActionsContainer extends React.Component {
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className="ActionsListWrapper">
|
|
||||||
<ul className="ActionsList">
|
|
||||||
{this.props.actions.map((action) => {
|
|
||||||
return (
|
|
||||||
<li className="Action" key={action.action_id}>
|
|
||||||
<Action action={action} />
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ActionsContainer.propTypes = {
|
|
||||||
actions: PropTypes.arrayOf(action),
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ActionsContainer;
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import PlanList from "./PlanList.jsx";
|
|
||||||
|
|
||||||
class AllPlansComponent extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
plans: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
getPlans() {
|
|
||||||
fetch("http://localhost:3000/api/plans", {
|
|
||||||
headers: {
|
|
||||||
Accept: "application/json",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((response) => response.json())
|
|
||||||
.then((data) => {
|
|
||||||
this.setState({
|
|
||||||
plans: data,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((error) => console.error(error));
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.getPlans();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className="AllPlans">
|
|
||||||
<PlanList plans={this.state.plans} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AllPlansComponent;
|
|
||||||
31
src/components/CurrentPlan/index.jsx
Normal file
31
src/components/CurrentPlan/index.jsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import Plan from "../Plan";
|
||||||
|
|
||||||
|
function CurrentPlan({ currentPlan, getPlanForID }) {
|
||||||
|
const [plan, setPlan] = useState();
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentPlan && currentPlan.plan_id) {
|
||||||
|
getPlanForID(currentPlan.plan_id).then((data) => {
|
||||||
|
setPlan(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [currentPlan, getPlanForID]);
|
||||||
|
|
||||||
|
const ret = currentPlan ? (
|
||||||
|
<div className="current-plan-wrapper">
|
||||||
|
<p>{JSON.stringify(currentPlan)}</p>
|
||||||
|
<Plan plan={plan} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>No current plan</div>
|
||||||
|
);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentPlan.propTypes = {
|
||||||
|
currentPlan: PropTypes.object,
|
||||||
|
getPlanForID: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CurrentPlan;
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import ActionsContainer from "./ActionsContainer.jsx";
|
|
||||||
import { plan } from "../types";
|
|
||||||
|
|
||||||
class Plan extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
actions: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
getActions() {
|
|
||||||
fetch(
|
|
||||||
"http://localhost:3000/api/actions?" +
|
|
||||||
new URLSearchParams({
|
|
||||||
plan_id: this.props.plan.plan_id,
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Accept: "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then((response) => response.json())
|
|
||||||
.then((data) => {
|
|
||||||
this.setState({
|
|
||||||
actions: data,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((error) => console.error(error));
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.getActions();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className="PlanActionsWrapper">
|
|
||||||
<div>
|
|
||||||
<h3>Plan for {this.props.plan.plan_date}</h3>
|
|
||||||
</div>
|
|
||||||
<ActionsContainer actions={this.state.actions.slice()} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Plan.propTypes = {
|
|
||||||
plan: plan,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Plan;
|
|
||||||
26
src/components/Plan/index.jsx
Normal file
26
src/components/Plan/index.jsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import React from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
function Plan({ initial_plan }) {
|
||||||
|
|
||||||
|
const onChangeDescription = (e) => {
|
||||||
|
const p = {
|
||||||
|
...plan,
|
||||||
|
plan_description: e.currentTarget.value
|
||||||
|
};
|
||||||
|
savePlan(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="plan-wrapper">
|
||||||
|
<p>{plan.plan_id}: </p>
|
||||||
|
<input type="text" onChange={onChangeDescription} value={plan.plan_description}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Plan.propTypes = {
|
||||||
|
plan: PropTypes.object
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Plan;
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import Plan from "./Plan.jsx";
|
|
||||||
import { plan } from "../types";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
|
|
||||||
class PlanList extends React.Component {
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className="PlanListWrapper">
|
|
||||||
<ul className="PlanList">
|
|
||||||
{this.props.plans.map((plan) => {
|
|
||||||
return (
|
|
||||||
<li className="Plan" key={plan.plan_id}>
|
|
||||||
<Plan plan={plan} />
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PlanList.propTypes = {
|
|
||||||
plans: PropTypes.arrayOf(plan),
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PlanList;
|
|
||||||
24
src/components/PlanList/index.jsx
Normal file
24
src/components/PlanList/index.jsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import React from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import Plan from "../Plan";
|
||||||
|
|
||||||
|
function PlanList({ plans, savePlan }) {
|
||||||
|
return (
|
||||||
|
<ul className="plan-list-wrapper">
|
||||||
|
{plans.map((plan) => {
|
||||||
|
return (
|
||||||
|
<li key={plan.plan_id}>
|
||||||
|
<Plan plan={plan} savePlan={savePlan}/>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
PlanList.propTypes = {
|
||||||
|
plans: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
savePlan: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PlanList;
|
||||||
73
src/context/AuthContext.jsx
Normal file
73
src/context/AuthContext.jsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { register, login } from "../services/auth-service";
|
||||||
|
import { getUserInfo } from "../services/user-service";
|
||||||
|
import { client } from "../services/api-client";
|
||||||
|
|
||||||
|
const localStorageKey = "__auth_token__";
|
||||||
|
const AuthContext = React.createContext();
|
||||||
|
|
||||||
|
const AuthProvider = (props) => {
|
||||||
|
const [user, setUser] = useState(null);
|
||||||
|
|
||||||
|
const setUserWithToken = (token) => {
|
||||||
|
if (user !== null) {
|
||||||
|
// Already set don't re-set.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
getUserInfo(token).then((user) => {
|
||||||
|
user.token = token;
|
||||||
|
setUser(user);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const tok = window.localStorage.getItem(localStorageKey);
|
||||||
|
|
||||||
|
if (tok !== null) {
|
||||||
|
setUserWithToken(tok);
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrappedLogin = (username, password) => {
|
||||||
|
login(username, password).then((token) => {
|
||||||
|
window.localStorage.setItem(localStorageKey, token);
|
||||||
|
setUserWithToken(token);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrappedRegister = register;
|
||||||
|
|
||||||
|
const wrappedLogout = () => {
|
||||||
|
window.localStorage.removeItem(localStorageKey);
|
||||||
|
setUser(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider
|
||||||
|
value={{
|
||||||
|
login: wrappedLogin,
|
||||||
|
register: wrappedRegister,
|
||||||
|
logout: wrappedLogout,
|
||||||
|
user,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function useAuth() {
|
||||||
|
const context = React.useContext(AuthContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error(`useAuth must be used within a AuthProvider`);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
function useClient() {
|
||||||
|
const { logout, user } = useAuth();
|
||||||
|
const token = user?.token;
|
||||||
|
return React.useCallback(
|
||||||
|
(endpoint, config) => client(endpoint, { ...config, token, logout }),
|
||||||
|
[token, logout]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { AuthProvider, AuthContext, useAuth, useClient };
|
||||||
13
src/context/index.jsx
Normal file
13
src/context/index.jsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { AuthProvider } from "./AuthContext";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
function AppProviders({ children }) {
|
||||||
|
return <AuthProvider>{children}</AuthProvider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppProviders;
|
||||||
|
|
||||||
|
AppProviders.propTypes = {
|
||||||
|
children: PropTypes.node,
|
||||||
|
};
|
||||||
@@ -1,4 +1,11 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import App from "./App.jsx";
|
import App from "./App.jsx";
|
||||||
ReactDOM.render(<App />, document.getElementById("root"));
|
import AppProviders from "./context";
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<AppProviders>
|
||||||
|
<App />
|
||||||
|
</AppProviders>,
|
||||||
|
document.getElementById("root")
|
||||||
|
);
|
||||||
|
|||||||
74
src/screens/AuthenticatedApp.jsx
Normal file
74
src/screens/AuthenticatedApp.jsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
|
import { useAuth, useClient } from "../context/AuthContext";
|
||||||
|
import "./AuthenticatedApp.scss";
|
||||||
|
import PlanList from "../components/PlanList";
|
||||||
|
import { getPlansFunc, savePlanFunc } from "../services/plans-service";
|
||||||
|
|
||||||
|
function AuthenticatedApp() {
|
||||||
|
const { logout, user } = useAuth();
|
||||||
|
|
||||||
|
const [plans, setPlans] = useState([]);
|
||||||
|
const [isLoading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState("HI");
|
||||||
|
const client = useClient();
|
||||||
|
// const getPlans = getPlansFunc(client);
|
||||||
|
|
||||||
|
const getPlans = useCallback(() => getPlansFunc(client)(), [client]);
|
||||||
|
const savePlan = useCallback(
|
||||||
|
(plan) => {
|
||||||
|
console.log("in the callback saveplan func");
|
||||||
|
console.log(plan);
|
||||||
|
return savePlanFunc(client)(plan)
|
||||||
|
}, [client]);
|
||||||
|
|
||||||
|
const updatePlan = (plan) => {
|
||||||
|
savePlan(plan)
|
||||||
|
.then((data) => {
|
||||||
|
console.log("returning from save plan");
|
||||||
|
console.log(data);
|
||||||
|
setError(null);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
setError(error);
|
||||||
|
})
|
||||||
|
const newPlans = plans.map(currentPlan => {
|
||||||
|
if (currentPlan.plan_id === plan.plan_id) {
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
return currentPlan;
|
||||||
|
});
|
||||||
|
setPlans(newPlans);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(true);
|
||||||
|
getPlans()
|
||||||
|
.then((data) => {
|
||||||
|
setPlans(data);
|
||||||
|
setLoading(false);
|
||||||
|
setError(null);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
setError(error);
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, [getPlans]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="authenticated-app-outer">
|
||||||
|
<button type="button" onClick={logout}>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
<div className="authenticated-app">
|
||||||
|
<p>
|
||||||
|
Howdy partner. Your name looks like it's {user.display_name}.
|
||||||
|
</p>
|
||||||
|
<p>Error: {error || "No errors"}</p>
|
||||||
|
<p>Loading: {isLoading ? "Loading" : "Not loading"}</p>
|
||||||
|
<PlanList plans={plans} savePlan={updatePlan} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AuthenticatedApp;
|
||||||
17
src/screens/AuthenticatedApp.scss
Normal file
17
src/screens/AuthenticatedApp.scss
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
@use '../common_styles/colours';
|
||||||
|
@use '../common_styles/typography';
|
||||||
|
|
||||||
|
.authenticated-app-outer {
|
||||||
|
.authenticated-app {
|
||||||
|
padding: 1em;
|
||||||
|
margin: 1em;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background-color: colours.$accent-color;
|
||||||
|
border: 0;
|
||||||
|
color: colours.$inverted-text-color;
|
||||||
|
font: typography.$stack;
|
||||||
|
margin: 10px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/screens/Login-snapshot.test.jsx
Normal file
13
src/screens/Login-snapshot.test.jsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import React from "react";
|
||||||
|
import renderer from "react-test-renderer";
|
||||||
|
import Login from "./Login";
|
||||||
|
|
||||||
|
test("Login Snapshot", () => {
|
||||||
|
const loginFunc = jest.fn();
|
||||||
|
|
||||||
|
const login = renderer.create(<Login login={loginFunc} />);
|
||||||
|
|
||||||
|
const tree = login.toJSON();
|
||||||
|
|
||||||
|
expect(tree).toMatchSnapshot();
|
||||||
|
});
|
||||||
44
src/screens/Login.jsx
Normal file
44
src/screens/Login.jsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
export default function Login({ login }) {
|
||||||
|
const [username, setUserName] = useState();
|
||||||
|
const [password, setPassword] = useState();
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
login(username, password);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="login-wrapper">
|
||||||
|
<h2>Please Log In</h2>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<label>
|
||||||
|
<p>Username</p>
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
type="text"
|
||||||
|
onChange={(e) => setUserName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<p>Password</p>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Login.propTypes = {
|
||||||
|
login: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
21
src/screens/Login.test.jsx
Normal file
21
src/screens/Login.test.jsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { render } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import Login from "./Login";
|
||||||
|
|
||||||
|
test("Login inputs", () => {
|
||||||
|
const loginFunc = jest.fn();
|
||||||
|
const login = render(<Login login={loginFunc} />);
|
||||||
|
|
||||||
|
const usernameInput = login.getByRole("textbox", { name: /Username/i });
|
||||||
|
const passwordInput = login.getByLabelText("Password");
|
||||||
|
const buttonInput = login.getByRole("button", { name: /Submit/i });
|
||||||
|
|
||||||
|
const username = "here's a username";
|
||||||
|
const pw = "here's a password";
|
||||||
|
|
||||||
|
userEvent.type(usernameInput, username);
|
||||||
|
userEvent.type(passwordInput, pw);
|
||||||
|
userEvent.click(buttonInput);
|
||||||
|
expect(loginFunc).toHaveBeenCalledWith(username, pw);
|
||||||
|
});
|
||||||
13
src/screens/Register-snapshot.test.jsx
Normal file
13
src/screens/Register-snapshot.test.jsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import React from "react";
|
||||||
|
import renderer from "react-test-renderer";
|
||||||
|
import Register from "./Register";
|
||||||
|
|
||||||
|
test("Register Snapshot", () => {
|
||||||
|
const registerFunc = jest.fn();
|
||||||
|
|
||||||
|
const register = renderer.create(<Register register={registerFunc} />);
|
||||||
|
|
||||||
|
const tree = register.toJSON();
|
||||||
|
|
||||||
|
expect(tree).toMatchSnapshot();
|
||||||
|
});
|
||||||
53
src/screens/Register.jsx
Normal file
53
src/screens/Register.jsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
export default function Register({ register }) {
|
||||||
|
const [username, setUserName] = useState();
|
||||||
|
const [displayName, setDisplayName] = useState();
|
||||||
|
const [password, setPassword] = useState();
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
register(username, displayName, password);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="register-wrapper">
|
||||||
|
<h2>Enter your data to register.</h2>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<label>
|
||||||
|
<p>Username</p>
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
type="text"
|
||||||
|
onChange={(e) => setUserName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<p>Name</p>
|
||||||
|
<input
|
||||||
|
id="displayName"
|
||||||
|
type="text"
|
||||||
|
onChange={(e) => setDisplayName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<p>Password</p>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<button type="submit">Sign me up</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Register.propTypes = {
|
||||||
|
register: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
24
src/screens/Register.test.jsx
Normal file
24
src/screens/Register.test.jsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { render } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import Register from "./Register";
|
||||||
|
|
||||||
|
test("Register inputs", () => {
|
||||||
|
const registerFunc = jest.fn();
|
||||||
|
const register = render(<Register register={registerFunc} />);
|
||||||
|
|
||||||
|
const usernameInput = register.getByRole("textbox", { name: /Username/ });
|
||||||
|
const displayNameInput = register.getByRole("textbox", { name: /Name/ });
|
||||||
|
const passwordInput = register.getByLabelText("Password");
|
||||||
|
const buttonInput = register.getByRole("button", { name: /Sign me up/i });
|
||||||
|
|
||||||
|
const username = "here's a username";
|
||||||
|
const displayName = "testing this is a display name for a person";
|
||||||
|
const pw = "here's a password";
|
||||||
|
|
||||||
|
userEvent.type(usernameInput, username);
|
||||||
|
userEvent.type(displayNameInput, displayName);
|
||||||
|
userEvent.type(passwordInput, pw);
|
||||||
|
userEvent.click(buttonInput);
|
||||||
|
expect(registerFunc).toHaveBeenCalledWith(username, displayName, pw);
|
||||||
|
});
|
||||||
18
src/screens/UnauthenticatedApp-snapshot.test.jsx
Normal file
18
src/screens/UnauthenticatedApp-snapshot.test.jsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import React from "react";
|
||||||
|
import renderer from "react-test-renderer";
|
||||||
|
import UnauthenticatedApp from "./UnauthenticatedApp";
|
||||||
|
import { AuthContext } from "../context/AuthContext";
|
||||||
|
|
||||||
|
test("UnauthenticatedApp Snapshot", () => {
|
||||||
|
const appRender = renderer.create(
|
||||||
|
<AuthContext.Provider
|
||||||
|
value={{ login: jest.fn(), register: jest.fn(), logout: jest.fn() }}
|
||||||
|
>
|
||||||
|
<UnauthenticatedApp />
|
||||||
|
</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
const tree = appRender.toJSON();
|
||||||
|
|
||||||
|
expect(tree).toMatchSnapshot();
|
||||||
|
});
|
||||||
40
src/screens/UnauthenticatedApp.jsx
Normal file
40
src/screens/UnauthenticatedApp.jsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { useAuth } from "../context/AuthContext";
|
||||||
|
import Login from "./Login";
|
||||||
|
import Register from "./Register";
|
||||||
|
import "./UnauthenticatedApp.scss";
|
||||||
|
|
||||||
|
function UnauthenticatedApp() {
|
||||||
|
const { login, register } = useAuth();
|
||||||
|
|
||||||
|
const [showRegister, setShowRegister] = useState(false);
|
||||||
|
|
||||||
|
const form = showRegister ? (
|
||||||
|
<Register register={register} />
|
||||||
|
) : (
|
||||||
|
<Login login={login} />
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div className="unauthenticated-app">
|
||||||
|
<div className="button-wrapper">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
onClick={() => setShowRegister(false)}
|
||||||
|
className="choose-login"
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
onClick={() => setShowRegister(true)}
|
||||||
|
className="choose-register"
|
||||||
|
>
|
||||||
|
Register
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{form}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UnauthenticatedApp;
|
||||||
31
src/screens/UnauthenticatedApp.scss
Normal file
31
src/screens/UnauthenticatedApp.scss
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
@use '../common_styles/colours';
|
||||||
|
@use '../common_styles/typography';
|
||||||
|
|
||||||
|
.unauthenticated-app {
|
||||||
|
button {
|
||||||
|
background-color: colours.$accent-color;
|
||||||
|
border: 0;
|
||||||
|
color: colours.$inverted-text-color;
|
||||||
|
font: typography.$stack;
|
||||||
|
margin: 10px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
}
|
||||||
|
.register-wrapper,
|
||||||
|
.login-wrapper {
|
||||||
|
padding: 1em;
|
||||||
|
margin: 1em;
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin: 1em auto;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.unauthenticated-app .choose-register {
|
||||||
|
background-color: colours.$dimmed-color;
|
||||||
|
color: colours.$text-color;
|
||||||
|
}
|
||||||
29
src/screens/UnauthenticatedApp.test.jsx
Normal file
29
src/screens/UnauthenticatedApp.test.jsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { render } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import UnauthenticatedApp from "./UnauthenticatedApp";
|
||||||
|
import "@testing-library/jest-dom/extend-expect";
|
||||||
|
import { AuthContext } from "../context/AuthContext";
|
||||||
|
|
||||||
|
test("Login inputs", () => {
|
||||||
|
const loginFunc = jest.fn();
|
||||||
|
const registerFunc = jest.fn();
|
||||||
|
const app = render(
|
||||||
|
<AuthContext.Provider
|
||||||
|
value={{ login: loginFunc, register: registerFunc, logout: jest.fn() }}
|
||||||
|
>
|
||||||
|
<UnauthenticatedApp />
|
||||||
|
</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
const chooseLoginButton = app.getByRole("button", { name: /Login/i });
|
||||||
|
const chooseRegisterButton = app.getByRole("button", { name: /Register/i });
|
||||||
|
|
||||||
|
userEvent.click(chooseLoginButton);
|
||||||
|
expect(app.getByRole("heading")).toHaveTextContent("Please Log In");
|
||||||
|
|
||||||
|
userEvent.click(chooseRegisterButton);
|
||||||
|
expect(app.getByRole("heading")).toHaveTextContent(
|
||||||
|
"Enter your data to register."
|
||||||
|
);
|
||||||
|
});
|
||||||
42
src/screens/__snapshots__/Login-snapshot.test.jsx.snap
Normal file
42
src/screens/__snapshots__/Login-snapshot.test.jsx.snap
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Login Snapshot 1`] = `
|
||||||
|
<div
|
||||||
|
className="login-wrapper"
|
||||||
|
>
|
||||||
|
<h2>
|
||||||
|
Please Log In
|
||||||
|
</h2>
|
||||||
|
<form
|
||||||
|
onSubmit={[Function]}
|
||||||
|
>
|
||||||
|
<label>
|
||||||
|
<p>
|
||||||
|
Username
|
||||||
|
</p>
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
onChange={[Function]}
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<p>
|
||||||
|
Password
|
||||||
|
</p>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
onChange={[Function]}
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
52
src/screens/__snapshots__/Register-snapshot.test.jsx.snap
Normal file
52
src/screens/__snapshots__/Register-snapshot.test.jsx.snap
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Register Snapshot 1`] = `
|
||||||
|
<div
|
||||||
|
className="register-wrapper"
|
||||||
|
>
|
||||||
|
<h2>
|
||||||
|
Enter your data to register.
|
||||||
|
</h2>
|
||||||
|
<form
|
||||||
|
onSubmit={[Function]}
|
||||||
|
>
|
||||||
|
<label>
|
||||||
|
<p>
|
||||||
|
Username
|
||||||
|
</p>
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
onChange={[Function]}
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<p>
|
||||||
|
Name
|
||||||
|
</p>
|
||||||
|
<input
|
||||||
|
id="displayName"
|
||||||
|
onChange={[Function]}
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<p>
|
||||||
|
Password
|
||||||
|
</p>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
onChange={[Function]}
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Sign me up
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`UnauthenticatedApp Snapshot 1`] = `
|
||||||
|
<div
|
||||||
|
className="unauthenticated-app"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="button-wrapper"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="choose-login"
|
||||||
|
onClick={[Function]}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="choose-register"
|
||||||
|
onClick={[Function]}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Register
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="login-wrapper"
|
||||||
|
>
|
||||||
|
<h2>
|
||||||
|
Please Log In
|
||||||
|
</h2>
|
||||||
|
<form
|
||||||
|
onSubmit={[Function]}
|
||||||
|
>
|
||||||
|
<label>
|
||||||
|
<p>
|
||||||
|
Username
|
||||||
|
</p>
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
onChange={[Function]}
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<p>
|
||||||
|
Password
|
||||||
|
</p>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
onChange={[Function]}
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
31
src/services/api-client.jsx
Normal file
31
src/services/api-client.jsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { API_ROOT } from "./config";
|
||||||
|
|
||||||
|
export const client = (
|
||||||
|
endpoint,
|
||||||
|
{ data, token, logout, headers: customHeaders, ...customConfig } = {}
|
||||||
|
) => {
|
||||||
|
const config = {
|
||||||
|
method: data ? "POST" : "GET",
|
||||||
|
body: data ? JSON.stringify(data) : undefined,
|
||||||
|
headers: {
|
||||||
|
Authorization: token ? `Bearer ${token}` : undefined,
|
||||||
|
"Content-Type": data ? "application/json" : undefined,
|
||||||
|
Accept: "application/json",
|
||||||
|
...customHeaders,
|
||||||
|
},
|
||||||
|
...customConfig,
|
||||||
|
};
|
||||||
|
const url = `${API_ROOT}${endpoint}`;
|
||||||
|
|
||||||
|
return fetch(url, config).then(async (response) => {
|
||||||
|
if (response.status === 401) {
|
||||||
|
logout();
|
||||||
|
window.location.assign(window.location);
|
||||||
|
return Promise.reject(Error("Gotta re-auth"));
|
||||||
|
}
|
||||||
|
if (!response.ok) {
|
||||||
|
return Promise.reject(Error(response.statusText));
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
});
|
||||||
|
};
|
||||||
48
src/services/auth-service.js
Normal file
48
src/services/auth-service.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { API_ROOT } from "./config";
|
||||||
|
|
||||||
|
export const register = (username, displayName, password) => {
|
||||||
|
const url = API_ROOT + "auth/register";
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
username: username,
|
||||||
|
display_name: displayName,
|
||||||
|
password: password,
|
||||||
|
};
|
||||||
|
return fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const login = (username, password) => {
|
||||||
|
const url = API_ROOT + "auth/tokens";
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
};
|
||||||
|
return fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw Error(response.statusText);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
if (data.token) {
|
||||||
|
return data.token;
|
||||||
|
}
|
||||||
|
throw Error("where's my token");
|
||||||
|
});
|
||||||
|
};
|
||||||
1
src/services/config.jsx
Normal file
1
src/services/config.jsx
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const API_ROOT = process.env.API_ROOT;
|
||||||
5
src/services/config.test.jsx
Normal file
5
src/services/config.test.jsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { API_ROOT } from "./config";
|
||||||
|
|
||||||
|
test("testing config api root", () => {
|
||||||
|
expect(API_ROOT).toEqual("http://localhost:8080/");
|
||||||
|
});
|
||||||
22
src/services/plans-service.jsx
Normal file
22
src/services/plans-service.jsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
export const getCurrentPlanFunc = (client) => {
|
||||||
|
return () => client("currentPlan");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPlanForIDFunc = (client) => {
|
||||||
|
return (id) => {
|
||||||
|
return client(`plans/${id}`);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPlansFunc = (client) => {
|
||||||
|
return () => client("plans/");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const savePlanFunc = (client) => {
|
||||||
|
return (plan) => {
|
||||||
|
return client(`plans/${plan.plan_id}`, {
|
||||||
|
data: plan,
|
||||||
|
method: "PUT",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
18
src/services/user-service.js
Normal file
18
src/services/user-service.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { API_ROOT } from "./config";
|
||||||
|
|
||||||
|
export const getUserInfo = (token) => {
|
||||||
|
const url = API_ROOT + "me";
|
||||||
|
|
||||||
|
return fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer " + token,
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
}).then((response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw Error(response.statusText);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -16,6 +16,33 @@ module.exports = {
|
|||||||
test: /\.css$/,
|
test: /\.css$/,
|
||||||
use: ["style-loader", "css-loader"],
|
use: ["style-loader", "css-loader"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
test: /\.s[ac]ss$/i,
|
||||||
|
use: [
|
||||||
|
"style-loader",
|
||||||
|
"css-loader",
|
||||||
|
"resolve-url-loader",
|
||||||
|
{
|
||||||
|
loader: "sass-loader",
|
||||||
|
options: {
|
||||||
|
implementation: require("sass"),
|
||||||
|
sourceMap: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: 'file-loader',
|
||||||
|
options: {
|
||||||
|
name: '[name].[ext]',
|
||||||
|
outputPath: 'fonts/'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
resolve: { extensions: ["*", ".js", ".jsx"] },
|
resolve: { extensions: ["*", ".js", ".jsx"] },
|
||||||
@@ -36,5 +63,10 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [new webpack.HotModuleReplacementPlugin()],
|
plugins: [
|
||||||
|
new webpack.HotModuleReplacementPlugin(),
|
||||||
|
new webpack.EnvironmentPlugin({
|
||||||
|
API_ROOT: "http://localhost:8080/"
|
||||||
|
})
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user