diff --git a/.eslintrc.js b/.eslintrc.js
index 64dee9b..a8e8325 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -1,5 +1,6 @@
module.exports = {
extends: ['airbnb-typescript-prettier'],
+ ignorePatterns: ['.eslintrc.js'],
// Rules can be here to override the preset of eslint from airbnb, if they are too strict.
rules: {
'@typescript-eslint/ban-ts-comment': 'off',
@@ -22,6 +23,8 @@ module.exports = {
'no-console': 'off',
'react/destructuring-assignment': 0,
+ 'react/jsx-props-no-spreading': 'off',
+ 'import/prefer-default-export': 0,
// 'react/prop-types': 0, -> this is an example
},
overrides: [
diff --git a/package-lock.json b/package-lock.json
index fe60949..5f4b849 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1273,6 +1273,11 @@
"@hapi/hoek": "^8.3.0"
}
},
+ "@hookform/resolvers": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-1.3.4.tgz",
+ "integrity": "sha512-K56VLSInXNIT/r14pkzRn1FJclqzGOWqpe3Bf0kz2Hf98ZOmRRFh4fhB7F3ofqCQ03CEQQkV44CTg7ql6nEvEg=="
+ },
"@istanbuljs/load-nyc-config": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
@@ -2026,6 +2031,11 @@
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4="
},
+ "@types/lodash": {
+ "version": "4.14.168",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.168.tgz",
+ "integrity": "sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q=="
+ },
"@types/minimatch": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
@@ -2872,6 +2882,14 @@
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.1.1.tgz",
"integrity": "sha512-5Kgy8Cz6LPC9DJcNb3yjAXTu3XihQgEdnIg50c//zOC/MyLP0Clg+Y8Sh9ZjjnvBrDZU4DgXS9C3T9r4/scGZQ=="
},
+ "axios": {
+ "version": "0.21.1",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
+ "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
+ "requires": {
+ "follow-redirects": "^1.10.0"
+ }
+ },
"axobject-query": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
@@ -10031,6 +10049,11 @@
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ=="
},
+ "nanoclone": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz",
+ "integrity": "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA=="
+ },
"nanoid": {
"version": "3.1.20",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz",
@@ -12289,6 +12312,11 @@
"react-is": "^16.8.1"
}
},
+ "property-expr": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.4.tgz",
+ "integrity": "sha512-sFPkHQjVKheDNnPvotjQmm3KD3uk1fWKUN7CrpdbwmUx3CrG3QiM8QpTSimvig5vTXmTvjz7+TDvXOI9+4rkcg=="
+ },
"proxy-addr": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
@@ -12623,6 +12651,11 @@
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
"integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
},
+ "react-hook-form": {
+ "version": "6.15.1",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-6.15.1.tgz",
+ "integrity": "sha512-bL0LQuQ3OlM3JYfbacKtBPLOHhmgYz8Lj6ivMrvu2M6e1wnt4sbGRtPEPYCc/8z3WDbjrMwfAfLX92OsB65pFA=="
+ },
"react-icons": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.2.0.tgz",
@@ -15145,6 +15178,11 @@
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
},
+ "toposort": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
+ "integrity": "sha1-riF2gXXRVZ1IvvNUILL0li8JwzA="
+ },
"tough-cookie": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz",
@@ -17175,6 +17213,20 @@
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="
+ },
+ "yup": {
+ "version": "0.32.8",
+ "resolved": "https://registry.npmjs.org/yup/-/yup-0.32.8.tgz",
+ "integrity": "sha512-SZulv5FIZ9d5H99EN5tRCRPXL0eyoYxWIP1AacCrjC9d4DfP13J1dROdKGfpfRHT3eQB6/ikBl5jG21smAfCkA==",
+ "requires": {
+ "@babel/runtime": "^7.10.5",
+ "@types/lodash": "^4.14.165",
+ "lodash": "^4.17.20",
+ "lodash-es": "^4.17.11",
+ "nanoclone": "^0.2.1",
+ "property-expr": "^2.0.4",
+ "toposort": "^2.0.2"
+ }
}
}
}
diff --git a/package.json b/package.json
index 4180365..d0bd862 100644
--- a/package.json
+++ b/package.json
@@ -3,22 +3,26 @@
"version": "0.1.0",
"private": true,
"dependencies": {
+ "@hookform/resolvers": "^1.3.4",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
+ "axios": "^0.21.1",
"brace": "^0.11.1",
"jsoneditor": "^9.1.9",
"jsoneditor-react": "^3.1.0",
"node-sass": "^4.14.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
+ "react-hook-form": "^6.15.1",
"react-icons": "^4.2.0",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.1",
"semantic-ui-css": "^2.4.1",
"semantic-ui-react": "^2.0.3",
"typescript": "^4.1.3",
- "web-vitals": "^0.2.4"
+ "web-vitals": "^0.2.4",
+ "yup": "^0.32.8"
},
"scripts": {
"start": "react-scripts start",
diff --git a/src/App.tsx b/src/App.tsx
index ed84c33..89ad187 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,21 +1,43 @@
-import React from 'react';
-import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
+import React, { useState } from 'react';
+import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
+import './App.scss';
import BaseLayout from './components/layouts/BaseLayout';
-import IpfsList from './pages/ipfs/IpfsList';
+import { storageNames } from './config';
+import UserContext from './hooks/UserContext';
import Ipfs from './pages/ipfs/Ipfs';
-
-import './App.scss';
+import IpfsList from './pages/ipfs/IpfsList';
+import Login from './pages/Login/Login';
+import { ProtectedRoute } from './shared/ProtectedRoute';
function App(): JSX.Element {
+ // Handle user state
+ const accessToken = localStorage.getItem(storageNames.user) || '';
+ const [isLoggedIn, setLoggedIn] = useState(!!accessToken);
+
+ // Handle user login by setting the storage and state
+ const login = (token: string): void => {
+ setLoggedIn(true);
+ localStorage.setItem(storageNames.user, token);
+ };
+
+ // Handle user logout
+ const logout = (): void => {
+ setLoggedIn(false);
+ localStorage.removeItem(storageNames.user);
+ };
+
return (
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/src/components/layouts/BaseLayout/BaseLayout.tsx b/src/components/layouts/BaseLayout/BaseLayout.tsx
index 9d43d7c..2cdf024 100644
--- a/src/components/layouts/BaseLayout/BaseLayout.tsx
+++ b/src/components/layouts/BaseLayout/BaseLayout.tsx
@@ -1,6 +1,6 @@
import React from 'react';
import { Container } from 'semantic-ui-react';
-import NavigationBar from '../NavigationBar';
+import { NavigationBar } from '../NavigationBar';
interface BaseLayoutProps {
children: any;
diff --git a/src/components/layouts/NavigationBar/NavigationBar.tsx b/src/components/layouts/NavigationBar/NavigationBar.tsx
index 8083009..daf082b 100644
--- a/src/components/layouts/NavigationBar/NavigationBar.tsx
+++ b/src/components/layouts/NavigationBar/NavigationBar.tsx
@@ -1,10 +1,16 @@
-import React from 'react';
+import React, { useContext } from 'react';
import { FaUserCircle } from 'react-icons/fa';
import { Link } from 'react-router-dom';
-
+import UserContext from '../../../hooks/UserContext';
import './NavigationBar.scss';
const NavigationBar = (): JSX.Element => {
+ const userContext = useContext(UserContext);
+
+ const logout = (): void => {
+ userContext.logout();
+ };
+
return (
-
- {/* TODO: Need to be a user page */}
-
-
-
-
+ {userContext.isLoggedIn ? (
+
+ {/* TODO: Need to have this as a small menu showing up */}
+
+
+
+
+ ) : (
+ ''
+ )}
);
diff --git a/src/components/layouts/NavigationBar/index.ts b/src/components/layouts/NavigationBar/index.ts
new file mode 100644
index 0000000..d4fd1e8
--- /dev/null
+++ b/src/components/layouts/NavigationBar/index.ts
@@ -0,0 +1 @@
+export { default as NavigationBar } from './NavigationBar';
diff --git a/src/components/layouts/NavigationBar/index.tsx b/src/components/layouts/NavigationBar/index.tsx
deleted file mode 100644
index 513a881..0000000
--- a/src/components/layouts/NavigationBar/index.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-import NavigationBar from './NavigationBar';
-
-export default NavigationBar;
diff --git a/src/config/api.ts b/src/config/api.ts
new file mode 100644
index 0000000..9df0189
--- /dev/null
+++ b/src/config/api.ts
@@ -0,0 +1,5 @@
+export const apis = {
+ // TODO: Replace this by the BE api urls we would be using. Probably storing this in a config file
+ url: 'https://jsonplaceholder.typicode.com',
+ timeout: 2 * 60 * 1000, // 2 minutes timeout
+};
diff --git a/src/config/index.ts b/src/config/index.ts
new file mode 100644
index 0000000..f8cb76b
--- /dev/null
+++ b/src/config/index.ts
@@ -0,0 +1,2 @@
+export { apis } from './api';
+export { storageNames } from './storage-names';
diff --git a/src/config/storage-names.ts b/src/config/storage-names.ts
new file mode 100644
index 0000000..b36ebf7
--- /dev/null
+++ b/src/config/storage-names.ts
@@ -0,0 +1,3 @@
+export const storageNames = {
+ user: 'pl11',
+};
diff --git a/src/hooks/UserContext.ts b/src/hooks/UserContext.ts
new file mode 100644
index 0000000..f33236e
--- /dev/null
+++ b/src/hooks/UserContext.ts
@@ -0,0 +1,11 @@
+import { createContext } from 'react';
+
+interface UserContextState {
+ isLoggedIn: boolean;
+ login: (token: string) => void;
+ logout: () => void;
+}
+
+const UserContext = createContext({} as UserContextState);
+
+export default UserContext;
diff --git a/src/hooks/index.ts b/src/hooks/index.ts
new file mode 100644
index 0000000..4780e53
--- /dev/null
+++ b/src/hooks/index.ts
@@ -0,0 +1 @@
+export { default as UserContext } from './UserContext';
diff --git a/src/pages/Login/Login.scss b/src/pages/Login/Login.scss
new file mode 100644
index 0000000..d12c6ab
--- /dev/null
+++ b/src/pages/Login/Login.scss
@@ -0,0 +1,83 @@
+$elevation-card: 0 0 1px 0 rgba(8, 11, 14, 0.6), 0 16px 16px -1px rgba(8, 11, 14, 0.1);
+$color-secondary: #e1e4e8;
+$error-color: red;
+
+.login {
+ &__card {
+ max-width: 400px;
+ margin: auto;
+ box-shadow: $elevation-card;
+ padding: 30px;
+ background: white;
+ }
+
+ &__title {
+ font-size: 22px;
+ line-height: 32px;
+ font-weight: 500;
+ text-align: center;
+ margin: 8px 0 20px;
+ font-weight: bold;
+ }
+
+ &__input-container {
+ position: relative;
+ margin-top: 20px;
+ }
+
+ &__input {
+ border-radius: 100px;
+ border: 1px solid $color-secondary;
+ font-size: 14px;
+ line-height: 24px;
+ font-weight: 500;
+ height: 40px;
+ width: 100%;
+ outline: none;
+ padding: 0 15px;
+
+ &--error {
+ border-color: $error-color;
+ }
+ }
+
+ &__input-error-msg {
+ font-size: 12px;
+ margin-left: 15px;
+ color: $error-color;
+ }
+
+ &__input-icon {
+ position: absolute;
+ right: 10px;
+ top: 50%;
+ transform: translateY(-50%);
+ background: transparent;
+ border: none;
+ outline: none;
+ display: inline-block;
+ padding: 10px;
+ color: $color-secondary;
+ }
+
+ &__password-icon {
+ color: black;
+ cursor: pointer;
+ }
+
+ &__button {
+ border-radius: 100px;
+ font-size: 14px;
+ line-height: 24px;
+ background: black;
+ color: white;
+ padding: 0 10px;
+ height: 40px;
+ width: 100%;
+ border: none;
+ margin-top: 40px;
+ cursor: pointer;
+ padding: 0 20px;
+ outline: none;
+ }
+}
diff --git a/src/pages/Login/Login.tsx b/src/pages/Login/Login.tsx
new file mode 100644
index 0000000..0136004
--- /dev/null
+++ b/src/pages/Login/Login.tsx
@@ -0,0 +1,131 @@
+import { yupResolver } from '@hookform/resolvers/yup';
+import React, { useContext } from 'react';
+import { useForm } from 'react-hook-form';
+import { FaEnvelope, FaEye, FaEyeSlash } from 'react-icons/fa';
+import { Redirect, RouteComponentProps } from 'react-router-dom';
+import * as yup from 'yup';
+import UserContext from '../../hooks/UserContext';
+import paralinkApi from '../../services/interceptor';
+import './Login.scss';
+
+interface LoginProps {
+ from: string;
+}
+
+type LoginFormData = {
+ email: string;
+ password: string;
+};
+
+const loginSchema = yup.object().shape({
+ email: yup.string().email().required(),
+ password: yup.string().required(), // We could define rules here for password if there are
+});
+
+const Login = (props: RouteComponentProps<{}, {}, LoginProps>): JSX.Element => {
+ const userContext = useContext(UserContext);
+ const { register, handleSubmit, errors } = useForm({ resolver: yupResolver(loginSchema) });
+
+ const [state, setState] = React.useState({
+ // email: '',
+ // password: '',
+ seePassword: false,
+ });
+
+ const { from } = props.location.state || { from: { pathname: '/' } };
+
+ // const handleChange = (e: React.ChangeEvent): void => {
+ // const { id, value } = e.target;
+ // setState((prevState) => ({
+ // ...prevState,
+ // [id]: value,
+ // }));
+ // console.log('errors', errors);
+ // };
+
+ const togglePassword = (): void => {
+ setState((prevState) => ({
+ ...prevState,
+ seePassword: !prevState.seePassword,
+ }));
+ };
+
+ const login = (data: LoginFormData): void => {
+ // Avoid the page to refresh from submitting the form
+ // event.preventDefault();
+ // TODO: put like a loading icon on the login button
+
+ // TODO: do like a safe check on the email & password here
+
+ console.log('data', data);
+ // Just to check
+ paralinkApi
+ // For now this is going to be the placeholder
+ .get('https://jsonplaceholder.typicode.com/todos')
+ .then(() => {
+ // We should get the login token here from the BE
+ const token = 'faketokenhere';
+ userContext.login(token);
+ })
+ .catch((err: any) => {
+ // TODO: Need to display the error in the UI ( user not found, not correct password, not correct email)
+ console.error(err);
+ })
+ .finally(() => {
+ // TODO: login loading should be set to false here
+ });
+ };
+
+ if (userContext.isLoggedIn) {
+ return ;
+ }
+
+ // TODO: handle pressing enter to submit ( maybe having it as a form instead and using submit )
+ return (
+
+
Login into your account
+
+
+ );
+};
+
+export default Login;
diff --git a/src/pages/Login/index.tsx b/src/pages/Login/index.tsx
new file mode 100644
index 0000000..bf79bae
--- /dev/null
+++ b/src/pages/Login/index.tsx
@@ -0,0 +1 @@
+export { default as Login } from './Login';
diff --git a/src/services/interceptor/auth.ts b/src/services/interceptor/auth.ts
new file mode 100644
index 0000000..0cc1482
--- /dev/null
+++ b/src/services/interceptor/auth.ts
@@ -0,0 +1,32 @@
+import axios from 'axios';
+import { apis, storageNames } from '../../config';
+
+const paralinkApi = axios.create({
+ baseURL: apis.url,
+ timeout: apis.timeout,
+});
+
+paralinkApi.interceptors.request.use(
+ (config: any) => {
+ // Check for user token, we can't use hooks from here
+ const accessToken = localStorage.getItem(storageNames.user);
+
+ // If user not logged we just pass it through, the backend should not accept it
+ // This lets us do api requests like for logging in for example.
+ // Depending on the API we could potentially reject here before doing the call and add an exception for login path
+ if (!accessToken) {
+ return config;
+ }
+
+ // Otherwise set the token in the request
+ const request = config;
+ request.headers.authorization = `Bearer ${accessToken}`;
+ return request;
+ },
+ (error) => {
+ // User might have been rejected
+ return Promise.reject(error);
+ },
+);
+
+export default paralinkApi;
diff --git a/src/services/interceptor/index.ts b/src/services/interceptor/index.ts
new file mode 100644
index 0000000..46571d5
--- /dev/null
+++ b/src/services/interceptor/index.ts
@@ -0,0 +1,3 @@
+import paralinkApi from './auth';
+
+export default paralinkApi;
diff --git a/src/shared/ProtectedRoute/ProtectedRoute.tsx b/src/shared/ProtectedRoute/ProtectedRoute.tsx
new file mode 100644
index 0000000..b7d01f5
--- /dev/null
+++ b/src/shared/ProtectedRoute/ProtectedRoute.tsx
@@ -0,0 +1,28 @@
+import React, { useContext } from 'react';
+import { Redirect, Route } from 'react-router-dom';
+import UserContext from '../../hooks/UserContext';
+
+// Disable the eslint for next any
+// eslint-disable-next-line
+const ProtectedRoute = ({ component: Component, ...rest }: any): JSX.Element => {
+ const userContext = useContext(UserContext);
+ /** Verify that user is logged in otherwise redirect to login page */
+ return (
+
+ userContext.isLoggedIn ? (
+
+ ) : (
+
+ )
+ }
+ />
+ );
+};
+export default ProtectedRoute;
diff --git a/src/shared/ProtectedRoute/index.tsx b/src/shared/ProtectedRoute/index.tsx
new file mode 100644
index 0000000..4de35d0
--- /dev/null
+++ b/src/shared/ProtectedRoute/index.tsx
@@ -0,0 +1 @@
+export { default as ProtectedRoute } from './ProtectedRoute';