Finished login component

Did the following:
 - Extract front-end from component;
 - Added localStorage for remembering API details and access key;
 - Added routing to login component;

Todo:
 - Remove getApi*FromDTO;
 - Create ProtectedRoute to prevent unauthorized entry to /dashboard;
This commit is contained in:
2020-02-19 00:48:30 +01:00
parent 60079a1417
commit ae7d63db35
9 changed files with 254 additions and 62 deletions
+17 -6
View File
@@ -1,4 +1,4 @@
[![Netlify Status](https://api.netlify.com/api/v1/badges/9527e4dc-1bb2-40ef-b6eb-8515b91579bf/deploy-status)](https://app.netlify.com/sites/upbeat-agnesi-aad3fb/deploys) [![Netlify Status](https://api.netlify.com/api/v1/badges/9527e4dc-1bb2-40ef-b6eb-8515b91579bf/deploy-status)](https://upbeat-agnesi-aad3fb.netlify.com/)
## Available Scripts ## Available Scripts
@@ -9,15 +9,26 @@ In the project directory, you can run:
Runs the app in the development mode.<br /> Runs the app in the development mode.<br />
Open [http://localhost:3000](http://localhost:3000) to view it in the browser. Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.<br /> The page will reload if you make edits, you will also see any lint errors in the console.
You will also see any lint errors in the console.
### `yarn build` ### `yarn build`
Builds the app for production to the `build` folder.<br /> Builds the app for production to the `build` folder.<br />
It correctly bundles React in production mode and optimizes the build for the best performance. It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.<br /> The build is minified and the filenames include the hashes. Your app is ready to be deployed!
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. ### Requirement list
- [X] Create React web app
- [ ] It should have fields for giving start date, end date and access token as arguments to the app.
- [ ] App should make HTTP GET request to the API to fetch chat counts between those two given dates.
- [ ] The dashboard should render three values from the API.
- [ ] The dashboard should render a Paginated List.
### Software used
\# | Software | Reason
--- | --- | ---
1. | React | Requirement.
2. | React Router | To route between different components, a react router is used.
3. | TypeScript | To make use of the strong types, I made use of TypeScript
4. | Bootstrap | To structure the front-end, bootstrap is used.
+6
View File
@@ -3,6 +3,9 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.27",
"@fortawesome/free-solid-svg-icons": "^5.12.1",
"@fortawesome/react-fontawesome": "^0.1.8",
"@testing-library/jest-dom": "^4.2.4", "@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.4.0", "@testing-library/react": "^9.4.0",
"@testing-library/user-event": "^7.1.2", "@testing-library/user-event": "^7.1.2",
@@ -13,13 +16,16 @@
"@types/prop-types": "^15.7.3", "@types/prop-types": "^15.7.3",
"@types/react": "^16.9.19", "@types/react": "^16.9.19",
"@types/react-dom": "^16.9.5", "@types/react-dom": "^16.9.5",
"@types/react-fontawesome": "^1.6.4",
"@types/react-router-dom": "^5.1.3", "@types/react-router-dom": "^5.1.3",
"axios": "^0.19.2",
"bootstrap": "^4.4.1", "bootstrap": "^4.4.1",
"jquery": "^3.4.1", "jquery": "^3.4.1",
"popper.js": "^1.16.1", "popper.js": "^1.16.1",
"react": "^16.12.0", "react": "^16.12.0",
"react-bootstrap": "^1.0.0-beta.16", "react-bootstrap": "^1.0.0-beta.16",
"react-dom": "^16.12.0", "react-dom": "^16.12.0",
"react-fontawesome": "^1.7.1",
"react-router-dom": "^5.1.2", "react-router-dom": "^5.1.2",
"react-scripts": "3.3.1", "react-scripts": "3.3.1",
"typescript": "~3.7.2" "typescript": "~3.7.2"
+1
View File
@@ -8,6 +8,7 @@
<meta name="description" content="Web site created using create-react-app" /> <meta name="description" content="Web site created using create-react-app" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous" /> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous" />
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>Demo App - Jeroen Vijgen</title> <title>Demo App - Jeroen Vijgen</title>
</head> </head>
+4 -8
View File
@@ -6,18 +6,14 @@ import Login from '../LoginComponent/Login';
import Dashboard from '../DashboardComponent/Dashboard'; import Dashboard from '../DashboardComponent/Dashboard';
export default class App extends React.Component { export default class App extends React.Component {
componentDidMount() {
if((localStorage.getItem("APILink") !== undefined || localStorage.getItem("APILink") !== '') &&
(localStorage.getItem("AccessToken") !== undefined || localStorage.getItem("AccessToken") !== '')) {
return <Redirect to="/dashboard" />
};
}
render() { render() {
return ( return (
<Switch> <Switch>
<Route exact path="/" component={Login} /> <Route exact path="/"
<Route path="/dashboard" component={Dashboard} /> component={Login} />
<Route path="/dashboard"
component={Dashboard} />
<Redirect to="/" /> <Redirect to="/" />
</Switch> </Switch>
); );
+52 -35
View File
@@ -1,44 +1,61 @@
import React from 'react'; import React from 'react';
import { Card, Button, InputGroup, FormControl } from 'react-bootstrap'; import LoginView from '../../Views/LoginView';
import API_DTO from '../../DTOs/api.dto';
export default class Login extends React.Component<{
history: any
}, {
dto: any
}> {
export default class Login extends React.Component {
// eslint-disable-next-line // eslint-disable-next-line
constructor(props: any) { constructor(props: any) {
super(props); super(props);
this.persistApiLink = this.persistApiLink.bind(this);
this.persistAccessToken = this.persistAccessToken.bind(this);
this.verifyLogin = this.verifyLogin.bind(this);
this.getApiLinkFromDTO = this.getApiLinkFromDTO.bind(this);
this.getApiTokenFromDTO = this.getApiTokenFromDTO.bind(this);
} }
render () { componentDidMount() {
return ( const DTO = new API_DTO();
<div className="LoginComponent"> DTO.setApiLink(localStorage.getItem("APILink"));
<Card className="text-center"> DTO.setApiToken(localStorage.getItem("AccessToken"));
<Card.Body> this.setState({ dto: DTO });
<Card.Title>Please login to see the dashboard.</Card.Title> }
<Card.Text>
Please fill in the following items: <br /><br /> verifyLogin() {
<InputGroup className="mb-3"> console.log("API Link: " + this.state.dto.getApiLink());
<InputGroup.Prepend> console.log("API Token: " + this.state.dto.getApiToken());
<InputGroup.Text id="inputGroup-sizing-default">API url</InputGroup.Text> localStorage.setItem("lastLoggedIn", new Date().toUTCString());
</InputGroup.Prepend> // eslint-disable-next-line
<FormControl this.props.history.push('/dashboard');
aria-label="Default" };
aria-describedby="inputGroup-sizing-default"
/> getApiLinkFromDTO(): string {
</InputGroup> return this.state.dto.getApiLink();
<InputGroup className="mb-3"> }
<InputGroup.Prepend>
<InputGroup.Text id="inputGroup-sizing-default">API token</InputGroup.Text> getApiTokenFromDTO(): string {
</InputGroup.Prepend> return this.state.dto.getApiToken();
<FormControl }
aria-label="Default"
aria-describedby="inputGroup-sizing-default" persistApiLink(e: any): void {
/> this.state.dto.setApiLink(e.target.value);
</InputGroup> }
</Card.Text>
<Button variant="primary">Login</Button> persistAccessToken(e: any): void {
</Card.Body> this.state.dto.setApiToken(e.target.value);
<Card.Footer className="text-muted">Your last login was X-X-X @ XX:XX</Card.Footer> }
</Card>
</div> render() {
); return (<LoginView
persistApiLink={this.persistApiLink}
persistAccessToken={this.persistAccessToken}
verifyLogin={this.verifyLogin}
getApiLinkFromDTO={this.getApiLinkFromDTO}
getApiTokenFromDTO={this.getApiTokenFromDTO}
/>);
} }
} }
+29
View File
@@ -0,0 +1,29 @@
export default class API_DTO {
API_Link: string;
API_Token: string;
constructor() {
this.API_Link = "";
this.API_Token = "";
}
setApiLink(_api_link: string | null): void {
if (_api_link === null) _api_link = "";
this.API_Link = _api_link;
localStorage.setItem("APILink", this.API_Link);
}
setApiToken(_api_token: string | null): void {
if (_api_token === null) _api_token = "";
this.API_Token = _api_token;
localStorage.setItem("AccessToken", this.API_Token);
}
getApiLink() {
return this.API_Link;
}
getApiToken() {
return this.API_Token;
}
}
+44
View File
@@ -0,0 +1,44 @@
import React from 'react';
import { Card, Button, FormControl } from 'react-bootstrap';
export default class LoginView extends React.Component<{
persistApiLink: any,
persistAccessToken: any,
verifyLogin: any,
getApiLinkFromDTO: any,
getApiTokenFromDTO: any
}> {
render () {
return (
<div className="LoginComponent">
<Card className="text-center">
<Card.Body>
<Card.Title>Please login to see the dashboard.</Card.Title>
<Card.Text>
Please fill in the following items: <br /><br />
<FormControl aria-label="Default"
aria-describedby="inputGroup-sizing-default"
type="text"
placeholder="API Link"
defaultValue={localStorage.getItem("APILink") || ""}
onChange={(e: any) => this.props.persistApiLink(e)}
/>
<FormControl
aria-label="Default"
aria-describedby="inputGroup-sizing-default"
type="text"
placeholder="API Token"
defaultValue={localStorage.getItem("AccessToken") || ""}
onChange={(e: any) => this.props.persistAccessToken(e)}
/>
</Card.Text>
<Button variant="primary" onClick={this.props.verifyLogin}>Login </Button>
</Card.Body>
{ localStorage.getItem("lastLoggedIn") !== null &&
<Card.Footer className="text-muted">Your last login was { localStorage.getItem("lastLoggedIn") }</Card.Footer>
}
</Card>
</div>
);
}
}
+5 -1
View File
@@ -4,4 +4,8 @@ import './index.css';
import { BrowserRouter as Router } from 'react-router-dom'; import { BrowserRouter as Router } from 'react-router-dom';
import App from './Components/AppComponent/App'; import App from './Components/AppComponent/App';
ReactDOM.render(<Router><App /></Router>, document.getElementById('App')); ReactDOM.render(
<Router >
<App />
</Router>,
document.getElementById('App'));
+95 -11
View File
@@ -1259,6 +1259,43 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@fortawesome/fontawesome-common-types@npm:^0.2.27":
version: 0.2.27
resolution: "@fortawesome/fontawesome-common-types@npm:0.2.27"
checksum: 2eb1f4adbe31a6533b3d3fdd20e37fd7c9e61cf29d144fcb3d225a5d16b57c16fa2a72dd6862382ea7b184aca85fd65b66781de1570a4b911eb4e9b39a74f9de
languageName: node
linkType: hard
"@fortawesome/fontawesome-svg-core@npm:^1.2.27":
version: 1.2.27
resolution: "@fortawesome/fontawesome-svg-core@npm:1.2.27"
dependencies:
"@fortawesome/fontawesome-common-types": ^0.2.27
checksum: 000027a6d1a33a152e4799bd0c3c1507205697f86ddc0961f69f83c8561bcf93327353980efc8794f1dd7e7a1a50a36f710198d9f7e03855d9832fd5df6958f9
languageName: node
linkType: hard
"@fortawesome/free-solid-svg-icons@npm:^5.12.1":
version: 5.12.1
resolution: "@fortawesome/free-solid-svg-icons@npm:5.12.1"
dependencies:
"@fortawesome/fontawesome-common-types": ^0.2.27
checksum: f966c13f826dfe57b2e2a212d5968ce7d4f9670ffb26d675259a2f0d19dfa777d269e6e4506e16d00da34943ae8526eb1fb2b889467f309ca3dbf371a57aec94
languageName: node
linkType: hard
"@fortawesome/react-fontawesome@npm:^0.1.8":
version: 0.1.8
resolution: "@fortawesome/react-fontawesome@npm:0.1.8"
dependencies:
prop-types: ^15.5.10
peerDependencies:
"@fortawesome/fontawesome-svg-core": ^1.2.20
react: 16.x
checksum: c13777b29061dcca6d39ee2f30d820e1221e4293ce87d336197bb6e0c19adbd4bbb2963be03c71f8e093a36e813b8acf2079849e62a7450f1d871a8678ecd5d6
languageName: node
linkType: hard
"@hapi/address@npm:2.x.x": "@hapi/address@npm:2.x.x":
version: 2.1.4 version: 2.1.4
resolution: "@hapi/address@npm:2.1.4" resolution: "@hapi/address@npm:2.1.4"
@@ -1769,7 +1806,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/history@npm:*": "@types/history@npm:*, @types/history@npm:^4.7.5":
version: 4.7.5 version: 4.7.5
resolution: "@types/history@npm:4.7.5" resolution: "@types/history@npm:4.7.5"
checksum: 16a19c2b1aee6d0ff055940e4ca29db26aa8b0c8ff69161e70dec2151abb4fe3d5be846577c696a5d870a281e362669707ff43323d435a3d84811e6a238348f1 checksum: 16a19c2b1aee6d0ff055940e4ca29db26aa8b0c8ff69161e70dec2151abb4fe3d5be846577c696a5d870a281e362669707ff43323d435a3d84811e6a238348f1
@@ -1876,6 +1913,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/react-fontawesome@npm:^1.6.4":
version: 1.6.4
resolution: "@types/react-fontawesome@npm:1.6.4"
dependencies:
"@types/react": "*"
checksum: ccf31aafa5c595ae9736ce5a9942c141a75c9ad11f8977c62ed4b06d7e1961ee8f226257fe867bd6df72ee62eed5bf8552250ada06d2fe38f8988a4a775fc0f6
languageName: node
linkType: hard
"@types/react-router-dom@npm:^5.1.3": "@types/react-router-dom@npm:^5.1.3":
version: 5.1.3 version: 5.1.3
resolution: "@types/react-router-dom@npm:5.1.3" resolution: "@types/react-router-dom@npm:5.1.3"
@@ -2817,6 +2863,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"axios@npm:^0.19.2":
version: 0.19.2
resolution: "axios@npm:0.19.2"
dependencies:
follow-redirects: 1.5.10
checksum: 28c860c203dc1e545a899eea83c95c51bb7de4a21ffef4781de11331207371c9cb1a680bcc7122dda936cd5e51f83f7b0a0eccc50801229b5e4914c8f2dd8907
languageName: node
linkType: hard
"axobject-query@npm:^2.0.2": "axobject-query@npm:^2.0.2":
version: 2.1.1 version: 2.1.1
resolution: "axobject-query@npm:2.1.1" resolution: "axobject-query@npm:2.1.1"
@@ -4530,6 +4585,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"debug@npm:=3.1.0":
version: 3.1.0
resolution: "debug@npm:3.1.0"
dependencies:
ms: 2.0.0
checksum: 31ac62be845b3e2bc49ea566665749e58030123e6d0b2960cb7c730037dc825f46d012e3f63f43fc9d60de3c52edbce705d22cb7e571e2822c81bc5d5ae09da9
languageName: node
linkType: hard
"debug@npm:^3.0.0, debug@npm:^3.1.1, debug@npm:^3.2.5, debug@npm:^3.2.6": "debug@npm:^3.0.0, debug@npm:^3.1.1, debug@npm:^3.2.5, debug@npm:^3.2.6":
version: 3.2.6 version: 3.2.6
resolution: "debug@npm:3.2.6" resolution: "debug@npm:3.2.6"
@@ -4670,9 +4734,13 @@ __metadata:
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "demo-app@workspace:." resolution: "demo-app@workspace:."
dependencies: dependencies:
"@fortawesome/fontawesome-svg-core": ^1.2.27
"@fortawesome/free-solid-svg-icons": ^5.12.1
"@fortawesome/react-fontawesome": ^0.1.8
"@testing-library/jest-dom": ^4.2.4 "@testing-library/jest-dom": ^4.2.4
"@testing-library/react": ^9.4.0 "@testing-library/react": ^9.4.0
"@testing-library/user-event": ^7.1.2 "@testing-library/user-event": ^7.1.2
"@types/history": ^4.7.5
"@types/istanbul-lib-coverage": ^2.0.1 "@types/istanbul-lib-coverage": ^2.0.1
"@types/jest": ^24.9.1 "@types/jest": ^24.9.1
"@types/mocha": ^7.0.1 "@types/mocha": ^7.0.1
@@ -4680,23 +4748,26 @@ __metadata:
"@types/prop-types": ^15.7.3 "@types/prop-types": ^15.7.3
"@types/react": ^16.9.19 "@types/react": ^16.9.19
"@types/react-dom": ^16.9.5 "@types/react-dom": ^16.9.5
"@types/react-fontawesome": ^1.6.4
"@types/react-router-dom": ^5.1.3 "@types/react-router-dom": ^5.1.3
"@typescript-eslint/eslint-plugin": ^2.19.2 "@typescript-eslint/eslint-plugin": ^2.19.2
"@typescript-eslint/parser": ^2.19.2 "@typescript-eslint/parser": ^2.19.2
axios: ^0.19.2
bootstrap: ^4.4.1 bootstrap: ^4.4.1
eslint: ^6.8.0 eslint: ^6.8.0
eslint-config-prettier: ^6.10.0 eslint-config-prettier: ^6.10.0
eslint-plugin-prettier: ^3.1.2 eslint-plugin-prettier: ^3.1.2
eslint-plugin-react: ^7.18.3 eslint-plugin-react: ^7.18.3
history: ^4.10.1
jquery: ^3.4.1 jquery: ^3.4.1
popper.js: ^1.16.1 popper.js: ^1.16.1
prettier: ^1.19.1 prettier: ^1.19.1
react: ^16.12.0 react: ^16.12.0
react-bootstrap: ^1.0.0-beta.16 react-bootstrap: ^1.0.0-beta.16
react-dom: ^16.12.0 react-dom: ^16.12.0
react-fontawesome: ^1.7.1
react-router-dom: ^5.1.2 react-router-dom: ^5.1.2
react-scripts: 3.3.1 react-scripts: 3.3.1
store: ^2.0.12
typescript: ~3.7.2 typescript: ~3.7.2
languageName: unknown languageName: unknown
linkType: soft linkType: soft
@@ -5994,6 +6065,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"follow-redirects@npm:1.5.10":
version: 1.5.10
resolution: "follow-redirects@npm:1.5.10"
dependencies:
debug: =3.1.0
checksum: 23c6333d60b73b899f9495d67b3dd5b2b99c26cd76278d03def3c39c715f0eed53cbfe1eb1e342a7281041961fe2c69c03daa97279df66a552ab6fb3d22059fc
languageName: node
linkType: hard
"follow-redirects@npm:^1.0.0": "follow-redirects@npm:^1.0.0":
version: 1.10.0 version: 1.10.0
resolution: "follow-redirects@npm:1.10.0" resolution: "follow-redirects@npm:1.10.0"
@@ -6582,7 +6662,7 @@ fsevents@^1.2.7:
languageName: node languageName: node
linkType: hard linkType: hard
"history@npm:^4.9.0": "history@npm:^4.10.1, history@npm:^4.9.0":
version: 4.10.1 version: 4.10.1
resolution: "history@npm:4.10.1" resolution: "history@npm:4.10.1"
dependencies: dependencies:
@@ -11118,7 +11198,7 @@ fsevents@^1.2.7:
languageName: node languageName: node
linkType: hard linkType: hard
"prop-types@npm:^15.6.2, prop-types@npm:^15.7.2": "prop-types@npm:^15.5.10, prop-types@npm:^15.5.6, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2":
version: 15.7.2 version: 15.7.2
resolution: "prop-types@npm:15.7.2" resolution: "prop-types@npm:15.7.2"
dependencies: dependencies:
@@ -11425,6 +11505,17 @@ fsevents@^1.2.7:
languageName: node languageName: node
linkType: hard linkType: hard
"react-fontawesome@npm:^1.7.1":
version: 1.7.1
resolution: "react-fontawesome@npm:1.7.1"
dependencies:
prop-types: ^15.5.6
peerDependencies:
react: ">=0.12.0"
checksum: 9cedcd84255cb13196e7db70d49e928dc26982d059ae72396171f132933a38f50c9454b93f1f127edfd4a7fc7a4d01611f82dd73e86110724884ae9e9c064596
languageName: node
linkType: hard
"react-is@npm:^16.3.2, react-is@npm:^16.6.0, react-is@npm:^16.7.0, react-is@npm:^16.8.1, react-is@npm:^16.8.4": "react-is@npm:^16.3.2, react-is@npm:^16.6.0, react-is@npm:^16.7.0, react-is@npm:^16.8.1, react-is@npm:^16.8.4":
version: 16.12.0 version: 16.12.0
resolution: "react-is@npm:16.12.0" resolution: "react-is@npm:16.12.0"
@@ -12833,13 +12924,6 @@ resolve@1.1.7:
languageName: node languageName: node
linkType: hard linkType: hard
"store@npm:^2.0.12":
version: 2.0.12
resolution: "store@npm:2.0.12"
checksum: 101053f2842b6a186d8556ef5a176feb4494cfe4a182b454c5c7150362c67344abafd8e012c7769ea51745cdff38447c3e0c9d5ef36740bcc65e174adb605309
languageName: node
linkType: hard
"stream-browserify@npm:^2.0.1": "stream-browserify@npm:^2.0.1":
version: 2.0.2 version: 2.0.2
resolution: "stream-browserify@npm:2.0.2" resolution: "stream-browserify@npm:2.0.2"