mern architecture

components

1 Set Up Folder Structure

💪 Practice Exercise Breakout Rooms - Delete Files We don't nee App.jsx (30 minutes)

  1. Make sure that you delete any files that you see that we don't need in the SRC Folder
  2. Make a styles.css file and remove the file that is no longer needed (i expect you to figure out which file you don't need, you know how we set up our react apps)
  3. Import the styles.css where you think it makes most logical sense the root of your app that imports everything else (what is the root of everything?)
  4. Move the App.js to the pages folder and make a App folder in pages and place the App.js in that folder
  5. Update the imports in your App.js and index.js. The current imports will not work because you have now moved folders and changed file names.
  6. We will be learning how to make SCSS Modules which makes special scoped css that only applies to the component that you attach the SCSS Module to.
  7. run npm i sass to install sass, so that we can use SCSS Sassy Cascading Style Sheets yes S A S S is the correct spelling for the npm module
  8. Create the following folder structure

ROOT

  1. config/

    • database.js
  2. controllers/

    • api/

      • fruits.js
      • users.js
  3. models/

    • fruit.js
    • user.js
  4. public/ (don't change anything it's all there already)

    • favicon.ico
    • index.html
    • logo*.png
    • manifest.json
    • robots.txt
  5. routes/

    • api/

      • fruits.js
      • users.js
  6. src/

    • components/

      • NavBar/

        • NavBar.js
        • NavBar.module.scss
    • pages/

      • App/

        • App.js
        • App.module.css
      • FruitsPage/

        • FruitsPage.js
        • FruitsPage.module.scss
      • AuthPage/

        • AuthPage.js
        • AuthPage.module.scss
      • NewOrderPage/

        • NewOrderPage.js
        • NewOrderPage.module.scss
      • OrderHistoryPage/

        • OrderHistoryPage.js
        • OrderHistoryPage.module.scss
    • utilities/

      • .gitkeep
    • styles.css
    • index.js
  7. .gitignore
  8. README.md
  9. crud-helper.js
  10. seed.js
  11. server.js
  12. .env
  13. .env-example

The CSS for the SEI CAFE app is not trivial, so instead of adding CSS to styles.css in bits and pieces, let's go ahead and add all the general purpose CSS up front:

/* CSS Custom Properties */
:root {
  --white: #FFFFFF;
  --tan-1: #FBF9F6;
  --tan-2: #E7E2DD;
  --tan-3: #E2D9D1;
  --tan-4: #D3C1AE;
  --orange: #F67F00;
  --text-light: #968c84;
  --text-dark: #615954;
}

*, *:before, *:after {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
  'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
  sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  background-color: var(--tan-4);
  padding: 2vmin;
  height: 100vh;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}

#root {
  height: 100%;
}

.align-ctr {
  text-align: center;
}

.align-rt {
  text-align: right;
}

.smaller {
  font-size: smaller;
}

.flex-ctr-ctr {
  display: flex;
  justify-content: center;
  align-items: center;
}

.flex-col {
  flex-direction: column;
}

.flex-j-end {
  justify-content: flex-end;
}

.scroll-y {
  overflow-y: scroll;
}

.section-heading {
  display: flex;
  justify-content: space-around;
  align-items: center;
  background-color: var(--tan-1);
  color: var(--text-dark);
  border: .1vmin solid var(--tan-3);
  border-radius: 1vmin;
  padding: .6vmin;
  text-align: center;
  font-size: 2vmin;
}

.form-container {
  padding: 3vmin;
  background-color: var(--tan-1);
  border: .1vmin solid var(--tan-3);
  border-radius: 1vmin;
}

p.error-message {
  color: var(--orange);
  text-align: center;
}

form {
  display: grid;
  grid-template-columns: 1fr 3fr;
  gap: 1.25vmin;
  color: var(--text-light);
}

label {
  font-size: 2vmin;
  display: flex;
  align-items: center;
}

input {
  padding: 1vmin;
  font-size: 2vmin;
  border: .1vmin solid var(--tan-3);
  border-radius: .5vmin;
  color: var(--text-dark);
  background-image: none !important; /* prevent lastpass */
  outline: none;
}

input:focus {
  border-color: var(--orange);
}

button, a.button {
  margin: 1vmin;
  padding: 1vmin;
  color: var(--white);
  background-color: var(--orange);
  font-size: 2vmin;
  font-weight: bold;
  text-decoration: none;
  text-align: center;
  border: .1vmin solid var(--tan-2);
  border-radius: .5vmin;
  outline: none;
  cursor: pointer;
}

button.btn-sm {
  font-size: 1.5vmin;
  padding: .6vmin .8vmin;
}

button.btn-xs {
  font-size: 1vmin;
  padding: .4vmin .5vmin;
}

button:disabled, form:invalid button[type="submit"] {
  cursor: not-allowed;
  background-color: var(--tan-4);
}

button[type="submit"] {
  grid-column: span 2;
  margin: 1vmin 0 0;
}

That's better! But rest assured we'll continue to improve the layout and styling as we continue coding out mern-infrastructure/SEI CAFE.


2 Create Sign-Up Form

2. Code the <SignUpForm> Component as a Class Component

We'll be adding a <SignUpForm> component that will be rendered by <AuthPage>.

In the new <SignUpForm> component, as you saw in the React Fundamentals - Handling Input and Events lesson, we'll need to manage state for the controlled inputs used to gather information from the user that wants to sign up.

However, instead of using a function component, we'll define <SignUpForm> as a class component which used to be the go to for defining components that needed to manage state prior to hooks being added to the React library.

💪 Practice Exercise - Set Up the Module for the <SignUpForm> Component (1 minute)

The naming conventions of the folders and files for class components is no different than that of function components.

  • Create the folder and module for <SignUpForm> following the same conventions and best practices as always.

Hint: Be careful of typos (SignUpForm vs. SignupForm).

Set Up <SignUpForm> as a Class Component

Class components in React typically inherit from the Component class defined in the React library, let's import it:

// SignUpForm.jsx

import { Component } from 'react'

Check out the Further Study section to learn more about the PureComponent class available to inherit from as well.

This is how we Set up a class component:

export default class SignUpForm extends Component {

  render() {
    return (
      <div>
        SignUpForm
      </div>
    );
  }
}

A class component must implement a render method responsible for returning the component's JSX - making the render method itself very much like a function component.

Importing and Rendering a Class Component

There's no difference in how we import and render function and class components.

Let's import and render <SignUpForm> within AuthPage.jsx and do a minor refactor while we're at it:

// AuthPage.jsx

import SignUpForm from '../../components/SignUpForm/SignUpForm';

export default function AuthPage() {
  return (
    <main>
      <h1>AuthPage</h1>
      <SignUpForm />
    </main>
  )
}

So, as you can see, we can't even tell if a component is defined as a function or class when we import and render it:

Initializing a Class Component's State

Our <SignUpForm> is going to need the following state:

  • name: Name of the user
  • email: The email address of the user
  • password: The user's password
  • confirm: Used to confirm the password is entered correctly
  • error: Used to display an error message if the sign up fails

Unlike with a function component that can define multiple pieces of state by using the useState hook multiple times, a class component's state is always a single object assigned to a state property on the instance of the component.

There are two ways to initialize the state property:

  • Using the constructor method (the original approach).
  • Using the newer class fields approach.

Of course, we'll use the class fields approach that all the cool kids are using, but here's how it would look if we initialized the state using the constructor method:

export default class SignUpForm extends Component {
  // state is always an object with a property for each "piece" of state
  constructor() {
    this.state = {
      name: '',
      email: '',
      password: '',
      confirm: '',
      error: ''
    };
  }

The following is the class field approach we're going with:

export default class SignUpForm extends Component {
  state = {
    name: '',
    email: '',
    password: '',
    confirm: '',
    error: ''
  };

FYI: Internally, the class field syntax is converted into the constructor method approach.

this Keyword in a Class Component

When we use class components, it's important to realize that the components themselves are instances of the class instantiated by React the first time they are rendered.

❓ An instance of a class is an _______.

object

❓ An object's methods accesses other properties/methods of the object via the _______ keyword.

this

Unlike with function components, a class component accesses its props and methods using this, for example:

  • this.props: Accesses the class component's props object, e.g., this.props.someProp.
  • this.state: Accesses the class component's state object, e.g., this.state.email.
  • this.someMethod(): Invokes a method defined in the class component or inherited by the superclass (Component) such as this.setState() used to update state.

The Complete render Method of <SignUpForm>

Here's the completed code of the render() method that we'll copy/paste and discuss:

render() {
  const disable = this.state.password !== this.state.confirm;
  return (
    <div>
      <div className="form-container">
        <form autoComplete="off" onSubmit={this.handleSubmit}>
          <label>Name</label>
          <input type="text" name="name" value={this.state.name} onChange={this.handleChange} required />
          <label>Email</label>
          <input type="email" name="email" value={this.state.email} onChange={this.handleChange} required />
          <label>Password</label>
          <input type="password" name="password" value={this.state.password} onChange={this.handleChange} required />
          <label>Confirm</label>
          <input type="password" name="confirm" value={this.state.confirm} onChange={this.handleChange} required />
          <button type="submit" disabled={disable}>SIGN UP</button>
        </form>
      </div>
      <p className="error-message">&nbsp;{this.state.error}</p>
    </div>
  );
}

The form is rendering but it's ugly and we can't type in the inputs yet because the this.handleChange method assigned to the onChange prop is not defined yet...

Defining Event Handler Methods in a Class Component

The handleChange() method can't be defined using the usual syntax for defining an instance method of a class like that of the render() method.

The reason the usual syntax won't work is because the method will be invoked as a callback and thus will not have this bound to the component instance as necessary if we want to be able to access this.props, this.setState(), etc.

There are two solutions to ensure that a method has this correctly set to the component instance:

  • Define the method as usual and use JavaScript's bind method in the constructor method to explicitly set the value of this.
  • Use the class field syntax along with an arrow function when defining the method which by its very nature fixes the issue due to the way class fields are actually initialized in the constructor method.

FYI: The trouble with the binding of this in class components is definitely one of the main inspirations for React hooks!

So, here's how we use class field syntax to properly define methods used to handle events in class components:

// The object passed to setState is merged with the current state object
handleChange = (evt) => {
  this.setState({
    [evt.target.name]: evt.target.value,
    error: ''
  });
};

render() {
  ...
}

The controlled inputs should now update their appropriate state!

Even the cool little line of code
const disable = this.state.password !== this.state.confirm;
combined with the disabled prop on the submit button should be working!

That code is pretty much what we learned in the React Fundamentals - Handling Input and Events lesson except...

Updating State in a Class Component

There's two key differences in how state is updated in class vs. function components:

  • Class components always update state by invoking the inherited setState() method vs. invoking the setter function(s) returned by the useState hook in function components.
  • setState() accepts an object as an arg and this object is merged into the existing state object. This differs with how a function component's setter function replaces that state with the value provided.

    Note: setState also has a couple of other signatures - refer to the docs for more info.

Handling the onSubmit Event

The <form> React Element in <SignUpForm> already has an event handler method assigned to its onSubmit prop.

💪 Practice Exercise - Set Up the handleSubmit() Method (2 minutes)

  • Using the same class field syntax used when defining handleChange(), define a method named handleSubmit() above the render() method.
  • As we learned during the React Fundamentals - Handling Input and Events lesson, we need to prevent the form from being submitted to the server by including evt.preventDefault(); as the first line of code.
  • Baby step by adding this additional line of code alert(JSON.stringify(this.state));.

Check it out by typing in some info and submitting the form:

Final Sign Up Code

import { Component } from 'react';

export default class SignUpForm extends Component {
    state = {
        name: '',
        email: '',
        password: '',
        confirm: '',
        error: ''
    }

    handleChange = (evt) => {
        this.setState({...this.state, [evt.target.name]: evt.target.value, error: '' })
    }

    handleSubmit = (evt) => {
        evt.preventDefault();
        alert(JSON.stringify(this.state))
    }


    render() {
        const disable = this.state.password !== this.state.confirm;
        return (
          <div>
            <div className="form-container">
              <form autoComplete="off" onSubmit={this.handleSubmit}>
                <label>Name</label>
                <input type="text" name="name" value={this.state.name} onChange={this.handleChange} required />
                <label>Email</label>
                <input type="email" name="email" value={this.state.email} onChange={this.handleChange} required />
                <label>Password</label>
                <input type="password" name="password" value={this.state.password} onChange={this.handleChange} required />
                <label>Confirm</label>
                <input type="password" name="confirm" value={this.state.confirm} onChange={this.handleChange} required />
                <button type="submit" disabled={disable}>SIGN UP</button>
              </form>
            </div>
            <p className="error-message">&nbsp;{this.state.error}</p>
          </div>
        )
      }
}

3. Set up Backend For JWT & User Sign Up

  1. Make the Model
  2. Make the Controller
  3. Add Config Files
  4. Make the Router
  5. Add the Router to to the Server
  6. Add the JWT & BCRYPT packages
  7. Set up login & create functions to use JWT
  8. Create the configuration files
  9. Make sure everything works

User Model setup

  • install bcrypt npm i bcrypt
const { Schema, model } = require('mongoose')
const bcrypt = require('bcrypt')

const SALT_ROUNDS = 6

const userSchema = new Schema({
  name: {type: String, required: true},
  email: {
    type: String,
    unique: true,
    trim: true,
    lowercase: true,
    required: true
  },
  password: {
    type: String,
    trim: true,
    minLength: 3,
    required: true
  }
}, {
  timestamps: true,
  toJSON: {
    transform (doc, ret) {
      delete ret.password
      return ret
    }
  }
})

userSchema.pre('save', async function (next) {
  // 'this' is the user doc
  if (!this.isModified('password')) return next()
  // update the password with the computed hash
  this.password = await bcrypt.hash(this.password, SALT_ROUNDS)
  return next()
})

module.exports = mongoose.model('User', userSchema)

Add Controllers

  • install jsonwebtoken npm i jsonwebtoken
// /controllers/api/users.js

const User = require('../../models/user')
const jwt = require('jsonwebtoken')
const bcrypt = require('bcrypt')

const checkToken = (req, res) => {
  console.log('req.user', req.user)
  res.json(req.exp)
}

const dataController = {
  async create (req, res, next) {
    try {
      const user = await User.create(req.body)
      // token will be a string
      const token = createJWT(user)
      // send back the token as a string
      // which we need to account for
      // in the client
      res.locals.data.user = user
      res.locals.data.token = token
      next()
    } catch (e) {
      res.status(400).json(e)
    }
  },
  async login (req, res, next) {
    try {
      const user = await User.findOne({ email: req.body.email })
      if (!user) throw new Error()
      const match = await bcrypt.compare(req.body.password, user.password)
      if (!match) throw new Error()
      res.locals.data.user = user
      res.locals.data.token = createJWT(user)
      next()
    } catch {
      res.status(400).json('Bad Credentials')
    }
  }
}

const apiController = {
  auth (req, res) {
    res.json(res.locals.data.token)
  }
}

module.exports = {
  checkToken,
  dataController,
  apiController
}

/* -- Helper Functions -- */

function createJWT (user) {
  return jwt.sign(
    // data payload
    { user },
    process.env.SECRET,
    { expiresIn: '24h' }
  )
}

Add Config Folder Code

You should already have database.js

// /config/database.js
const mongoose = require('mongoose')

mongoose.connect(process.env.MONGO_URI)

const db = mongoose.connection

db.on('connected', function () {
  console.log(`Connected to ${db.name} at ${db.host}:${db.port}`)
})
module.exports = mongoose

Now lets create the checkToken Middleware and add it to server.js as well as make a route the frontend can call to check the token

As we learned many moons ago, middleware is used to process requests in an Express app.

Yay! Another opportunity to write a custom middleware function that:

  1. Checks if there's a token sent in an Authorization header of the HTTP request. For additional flexibility, we'll also check for a token being sent as a query string parameter.
  2. Verifies the token is valid and hasn't expired.
  3. Decodes the token to obtain the user data from its payload.
  4. Then finally, adds the user payload to the Express request object.
// /config/checkToken.js
const jwt = require('jsonwebtoken')

module.exports = (req, res, next) => {
  // Check for the token being sent in a header or a query parameter
  let token = req.get('Authorization') || req.query.token
  if (token) {
    token = token.replace('Bearer ', '')
    // Check if token is valid and not expired
    jwt.verify(token, process.env.SECRET, function (err, decoded) {
      req.user = err ? null : decoded.user
      // Can remove this...
      // If your app doesn't care
      req.exp = err ? null : new Date(decoded.exp * 1000)
    })
    return next()
  } else {
    // No token was sent
    req.user = null
    return next()
  }
}
// /config/ensureLoggedIn.js
module.exports = (req, res, next) => {
  if (!req.user) return res.status(401).json('Unauthorized')
  next()
};

Add Router

Routes API

// /routes/api/users.js
const express = require('express')
const router = express.Router()
const { checkToken, dataController, apiController } = require('../../controllers/api/users')
const ensureLoggedIn = require('../../config/ensureLoggedIn')

// POST /api/users
router.post('/', dataController.create, apiController.auth)
// POST /api/users/login
router.post('/login', dataController.login, apiController.auth)

// GET /api/users/check-token
router.get('/check-token', ensureLoggedIn, checkToken)

module.exports = router

Update Server.js

// ./server.js
require('dotenv').config()
require('./config/database')
const express = require('express')
const path = require('path')
const favicon = require('serve-favicon')
const logger = require('morgan')
const PORT = process.env.PORT || 3001

const app = express()

app.use(logger('dev'))
// there's no need to mount express.urlencoded middleware
// why is that?
app.use(express.json())
// Configure both serve-favicon & static middleware
// to serve from the production 'build' folder
app.use(favicon(path.join(__dirname, 'build', 'favicon.ico')))
app.use(express.static(path.join(__dirname, 'build')))

// Check if token and create req.user
app.use(require('./config/checkToken'))

// Put API routes here, before the "catch all" route
app.use('/api/users', require('./routes/api/users'))
app.use('/api/fruits', require('./routes/api/fruits'))

// The following "catch all" route (note the *) is necessary
// to return the index.html on all non-AJAX requests
app.get('*', function (req, res) {
  res.sendFile(path.join(__dirname, 'build', 'index.html'))
})

// Configure to use port 3001 instead of 3000 during
// development to avoid collision with React's dev server

app.listen(PORT, function () {
  console.log(`Express app running on port ${PORT}`)
})

4 Setup Frontend API Services and Utilities

  1. Make functions to extract the user from the JWT
  2. Make functions to handle login and signup
  3. Connect the signup form
  4. Setup the login form
  5. Swap between Login & Signup
  6. Make the NavBar React to logged in state of the user

JWT Breakdown

  • The token can contain whatever custom data (called claims) we want to put in it.
  • The token is cryptographically signed by the server when it is created so that if the token is changed in any way, it is considered invalid.
  • The token is encoded, but not encrypted. It is encoded (converted) using a standard known as base64url encoding so that it can be serialized across the internet or even be included in a URL's querystring. It may seem that encoded data is "secret" - it's not as you'll soon see!
> const jwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ';
> const payload = jwt.split('.')[1]  // only interested in the payload (claims)
> atob(payload)
< "{"sub":"1234567890","name":"John Doe","admin":true}"

The atob() method decodes a base-64 encoded string and btoa() base-64 encodes data.

Because the data in a JWT can be easily read, it's important not to include sensitive/secret data such as Social Security Numbers, etc.

Okay, JWT-based auth is cool, let's see how we use them in a SPA...

Typical Token-Based Flow in a SPA

The following depicts the typical flow of JWT-based auth in a SPA:

Additional clarification on the above steps:

  • STEP 1: Applies to logging in and signing up.
  • STEP 2: The JWT is created only after the login credentials have been validated, or the visitor signing up has been saved to the database.
  • STEP 3: After the JWT has been received by the client, it needs to be persisted, usually in local storage, so that it can be sent in future requests as needed (STEP 4).
  • STEP 4: We will be including the JWT with any request that needs to be authenticated on the server.
  • STEP 5: We will write a tidy middleware function used to validate the token and add the user data to Express's req object - cool beans for sure!
  • STEP 6: Send a response positive or negative back to the client

Final Front End Code

send-request

Note: The sendRequest function always returns a promise and we are passing that promise to the caller of checkToken.

Each of these utilities work together to make our life easier and reference eachother

import { getToken } from './users-service'

export default async function sendRequest (url, method = 'GET', payload = null) {
  // Fetch takes an optional options object as the 2nd argument
  // used to include a data payload, set headers, etc.
  const options = { method }
  if (payload) {
    options.headers = { 'Content-Type': 'application/json' }
    options.body = JSON.stringify(payload)
  }
  const token = getToken()
  if (token) {
    // Ensure headers object exists
    options.headers = options.headers || {}
    // Add token to an Authorization header
    // Prefacing with 'Bearer' is recommended in the HTTP specification
    options.headers.Authorization = `Bearer ${token}`
  }
  const res = await fetch(url, options)
  // res.ok will be false if the status code set to 4xx in the controller action
  if (res.ok) return res.json()
  throw new Error('Bad Request')
}

users-service.js

import * as usersAPI from './users-api'

export async function signUp (userData) {
  // Delete the network request code to the
  // users-api.js module which will ultimately
  // return the JWT
  const token = await usersAPI.signUp(userData)
  // Persist the token to localStorage
  window.localStorage.setItem('token', token)
  return getUser()
}

export async function login (credentials) {
  const token = await usersAPI.login(credentials)
  // Persist the token to window.localStorage
  window.localStorage.setItem('token', token)
  return getUser()
}

export function getToken () {
  const token = window.localStorage.getItem('token')
  // getItem will return null if no key
  if (!token) return null
  const payload = JSON.parse(atob(token.split('.')[1]))
  // A JWT's expiration is expressed in seconds, not miliseconds
  if (payload.exp < Date.now() / 1000) {
    // Token has expired
    window.localStorage.removeItem('token')
    return null
  }
  return token
}

export function getUser () {
  const token = getToken()
  return token ? JSON.parse(atob(token.split('.')[1])).user : null
}

export function logOut () {
  window.localStorage.removeItem('token')
}

users-api.js

import sendRequest from './send-request'

const BASE_URL = '/api/users'

export function signUp(userData) {
  return sendRequest(BASE_URL, 'POST', userData)
}

export function login(credentials) {
  return sendRequest(`${BASE_URL}/login`, 'POST', credentials)
}

Sign Up

import { Component } from 'react'
import { signUp } from '../../utilities/users-service'

export default class SignUpForm extends Component {
  state = {
    name: '',
    email: '',
    password: '',
    confirm: '',
    error: ''
  }

  handleChange = (evt) => {
    this.setState({ ...this.state, [evt.target.name]: evt.target.value, error: '' })
  }

  handleSubmit = async (evt) => {
    evt.preventDefault()
    try {
      const formData = { ...this.state }
      delete formData.error
      delete formData.confirm
      const user = await signUp(formData)
      this.props.setUser(user)
    } catch (error) {
      this.setState({ error: 'Sign Up Failed' })
    }
  }

  render () {
    const disable = this.state.password !== this.state.confirm
    return (
      <div>
        <div className='form-container'>
          <form autoComplete='off' onSubmit={this.handleSubmit}>
            <label>Name</label>
            <input type='text' name='name' value={this.state.name} onChange={this.handleChange} required />
            <label>Email</label>
            <input type='email' name='email' value={this.state.email} onChange={this.handleChange} required />
            <label>Password</label>
            <input type='password' name='password' value={this.state.password} onChange={this.handleChange} required />
            <label>Confirm</label>
            <input type='password' name='confirm' value={this.state.confirm} onChange={this.handleChange} required />
            <button type='submit' disabled={disable}>SIGN UP</button>
          </form>
        </div>
        <h1 className='error-message'>&nbsp;{this.state.error}</h1>
      </div>
    )
  }
}

Login

import { useState } from 'react'
import * as userService from '../../utilities/users-service'

export default function LoginForm ({ setUser }) {
  const [credentials, setCredentials] = useState({
    email: '',
    password: ''
  })
  const [error, setError] = useState('')

  const handleChange = (evt) => {
    setCredentials({ ...credentials, [evt.target.name]: evt.target.value })
    setError('')
  }

  const handleSubmit = async (evt) => {
    evt.preventDefault()
    try {
      const user = await userService.login(credentials)
      setUser(user)
    } catch (error) {
      setError(error.message)
    }
  }

  return (
    <div>
      <div className='form-container'>
        <form autoComplete='off' onSubmit={handleSubmit}>
          <label>Email</label>
          <input type='email' name='email' value={credentials.email} onChange={handleChange} required />
          <label>Password</label>
          <input type='password' name='password' value={credentials.password} onChange={handleChange} required />
          <button type='submit'>LOG IN</button>
        </form>
      </div>
      <h1 className='error-message'>&nbsp;{error}</h1>
    </div>
  )
}