5. Component Pages

Signup page and Notes components

Create Signup Page

In the same way with the Login page, we can create a page for the registration.

For the registration, we need to collect the username and the email, and redirect to the Hatters to complete the registration.

Lets create the SignupPage.js in the Signup feature folder:

// src/features/Signup/SignupPage.js
import React, { useState } from 'react';
import { appConfig } from '../../app.config';
import { isEmail, isHatName } from '../../utils/validations';

function SignupPage() {
    const initUser = {
        username: '',
        email: '',
    };

    const [user, setUser] = useState(initUser);
    const [errorMsg, setErrorMsg] = useState('');

    const errorMessages = {
        usernameNotValid: 'Sorry, this username is not valid',
        emailNotRecognised: 'Sorry, email is not valid',
    };

    const navigateToHatters = () => {
        const redirectUrl = `http://${window.location.host}/authentication`;
        const applicationId = appConfig.applicationId;

        window.location.href = `https://hatters.dataswift.io/services/baas/signup?email=${user.email}&hat_name=${user.username}&application_id=${applicationId}&redirect_uri=${redirectUrl}&lang=en`;
    };

    const handleChange = event => {
        const name = event.target.name;
        const value = event.target.value;

        setUser({ ...user, [name]: value });
        setErrorMsg('');
    };

    const handleSubmit = event => {
        event.preventDefault();
        validateSignupDetails();
    };

    const validateSignupDetails = () => {
        let errorMsg = '';

        if (!isHatName(user.username)) {
            errorMsg = errorMessages.usernameNotValid;
        } else if (!isEmail(user.email)) {
            errorMsg = errorMessages.emailNotRecognised;
        } else {
            navigateToHatters();
        }

        if (errorMsg) {
            setErrorMsg(errorMsg);
        }
    };

    return (
        <form onSubmit={e => handleSubmit(e)} className={'flex-column-wrapper flex-content-center flex-align-items-center'}>
            <div className={'flex-spacer-small'} />

            <h3>Create Account</h3>
            <div className={'flex-spacer-small'} />

            <input
                className={` ${errorMsg ? 'input-error-field' : null}`}
                name={'username'}
                type={'text'}
                placeholder="Username"
                autoComplete={'username'}
                value={user.username}
                onChange={e => handleChange(e)}
            />
            <input
                className={` ${errorMsg ? 'input-error-field' : null}`}
                name={'email'}
                type={'text'}
                placeholder="Email"
                autoComplete={'email'}
                value={user.email}
                onChange={e => handleChange(e)}
            />
            {errorMsg && <div className={'input-error-label'}>{errorMsg}</div>}

            <div className={'flex-spacer-large'} />

            <button className={'btn btn-accent'} type={'submit'}>
                Next
            </button>

            <div className={'flex-spacer-small'} />
        </form>
    );
}

export default SignupPage;

In the validations.js file, include a function to check if the email is valid:

// src/utils/validations.js
const EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@(([[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;

export const isEmail = email => {
  return EMAIL_REGEX.test(email);
};

Include the "/signup" route in the Routing.js:

// src/app/Routing.js
function Routing() {
    return (
        <Router>
            <Header />
            <Switch>
                <Route exact path="/" component={HomePage} />
                <Route path="/login" component={LoginPage} />
                <Route path="/signup" component={SignupPage} />
                <Route path="/authentication" component={AuthenticationHandler} />
            </Switch>
        </Router>
    );
}

export default Routing;

Create a private Route for the authenticated username

In this case, we need to create a "global" state to keep information for the current authenticated user. With the Context we can create a simple state share across all the components.

We'll need to keep information such as a flag to check if the user is authenticated, the JWT token, and the username.

state = {
    user: {
      isAuthenticated: false,
      token: '',
      hatName: '',
    },
  }

Firstly, we have to create the Context object. Create the AuthContext.js file in the context folder under the components/ folder:

// src/components/context/AuthContext.js
import React from 'react';

const AuthContext = React.createContext();

export default AuthContext;

Secondly, create the Context Provider with state and helper function to log in/out. Create the AuthProvider.js:

// src/components/context/AuthProvider.js
import React, { Component } from 'react';
import AuthContext from './AuthContext';
import { HatTokenValidation } from '@dataswift/hat-js/lib/utils/HatTokenValidation';

export class AuthProvider extends Component {
  state = {
    user: {
      isAuthenticated: false,
      token: '',
      hatName: '',
    },
  };

  render() {
    return (
      <AuthContext.Provider
        value={{
          user: this.state.user,
          logout: () => {
            sessionStorage.removeItem('token');

            this.setState({
              user: {
                isAuthenticated: false,
                token: '',
                hatName: '',
              },
            });
          },
          login: (token, hatName) => {
            if (token && !HatTokenValidation.isEncodedTokenExpired(token)) {
              this.setState({
                user: {
                  isAuthenticated: true,
                  token: token,
                  hatName: hatName,
                },
              });
            }
          },
        }}
      >
        {this.props.children}
      </AuthContext.Provider>
    );
  }
}

Include AuthProvider in the App component

// src/app/App.js
import { AuthProvider } from "../components/context/AuthProvider";

function App() {
  return (
      <AuthProvider>
        <div className="app">
          <Routing/>
        </div>
      </AuthProvider>
  );
}

Check if the user is authenticated

Now, in the Home page, we can check if the user is authenticated and display a protected page. In order to see this private page, they must login first.

HAT-JS offers the option to import the HatTokenValidation class to help decode the token, accessing the JWT's properties and check the token expiration time:

// src/features/Home/HomePage.js
import React, { useEffect, useContext } from 'react';
import './HomePage.scss';
import { HatTokenValidation } from '@dataswift/hat-js/lib/utils/HatTokenValidation';
import AuthContext from "../../components/context/AuthContext";
function HomePage() {
  const authContext = useContext(AuthContext);

  useEffect(() => {
    const token = sessionStorage.getItem('token');

    if (token) {
      const decodedToken = HatTokenValidation.decodeToken(token);

      // If the token has not expired, log in the user.
      if (!HatTokenValidation.isExpired(decodedToken)) {
        authContext.login(token, decodedToken.iss);
      }
    }
  }, []);

  return (
    <AuthContext.Consumer>
      {context => (
        <>
          <div className="home-wrapper">{context.user.isAuthenticated && <div>Authenticated!</div>}</div>
        </>
      )}
    </AuthContext.Consumer>
  );
}

export default HomePage;

Add a logout button

In the Header.js we can use the AuthContext and display the "Logout" button when the user is authenticated:

// src/components/header/Header.js
import React from 'react';
import { Link, NavLink } from 'react-router-dom';
import './Header.scss';
import AuthContext from '../context/AuthContext';

function Header() {
    return (
        <AuthContext.Consumer>
            {context => (
                <>
                    <header className="header">
                        <NavLink exact activeClassName="active" to="/">
                            Home
                        </NavLink>

                        {context.user.isAuthenticated && (
                            <Link to="/login" onClick={() => context.logout()}>
                                Log out
                            </Link>
                        )}

                        {!context.user.isAuthenticated && (
                            <ul>
                                <li>
                                    <NavLink activeClassName="active" to="/login">
                                        Log in
                                    </NavLink>
                                </li>
                                <li>
                                    <NavLink activeClassName="active" to="/signup">
                                        Create account
                                    </NavLink>
                                </li>
                            </ul>
                        )}
                    </header>
                </>
            )}
        </AuthContext.Consumer>
    );
}
export default Header;

Create a protected Home component

The next step is to create the protected Home component. Lets create the HomePrivate.js file into the Home feature:

// src/features/Home/HomePrivate.js
import React from 'react';

function HomePrivate() {
    return (
        <div className="home-private">
            <div className="container">
                <div className="content">
                    Private content
                </div>
            </div>
        </div>
    );
}

export default HomePrivate;

And include this CSS in the HomePage.scss:

// src/features/Home/HomePage.scss
.
.

.home-private {
    height: 100%;
    width: 100%;

    .container {
        border-radius: 6px;
        padding: 2.4rem;
    }

    .content {
        background: #e9e9e9;
        padding: 2.4rem;
        border-radius: 6px;
    }
}

And then, replace the \Authenticated!\</div> with the component:

// src/features/Home/HomePage.js
.
.
import HomePrivate from "./HomePrivate";

function HomePage() {
  .
  .

  return (
    <AuthContext.Consumer>
      {context => (
        <>
          <div className="home-wrapper">{context.user.isAuthenticated && <HomePrivate />}</div>
        </>
      )}
    </AuthContext.Consumer>
  );
}

export default HomePage;

Create Notes component

Create a new folder Notes in the features/ with the Notes.js file:

// src/features/Notes/Notes.js
import React from 'react';
import './Notes.scss';

function Notes() {

    return (
        <form
            className={'notes-wrapper flex-column-wrapper flex-content-center flex-align-items-center'}
        >
            <div className={'flex-spacer-small'} />
            <h3>Save a note on your HAT</h3>
            <input
                name={'note'}
                type={'text'}
                placeholder="Remember to..."
                autoComplete={'text'}
            />
            <div className={'flex-spacer-small'} />
            <button className={'btn btn-accent'} type={'submit'}>
                Save
            </button>
            <div className={'flex-spacer-small'} />
        </form>
    );
}

export default Notes;

And the Notes.scss:

// src/features/Notes/Notes.scss
.notes-wrapper {
  input {
    margin-top: 2.5rem;
    margin-bottom: 2.5rem;
  }
}

.note-row-wrapper {
  display: flex;
  flex-direction: row;
  margin-top: 1rem;

  .note-content {
    text-align: left;
    flex: 1 1;
    color: #4a556b;
    font-size: 1.6rem;
  }

  button {
    margin-left: 1rem;
  }
}

.notes-list {
  margin-top: 2.5rem;
  width: 100%;
  max-width: 32rem;
  list-style-type: none;
}

Include the Notes component in the HomePrivate.js:

// src/features/Home/HomePrivate.js
import React from 'react';
import Notes from "../Notes/Notes";

function HomePrivate() {
    return (
        <div className="home-private">
            <div className="container">
                <div className="content">
                    <Notes />
                </div>
            </div>
        </div>
    );
}

export default HomePrivate;

Last updated