Compare commits

27 Commits

Author SHA1 Message Date
075c80ad07 Adds readme
Some checks failed
gitea-deepak/gog_frontend/pipeline/head There was a failure building this commit
2021-04-19 12:33:30 -05:00
10b0b7544c Adds authenticated app stuff
Some checks failed
gitea-deepak/gog_frontend/pipeline/head There was a failure building this commit
2021-04-19 12:32:57 -05:00
fd4983b2dd Rearranges and adds plans search thing 2021-02-07 11:31:59 -06:00
a74d17b315 styling for authenticated app
All checks were successful
gitea-deepak/gog_frontend/pipeline/head This commit looks good
2021-02-01 16:23:07 -06:00
3aa3d41b84 Updates colour 2021-02-01 14:46:40 -06:00
021cfe24f2 refactors style a bit and updates snaps
All checks were successful
gitea-deepak/gog_frontend/pipeline/head This commit looks good
2021-02-01 14:43:25 -06:00
e5e123670c Prettier for scss lint
All checks were successful
gitea-deepak/gog_frontend/pipeline/head This commit looks good
2021-02-01 14:09:11 -06:00
d53388c9ae Adds variable fonts! 2021-02-01 14:05:42 -06:00
0743fa3576 Updates snaps with new classnames
All checks were successful
gitea-deepak/gog_frontend/pipeline/head This commit looks good
2021-02-01 12:29:44 -06:00
ee7bcbcdfe Adds stylelint and related changes 2021-02-01 12:28:51 -06:00
d182657b44 fmt results 2021-02-01 12:14:03 -06:00
12d399511c Adds scss instead of css, fixes jest and adds test font
All checks were successful
gitea-deepak/gog_frontend/pipeline/head This commit looks good
2021-02-01 11:02:34 -06:00
d6211d91e6 Adds styling for unauthenticated app
All checks were successful
gitea-deepak/gog_frontend/pipeline/head This commit looks good
2021-01-31 18:44:41 -06:00
dc1b459c56 Adds tests for unauthenticated app
All checks were successful
gitea-deepak/gog_frontend/pipeline/head This commit looks good
2021-01-31 18:30:16 -06:00
8a088b718a Adds some snaps and gets set to add router 2021-01-31 16:30:36 -06:00
35777252d0 Adds user service to get people's names and things
All checks were successful
gitea-deepak/gog_frontend/pipeline/head This commit looks good
2021-01-24 20:02:38 -06:00
a4b4cddb36 Adds snapshot tests for authenticated app and unauthenticated app components, which now logout properly
All checks were successful
gitea-deepak/gog_frontend/pipeline/head This commit looks good
2021-01-24 19:10:56 -06:00
aa89bf9ca6 fmt changes 2021-01-24 14:54:55 -06:00
ec673515b8 Adds command to update jest snapshots
All checks were successful
gitea-deepak/gog_frontend/pipeline/head This commit looks good
2021-01-24 13:15:44 -06:00
93eea91ae6 Adds test for register.jsx and snapshot 2021-01-24 13:15:32 -06:00
55a8b26a7d Adds login tests
All checks were successful
gitea-deepak/gog_frontend/pipeline/head This commit looks good
2021-01-23 19:34:23 -06:00
4810a6ed74 Adds testing-library eslint plugin 2021-01-23 18:56:26 -06:00
6da0c2b84a Adds snapshot and begins to add login component tests
All checks were successful
gitea-deepak/gog_frontend/pipeline/head This commit looks good
2021-01-23 18:53:53 -06:00
ea710c3845 Adds services directory and env var flag- 2021-01-18 17:57:44 -06:00
3998d43041 Adds jest env var setup
Some checks failed
gitea-deepak/gog_frontend/pipeline/head There was a failure building this commit
2021-01-18 17:56:22 -06:00
d680d16a76 Adds more info to jest coverage 2021-01-18 17:55:56 -06:00
b78929b566 initial commit for auth context stuff
Some checks failed
gitea-deepak/gog_frontend/pipeline/head There was a failure building this commit
2021-01-18 10:27:45 -06:00
53 changed files with 3494 additions and 399 deletions

View File

@@ -9,6 +9,7 @@ extends:
- plugin:jest/style
- prettier
- prettier/standard
- plugin:react-hooks/recommended
parserOptions:
ecmaFeatures:
jsx: true
@@ -17,6 +18,7 @@ parserOptions:
plugins:
- react
- jest
- testing-library
rules:
no-tabs: 0
indent:

3
.stylelintrc.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": ["stylelint-prettier/recommended"]
}

6
Jenkinsfile vendored
View File

@@ -26,6 +26,12 @@ pipeline {
sh './do.sh _lint'
}
}
stage('stylelint') {
steps {
echo 'Running stylelint'
sh './do.sh _stylelint'
}
}
stage('test') {
steps {
echo 'Running test'

1
README.md Normal file
View File

@@ -0,0 +1 @@
React project for gogmagog frontend

3
__mocks__/styleMock.js Normal file
View File

@@ -0,0 +1,3 @@
// __mocks__/styleMock.js
module.exports = {};

12
do.sh
View File

@@ -11,7 +11,7 @@ build() {
run() {
echo "I am ${FUNCNAME[0]}ning"
npx webpack serve
API_ROOT=http://localhost:3000/api npx webpack serve
}
fmt() {
@@ -22,13 +22,21 @@ _lint() {
npx eslint src --ext .js,.jsx
}
_stylelint() {
npx stylelint "src/**/*.{css,scss,sass}"
}
_jest() {
npx jest --ci --coverage
}
_jestUpdateSnaps() {
npx jest -u
}
test() {
echo "I am ${FUNCNAME[0]}ing"
_lint && _jest
_lint && _stylelint && _jest
}
"$@" # <- execute the task

1
jest/setEnvVars.js Normal file
View File

@@ -0,0 +1 @@
process.env.API_ROOT = "http://localhost:8080/";

2649
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,9 @@
"name": "Deepak Mallubhotla"
},
"jest": {
"setupFiles": [
"<rootDir>/jest/setEnvVars.js"
],
"reporters": [
"default",
"jest-junit"
@@ -17,7 +20,16 @@
"coverageReporters": [
"text",
"cobertura"
]
],
"collectCoverage": true,
"collectCoverageFrom": [
"<rootDir>/src/**/*.jsx",
"!<rootDir>/src/**/*.test.jsx"
],
"coverageProvider": "babel",
"moduleNameMapper": {
"\\.(css|less|scss)$": "<rootDir>/__mocks__/styleMock.js"
}
},
"private": "true",
"license": "ISC",
@@ -26,6 +38,9 @@
"@babel/core": "^7.12.10",
"@babel/preset-env": "^7.12.11",
"@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",
"css-loader": "^5.0.1",
"eslint": "^7.17.0",
@@ -36,12 +51,22 @@
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"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-junit": "^12.0.0",
"prettier": "2.2.1",
"prettier": "^2.2.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",
"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-dev-server": "^3.11.1"
},
@@ -49,6 +74,10 @@
"prop-types": "^15.7.2",
"react": "^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"
]
}

View File

@@ -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);
}

View File

@@ -1,19 +1,18 @@
import React from "react";
import { hot } from "react-hot-loader";
import "./App.css";
import AllPlansComponent from "./components/AllPlansComponent.jsx";
import "./App.scss";
import { useAuth } from "./context/AuthContext";
import UnauthenticatedApp from "./screens/UnauthenticatedApp";
import AuthenticatedApp from "./screens/AuthenticatedApp";
class App extends React.Component {
render() {
return (
<div className="App">
<h1> Hello, World! </h1>
<div className="mainContent">
<AllPlansComponent />
</div>
</div>
);
}
function App() {
const { user } = useAuth();
return (
<div className="app">
<h1 className="main-title">GOG</h1>
{user ? <AuthenticatedApp /> : <UnauthenticatedApp />}
</div>
);
}
export default hot(module)(App);

17
src/App.scss Normal file
View 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;
}

View 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);

Binary file not shown.

View 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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -1,3 +0,0 @@
test("trying it out", () => {
expect(true).toEqual(true);
});

View File

@@ -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;

View File

@@ -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;

View 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;

View File

@@ -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;

View 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;

View File

@@ -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;

View 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;

View 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
View 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,
};

View File

@@ -1,4 +1,11 @@
import React from "react";
import ReactDOM from "react-dom";
import App from "./App.jsx";
ReactDOM.render(<App />, document.getElementById("root"));
import AppProviders from "./context";
ReactDOM.render(
<AppProviders>
<App />
</AppProviders>,
document.getElementById("root")
);

View 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&apos;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;

View 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;
}
}

View 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
View 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,
};

View 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);
});

View 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
View 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,
};

View 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);
});

View 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();
});

View 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;

View 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;
}

View 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."
);
});

View 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>
`;

View 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>
`;

View File

@@ -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>
`;

View 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();
});
};

View 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
View File

@@ -0,0 +1 @@
export const API_ROOT = process.env.API_ROOT;

View File

@@ -0,0 +1,5 @@
import { API_ROOT } from "./config";
test("testing config api root", () => {
expect(API_ROOT).toEqual("http://localhost:8080/");
});

View 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",
});
};
};

View 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();
});
};

View File

@@ -16,6 +16,33 @@ module.exports = {
test: /\.css$/,
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"] },
@@ -36,5 +63,10 @@ module.exports = {
},
},
},
plugins: [new webpack.HotModuleReplacementPlugin()],
plugins: [
new webpack.HotModuleReplacementPlugin(),
new webpack.EnvironmentPlugin({
API_ROOT: "http://localhost:8080/"
})
],
};