DEV Community

Cover image for Let's Build A Currency Exchange Part II
Marlon Decosta
Marlon Decosta

Posted on • Updated on

Let's Build A Currency Exchange Part II

In this half of the tutorial we'll focus on the frontend. The code for this project is on my GitHub. You can find the first half of this article here. We'll store the code for our frontend in a folder named client. Create client at the root level, cd into this folder and run the following command in the terminal:

npx create-react-app .
Enter fullscreen mode Exit fullscreen mode

We use npx so that we don't have to install create-react-app globally. Run the following command in your terminal and let's get our dependencies:

npm i @apollo/react-hooks apollo-cache-inmemory apollo-client apollo-link-http graphql-tag react-chartjs-2 chart.js react-router-dom
Enter fullscreen mode Exit fullscreen mode

With our dependencies in tow, let's do a little spring cleaning. Delete logo.svg, serviceWorker.js, App.test.js and App.css. Now remove their imports (and all those weird semicolons galavanting about) from index.js and App.js. Afterwards, adjust index.js such that it resembles the below code:

// index.js

import React from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter } from 'react-router-dom'
import { ApolloClient } from 'apollo-client'
import { ApolloProvider } from '@apollo/react-hooks'
import { InMemoryCache } from 'apollo-cache-inmemory' 
import { HttpLink } from 'apollo-link-http'

import App from './App'
import './index.css'

const cache = new InMemoryCache() 
const client = new ApolloClient({
  cache,
  link: new HttpLink({
    uri: 'http://localhost:4000/graphql',
    credentials: 'include' 
  })
})

ReactDOM.render(
  <ApolloProvider client={client}>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </ApolloProvider>, document.getElementById('root')
)
Enter fullscreen mode Exit fullscreen mode

First, we handle our imports. Then we create a new instance of InMemoryCache and add it to our ApolloClient configuration Object. We use HttpLink to hit our GraphQL API and just as we did in the GraphQL Playground, we add credentials: 'include' to ensure that our cookie is sent along with every request.

Inside of our render function we wrap everything with React Router's BrowserRouter. react-router describes BrowserRouter as, "A router that uses the HTML5 history API to keep your UI in sync with the URL."

We pass ApolloProvider our new instance of ApolloClient so that later we can consume it (akin to the React Context API). As I write this @apollo/react-hooks is a nice ripe age of one day old. This is a minified version of react-apollo which doesn't offer render prop functionality, but reduces bundle size by 50%!

Open up App.js and add the following code:

// App.js

import React from 'react'
import { Route } from 'react-router-dom'

import Landing from './pages/Landing'

const App = () => <Route exact path='/' component={ Landing } />

export default App
Enter fullscreen mode Exit fullscreen mode

React Router's Route component allows us to define a routes path, and assign said path a component to be rendered. In our case this component is Landing. Create a pages folder inside of the src folder. Inside pages create a new file and name it Landing.js. Insert the following code:

// Landing.js

import React from 'react'

const Landing = () => <div>Hello world!</div>

export default Landing
Enter fullscreen mode Exit fullscreen mode

Once more, we demonstrate our respect for tradition and muster our most majestic, 'Hello world' yet! Nothing quite tucks me in like a well-groomed, "Hello world!"

Inside the src folder, create another folder and name it graphql. Inside of this folder create two subfolders: mutations and queries. Inside of queries create a new file and name it currencyPairInfo.js.

Add the following code:

// currencyPairInfo.js

import gql from 'graphql-tag'

export const CURRENCY_PAIR_INFO = gql`
  query CurrencyPairInfo($fc: String, $tc: String) {
    currencyPairInfo(tc: $tc, fc: $fc) {
      fromCurrency 
      fromCurrencyName
      toCurrency
      toCurrencyName
      exchangeRate
      lastRefreshed
      timeZone
      bidPrice
      askPrice
    }
  }
`
Enter fullscreen mode Exit fullscreen mode

First, we import gql from graphql-tag so that we can define our mutations and queries. Inside of this file we're doing the exact same thing we did in the GraphQL Playground, except we add an additional name (CurrencyPairInfo) to our query and further describe the shape of our schema. Finally, we store this query in the constant CURRENCY_PAIR_INFO.

Now that we have our query, let's return to Landing.js and use it.

// Landing.js

import React, { useState } from 'react'
import { useQuery } from '@apollo/react-hooks'

import { CURRENCY_PAIR_INFO } from '../graphql/queries/currencyPairInfo'

const Landing = () => {
  const [ fc, setFc ] = useState('EUR'),
        [ tc, setTc ] = useState('USD'),
        { data, loading, error, refetch } = useQuery(CURRENCY_PAIR_INFO, {
          variables: { fc, tc }
        })

  if(loading) return <p>Loading...</p>
  if(error) return <button onClick={() => refetch()}>Retry</button>

  return data && (
    <section>
      <h3>Currency Exchange</h3>
      <div>
        <select
          value={`${fc}/${tc}`}
          onChange={e => {
            const [ fromCurrency, toCurrency ] = e.target.value.split('/')
            setFc(fromCurrency)
            setTc(toCurrency)
          }}>
          <option>EUR/USD</option>
          <option>JPY/USD</option>
          <option>GBP/USD</option>
          <option>AUD/USD</option>
          <option>USD/CHF</option>
          <option>NZD/USD</option>
          <option>USD/CAD</option>
        </select>
        <button onClick={() => refetch()}>refresh</button>
      </div>
      <div className='landing_pair_data'>
        { data.currencyPairInfo && Object.keys(data.currencyPairInfo).map(val => (
          <div key={val} className='data'>
            <p><span>{val}: </span>{ data.currencyPairInfo[val] }</p>
          </div>
        ))}
      </div>
    </section>
  )
}

export default Landing
Enter fullscreen mode Exit fullscreen mode

We import useQuery from @apollo/react-hooks, the query we wrote in currencyPairInfo.js and useState from React. Instead of using a class component to initialize state via this.state, and later using setState to update it, we're going to be using the React Hook useState. useState takes the initial state as an argument and returns the current state and a function to update said state. This state will be used to collect user input. We provide our query this input as variables and useQuery returns the response.

The most traded pairs of currencies in the world are called the Majors. They constitute the largest share of the foreign exchange market, about 85%, and therefore they exhibit high market liquidity. The Majors are: EUR/USD, USD/JPY, GBP/USD, AUD/USD, USD/CHF, NZD/USD and USD/CAD. These are the currency pairs we'll provide to our users.

We create a select list, each option providing the variables to our query. These options make up the Majors. Apollo provides a refetch function that will reload the given query. We place this function in a button so that onClick the user can get up-to-date data. Take heed not to ping the Alpha Vantage API too often. If you send too many request, they'll graciously provide you with a timeout lasting a few seconds. Just enough time to ponder your insolence.

Our data is returned to us via data.currencyPairInfo. We map over said data and provide it to the DOM. You'll notice we're rendering __typename: PairDisplay. Apollo Client uses __typename and id fields to handle cache updates. If you query a different currency pair, then query the original pair again, you'll notice that the previous pairs data is instantly available via apollo-cache-inmemory.

I can't stare at our data pressed up against the left margin like this. Head into index.css and just add a quick text-align: center to the body.

With that quick aside, let's clean up Landing.js. Create a new folder in src and call it components. Inside of components create a pairs folder. Inside of pairs create a new file SelectList.js and insert the following:

// SelectList.js

import React from 'react'

const SelectList = ({ fc, setFc, tc, setTc }) => (
  <select
    value={`${fc}/${tc}`}
    onChange={e => {
      const [ fromCurrency, toCurrency ] = e.target.value.split('/')
      setFc(fromCurrency)
      setTc(toCurrency)
    }}>
    <option>EUR/USD</option>
    <option>JPY/USD</option>
    <option>GBP/USD</option>
    <option>AUD/USD</option>
    <option>USD/CHF</option>
    <option>NZD/USD</option>
    <option>USD/CAD</option>
  </select>
)

export default SelectList
Enter fullscreen mode Exit fullscreen mode

Back in Landing.js replace select with SelectList and pass the necessary props.


import React, { useState } from 'react'
import { useQuery } from '@apollo/react-hooks'

import { CURRENCY_PAIR_INFO } from '../graphql/queries/currencyPairInfo'
+import SelectList from '../components/SelectList'

const Landing = () => {
  const [ fc, setFc ] = useState('EUR'),
        [ tc, setTc ] = useState('USD'),
        { data, loading, error, refetch } = useQuery(CURRENCY_PAIR_INFO, {
          variables: { fc, tc }
        })

  if(loading) return <p>Loading...</p>
  if(error) return <button onClick={() => refetch()}>Retry</button>

  return data && (
    <section>
      <h3>Currency Exchange</h3>
      <div>
+       <SelectList fc={fc} tc={tc} setFc={setFc} setTc={setTc} />
        <button onClick={() => refetch()}>refresh</button>
      </div>
      <div className='landing_pair_data'>
        { data.currencyPairInfo && Object.keys(data.currencyPairInfo).map(val => (
          <div key={val} className='data'>
            <p><span>{val}: </span>{ data.currencyPairInfo[val] }</p>
          </div>
        ))}
      </div>
    </section>
  )
}

export default Landing
Enter fullscreen mode Exit fullscreen mode

Much better! Now that we're receiving data from the Aplha Vantage API let's get to navigation. Open up App.js and make the following adjustments:

// App.js

import React from 'react'
import { Route, Switch } from 'react-router-dom'

import Landing from './pages/Landing'
import Navbar from './components/navbar/Navbar'

const App = () => (
  <main>
    <div className='navbar'><Navbar /></div>
    <Switch>
      <Route exact path='/' component={ Landing } />
    </Switch>
  </main>
)

export default App
Enter fullscreen mode Exit fullscreen mode

We import Switch from react-router-dom and a file named Navbar that we're about to create. The Switch component renders the first child (Route or Redirect) that matches a routes path and displays it.

Inside of components create a new folder and call it navbar. Inside create a new file named Navbar.js and insert the following:

// Navbar.js

import React from 'react'
import { NavLink } from 'react-router-dom'

import './Navbar.css'

const Navbar = () => (
  <div className='navigation'>
    <header><NavLink exact to='/'>Forex</NavLink></header>
    <ul>
      <li><NavLink exact to="/login">Login</NavLink></li>
      <li><NavLink exact to='/register'>Sign Up</NavLink></li>
      <li>Logout</li>
    </ul>
  </div>
)

export default Navbar
Enter fullscreen mode Exit fullscreen mode

This article is not about styling. I wanted to be careful not to pollute the codebase with styled components, making it both time consuming and harder for some to reason about the logic. For this reason, I've decided to use only two CSS files: index.css and Navbar.css. We'll be using very little CSS — just enough for dark mode. 😎

Inside of the navbar folder create Navbar.css and insert the below code:

/* Navbar.css */

.navbar { margin-bottom: 55px; }

.navigation {
  position: fixed;
  left: 0;
  top: 0;
  background: var(--secondary-color);
  width: 100vw;
  height: 55px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.navigation header a {
  text-decoration: none;
  color: var(--header-text-color);
  margin-left: 10px;
}
.navigation ul {
  display: flex;
  list-style: none;
  margin-right: 15px;
}

.navigation li {
  margin: 0 15px;
  color: var(--header-text-color);
}
.navigation li:hover {
  cursor: pointer;
  color: var(--main-color);
}

.navigation a {
  text-decoration: none;
  color: var(--header-text-color);
}
.navigation a:hover,
.navigation a:active,
.navigation a.active {
  color: var(--main-color);
}
Enter fullscreen mode Exit fullscreen mode

Adjust index.css to the following:

/* index.css */

/* Global */

* {
  --main-color: rgb(0,0,0);
  --secondary-color: rgb(55,131,194);
  --text-color: rgba(200,200,200, 0.6);
  --header-text-color: rgb(200,200,200);
}

body {
  font-family: Arial, Helvetica, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  margin: 0;
  background: var(--main-color);
  text-align: center;
  box-sizing: border-box;
}

a { text-decoration: none;  color: rgb(0,0,0); }
section { padding-top: 50px; }
span { color: var(--secondary-color); }
p { color: var(--text-color); font-size: 14px; }
p:hover { color: rgba(200,200,200, 0.4); }
h1,h2, h3, h4 { color: var(--header-text-color); }
button, select { cursor: pointer; }


/* Landing && Pair */

.landing_pair_data {
  margin: 20px 0 20px calc(50% - 170px);
  padding: 20px;
  width: 300px;
  border-radius: 20px;
  box-shadow: 1px 1px 1px 1px var(--secondary-color), 
    -1px -1px 1px 1px var(--secondary-color);
}

.data {
  border-bottom: 1px solid var(--secondary-color);
  width: 280px;
  margin-left: calc(50% - 140px);
  text-align: start;
  text-transform: capitalize;
  padding: 2px 2px 2px 0;
}

.modal {
  position: absolute;
  background: rgb(225,225,225);
  color: var(--main-color);
  width: 280px;
  left: calc(50% - 160px);
  top: 25%;
  padding: 20px;
  animation: modal .5s;
}
.modal p {
  color: var(--main-color);
}

@keyframes modal {
  from { opacity: 0; }
  to { opacity: 1; }
}


/* Account  */

.pair_divs {
  padding: 20; 
  border: 1px solid rgba(255,255,255,0.1); 
  border-radius: 5px;
  width: 400px; 
  margin: 10px auto;
}
.pair_divs p {
  text-align: start;
  padding-left: 20px;
}
.pair_divs:hover {
  border: 1px solid rgba(55,131,194, 0.3);
}


/* Chart  */

.chartData {  
  padding-top: 50px;  
  height: calc(100vh - 105px); 
}
.chartData form input,
.chartData form button {
  margin: 10px;
}


/* Login && Register */

.login input,
.register input {
  padding: 5px; 
  margin: 10px 0px; 
  width: 60%;
  max-width: 400px;
  background: var(--main-color);
  color: var(--header-text-color);
  font-size: 13px;
}

.login form,
.register form {
  display: flex; 
  justify-content: center; 
  flex-direction: column; 
  align-items: center;
}
Enter fullscreen mode Exit fullscreen mode

These two files represent the entirety of our CSS. Save your files and take a look at the browser.

navbar

Now that we have our navbar, let's create a register route. Inside of graphql/mutations create a new file named register.js and insert the below code:

// graphql/mutations/register.js

import gql from 'graphql-tag'

export const REGISTERMUTATION = gql`
  mutation RegisterMutation($email: String!, $password: String!, $name: String!) {
    register(email: $email, password: $password, name: $name)  
  }
`
Enter fullscreen mode Exit fullscreen mode

Inside components, create a new folder and name it auth. Inside of auth create a new file and name it Register.js. Insert the following:

// components/auth/Register.js

import React, { useState } from 'react'
import { useMutation } from '@apollo/react-hooks'

import { REGISTERMUTATION } from '../../graphql/mutations/register'

export default function Register(props) {
  const [ email, setEmail ] = useState(''),
        [ password, setPassword ] = useState(''),
        [ name, setName ] = useState(''),
        [ register, { error } ] = useMutation(REGISTERMUTATION, {
          variables: { email, password, name }
        })

  return (
    <div className='register'>
      <form onSubmit={ async e => {
        e.preventDefault()
        await register()
        props.history.push('/login')
      }}>
        <h2>Sign Up</h2>
        <input
          required
          name='email'
          type='email'
          value={ email }
          onChange={ e => setEmail(e.target.value) }
          placeholder='Enter your email'
        />
        <input
          required
          type='password'
          value={ password }
          onChange={ e => setPassword(e.target.value) }
          placeholder='Enter your password'
        />
        <input
          required
          type='text'
          value={ name }
          onChange={ e => setName(e.target.value) }
          placeholder='Enter your name'
        />
        { error && <p>{ error.message }</p> }
        <button>SignUp</button>
      </form>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

We use a form to collect the users data and place it in the components state. onSubmit, we pass the state as variables to the register mutation. Since we're setting the emails input type to 'email' and passing the required prop, we won't see the error we threw on the backend. We're not comparing passwords so there's no error to be thrown there. The only error we created that will make it to us is 'User already exists.' That's why I'm not checking for individual errors and just displaying the error under all of the inputs.

Open up App.js. Import Register.js and create the Register components Route.

// App.js

import React from 'react'
import { Route, Switch } from 'react-router-dom'

import Landing from './pages/Landing'
import Navbar from './components/navbar/Navbar'
import Register from './components/auth/Register'

const App = () => (
  <main>
    <div className='navbar'><Navbar /></div>
    <Switch>
      <Route exact path='/' component={ Landing } />
      <Route path='/register' component={ Register } />
    </Switch>
  </main>
)

export default App
Enter fullscreen mode Exit fullscreen mode

If you navigate to our Register component, you'll be able to register a new user. We can confirm this by checking our database.

Inside of graphql/mutations create a new file, name it login.js and insert the following:

// graphql/mutations/login.js

import gql from 'graphql-tag'

export const LOGINMUTATION = gql`
  mutation LoginMutation($email: String!, $password: String!) {
    login(email: $email, password: $password) {
      id
      email
      name
    }
  }
`
Enter fullscreen mode Exit fullscreen mode

Inside of graphql/queries create a new file named me.js and add the following code:

// graphql/queries/me.js

import gql from 'graphql-tag'

export const MEQUERY = gql`
  query MeQuery {
    me {
      id
      email 
      name
      bankroll
    }
  }
`
Enter fullscreen mode Exit fullscreen mode

Head to the auth folder, create a new file and name it Login.js. Adjust Login.js such that it resembles the below code:

// Login.js

import React, { useState } from 'react'
import { useMutation } from '@apollo/react-hooks'

import { MEQUERY } from '../../graphql/queries/me'
import { LOGINMUTATION } from '../../graphql/mutations/login'

export default function Login(props) {
  const [ email, setEmail ] = useState(''),
        [ password, setPassword ] = useState(''),
        [ login, { error } ] = useMutation(LOGINMUTATION, {
          variables: { email, password },
          update: (cache, { data }) => {
            if(!data || !data.login) return 
            cache.reset()
            cache.writeQuery({
              query: MEQUERY,
              data: { me: data.login }
            })
          }
        })

  return (
    <div className='login'>
      <form onSubmit={ async e => {
        e.preventDefault()
        await login()
        props.history.push('/') 
      }}>
        <h2>Login</h2>
        <input
          required
          name='email'
          type='email'
          value={ email }
          onChange={ e => setEmail(e.target.value) }
          placeholder='Enter your email'
        />
        <input
          required
          type='password'
          value={ password }
          onChange={ e => setPassword(e.target.value) }
          placeholder='Enter your password'
        />
        { error && <p>{ error.message }</p> }
        <button type='submit'>Login</button>
      </form>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

onSubmit we log the user in and redirect them back to the home page. ApolloClient provides us with an update function that we can use to update the cache once a user logs in. Once a user logs in successfully we update the cache such that the me query represents the current user.

From the docs:

The update function is called with the Apollo cache as the first argument. The cache has several utility functions such as cache.readQuery and cache.writeQuery that allow you to read and write queries to the cache with GraphQL as if it were a server. There are other cache methods, such as cache.readFragment, cache.writeFragment, and cache.writeData, which you can learn about in our detailed caching guide if you're curious.

Note: The update function receives cache rather than client as its first parameter. This cache is typically an instance of InMemoryCache, as supplied to the ApolloClient constructor when the client was created. In case of the update function, when you call cache.writeQuery, the update internally calls broadcastQueries, so queries listening to the changes will update. However, this behavior of broadcasting changes after cache.writeQuery happens only with the update function. Anywhere else, cache.writeQuery would just write to the cache, and the changes would not be immediately broadcast to the view layer. To avoid this confusion, prefer client.writeQuery when writing to cache.

The second argument to the update function is an object with a data property containing your mutation result. If you specify an optimistic response, your update function will be called twice: once with your optimistic result, and another time with your actual result. You can use your mutation result to update the cache with cache.writeQuery.

If a user enters an invalid email they will see the HTML error, not ours. If a user enters a valid but incorrect email we throw, 'Email or password is incorrect!' If a user enters an incorrect password, we throw the exact same error, making it harder for a bad actor to decipher which input is incorrect. This being the case, we probably don't want to display the error in the place that it occurs, lest we give away the game.

Open up App.js and make the following adjustments:

// App.js

import React from 'react'
import { Route, Switch } from 'react-router-dom'

import Landing from './pages/Landing'
import Navbar from './components/navbar/Navbar'
import Register from './components/auth/Register'
import Login from './components/auth/Login'

const App = () => (
  <Switch>
    <Route path='/login' component={ Login } />
    <Route path='/' render={() => (
      <main>
        <div className='navbar'><Navbar /></div>
        <Route exact path='/' component={ Landing } />
        <Route path='/register' component={ Register } />
      </main>
    )} />
  </Switch>
)

export default App
Enter fullscreen mode Exit fullscreen mode

Since we're clearing the cache before a user logs in, and the navbar utilizes the me query for authorization, we're going to place the Login component outside of the navbar.

We can now login a user and we are persisting the users session id in a cookie. If you open up your DevTools, under the Application folder, and inside the Cookies tab, you'll see our cookie.

It'd probably be best if we weren't simultaneously displaying both Login and Logout in our navbar. Adjust Navbar.js like so:

// Navbar.js

import React from 'react'
import { NavLink, Redirect } from 'react-router-dom'
import { useQuery } from '@apollo/react-hooks'

import { MEQUERY } from '../../graphql/queries/me'
import './Navbar.css'

const Navbar = () => {
  const { data, loading, error } = useQuery(MEQUERY)

  if(loading) return <p>Loading....</p>
  if(error) return <Redirect to='/login' />
  if(!data) return <p>This is unfortunate</p>

  return (
    <div className='navigation'>
      <header><NavLink exact to='/'>Forex</NavLink></header>
      { !data.me ? (
        <ul>
          <li><NavLink exact to='/login'>Login</NavLink></li>
          <li><NavLink exact to='/register'>SignUp</NavLink></li>
        </ul> ) 
      : (
        <ul>
          <li>Logout</li>
        </ul>
      )}
    </div>
  )
}

export default Navbar
Enter fullscreen mode Exit fullscreen mode

In the case of an error in our me query, we Redirect the user to login. If a user is returned we display Logout, else Login/SignUp. At the moment Logout isn't very useful. We'll start with the mutation. Create a new file named logout.js inside of graphql/mutations and insert the code below.

// graphql/mutations/logout.js

import gql from 'graphql-tag'

export const LOGOUT_MUTATION = gql`
  mutation Logout {
    logout
  }
`
Enter fullscreen mode Exit fullscreen mode

Create Logout.js inside of components/auth and insert the following:

// components/auth/Logout.js

import React from 'react' 
import { useMutation } from '@apollo/react-hooks'
import { withRouter } from 'react-router-dom'

import { MEQUERY } from '../../graphql/queries/me'
import { LOGOUT_MUTATION } from '../../graphql/mutations/logout'

const Logout = props => {
  const [logout] = useMutation(LOGOUT_MUTATION, {
    update: cache => {
      cache.writeQuery({
        query: MEQUERY,
        data: { me: null }
      })
      props.history.push('/')
    }
  })

  return <div onClick={() => logout()}>Logout</div>
}

export default withRouter(Logout)
Enter fullscreen mode Exit fullscreen mode

When a user clicks Logout three things happen:

  • The logout mutation destroys the session on the req Object.

  • We update the cache such that the me query returns null.

  • We redirect the user to the home page.

If a component isn't rendered by React Router (passed as a component prop to a Route), then we won't have access to history.push. React Router's HOC withRouter provides us access to the history Object via props. We utilize props.history.push('/') to navigate the user back to the home page. Don't forget to wrap the Logout component with withRouter when exporting the file.

Import Logout.js into Navbar.js and replace <li><Logout></li> with our new component. With that adjustment thou shalt logout!

We can now focus on allowing users to open long/short positions. Open up Landing.js and make the following adjustments:

// Landing.js

import React, { useState } from 'react'
import { useQuery } from '@apollo/react-hooks'

import { MEQUERY } from '../graphql/queries/me'
import { CURRENCY_PAIR_INFO } from '../graphql/queries/currencyPairInfo'
import SelectList from '../components/pairs/SelectList'
import OpenLongPosition from '../components/positions/OpenLongPosition'

const Landing = () => {
  const [ fc, setFc ] = useState('EUR'),
        [ tc, setTc ] = useState('USD'),
        [ askPrice, setAskPrice ] = useState(0),
        [ bidPrice, setBidPrice ] = useState(0),
        [ showModal, setShowModal ] = useState(false),
        user = useQuery(MEQUERY),
        { data, loading, error, refetch } = useQuery(CURRENCY_PAIR_INFO, {
          variables: { fc, tc }
        })

  if(loading) return <p>Loading...</p>
  if(error) return <button onClick={() => refetch()}>Retry</button>

  return data && (
    <section>
      <h2>Currency Exchange</h2>
      { user.data.me && <p>Available Balance { user.data.me.bankroll.toLocaleString()}.00</p> }
      <div>
        <SelectList fc={fc} tc={tc} setFc={setFc} setTc={setTc} />
        <button onClick={() => refetch()}>Refresh</button>
        { user.data.me && (
          <OpenLongPosition
            fc={fc}
            tc={tc}
            pairData={data}
            askPrice={askPrice}
            setAskPrice={setAskPrice}
            showModal={showModal}
            setShowModal={setShowModal}
        />)}
        <button>Sell</button>
      </div>
      <div className='landing_pair_data'>
        { data.currencyPairInfo && Object.keys(data.currencyPairInfo).map(val => (
          <div key={val} className='data'>
            <p><span>{val}: </span>{ data.currencyPairInfo[val] }</p>
          </div>
        ))}
      </div>
    </section>
  )
}

export default Landing
Enter fullscreen mode Exit fullscreen mode

We import MEQUERY and a file we'll need to create called OpenLongPosition. We integrate useState to store/update the askPrice, bidPrice, and to toggle a modal. After we have our user, we display their bankroll (available funds). If a user alters the currency pair or refreshes the data, we change the state of askPrice and bidPrice accordingly. Finally, if a user is found we display a 'Buy' button (OpenLongPosition).

Inside of graphql/mutations create a new file, name it openPosition.js, and add the below code:

// openPosition.js

import gql from 'graphql-tag'

export const OPENPOSITION = gql`
  mutation OpenPosition(
    $pair: String!, 
    $lotSize: Int!, 
    $openedAt: Float!, 
    $position: String!
  ) {
    openPosition(
      pair: $pair, 
      lotSize: $lotSize, 
      openedAt: $openedAt, 
      position: $position
    ) {
      success
      message
      pair {
        id
        user
        position
        pair
        lotSize
        openedAt
      }
    }
  }
`
Enter fullscreen mode Exit fullscreen mode

In graphql/queries create a new file named getPairs.js and insert the below code:

// graphql/queries/getPairs.js

import gql from 'graphql-tag'

export const GETPAIRS = gql`
  query GetPairs {
    getPairs {
      id
      user
      pair
      lotSize
      openedAt
      closedAt
      pipDif
      profitLoss
      open
      position
      createdAt
      updatedAt
    }
  }
`
Enter fullscreen mode Exit fullscreen mode

In components create a new folder and name it positions. Inside create a new file named OpenLongPosition.js and add the following code:

// OpenLongPosition.js

import React from 'react'
import { Link } from 'react-router-dom'
import { useMutation } from '@apollo/react-hooks'

import { OPENPOSITION } from '../../graphql/mutations/openPosition'
import { MEQUERY } from '../../graphql/queries/me'
import { GETPAIRS } from '../../graphql/queries/getPairs'

const OpenLongPosition = ({
  fc, 
  tc, 
  pairData,
  askPrice,
  setAskPrice,
  showModal,
  setShowModal
}) => {

  const [ openPosition, { data, loading, error }] = useMutation(OPENPOSITION, {
    variables: {
      pair: `${fc}/${tc}`,
      lotSize: 100000,
      openedAt: askPrice,
      position: 'long'
    },
    update: cache => {
      const user = cache.readQuery({ query: MEQUERY })
      user.me.bankroll -= 100000
      cache.writeQuery({
        query: MEQUERY,
        data: { me: user.me }
      })
    },
    refetchQueries: [{ query: GETPAIRS }]
  })

  if(loading) return <p>Loading...</p>
  if(error) return <p>{ error.message }</p>

  return openPosition && (
    <>
      <button onClick={ async () => {
        await setAskPrice(+pairData.currencyPairInfo.askPrice)
        alert('Are you sure you want to buy?')
        await openPosition()
        setShowModal(true)
      }}>
        Buy
      </button>
      { data && data.openPosition.message && showModal && (
        <div className='modal'>
          <button onClick={() => setShowModal(false)}>x</button>
          <p>{ data.openPosition.message }</p>
          <p>Currency Pair: { data.openPosition.pair.pair }</p>
          <p>Lot Size: { data.openPosition.pair.lotSize.toLocaleString() }.00</p>
          <p>Opened At: { data.openPosition.pair.openedAt }</p>
          <p>Position: { data.openPosition.pair.position }</p>
          <Link to={{ pathname: '/account', state: { data } }}>
            <button>Details</button>
          </Link>
        </div>
      )}
    </>
  )
}

export default OpenLongPosition
Enter fullscreen mode Exit fullscreen mode

We pass our mutation the required variables. Once the user clicks the 'Buy' button we'd usually want to display some data and allow them to confirm the purchase. Here we're just using an alert. The user is then displayed a modal describing their transaction and a details button that will redirect them to a page we still need to create — Account. Open up MongoDB Atlas and you'll see the newly created position.

Apollo provides us a number of ways to update the cache after a mutation. We've implemented a few of them in this project. In this component we're utilizing refetchQueries to update our pairs. Let's take a look at the docs:

refetchQueries is the simplest way of updating the cache. With refetchQueries you can specify one or more queries that you want to run after a mutation is completed in order to refetch the parts of the store that may have been affected by the mutation.

We've seen a few of the options that the Mutation hook accepts. Take a peek at the docs for the full list.

Before we get to creating the Account component, let's allow a user to open a short position. Open up components/positions, create a new file named OpenShortPosition.js and add the below code:

// components/positions/OpenShortPosition.js

import React from 'react'
import { Link } from 'react-router-dom'
import { useMutation } from '@apollo/react-hooks'

import { OPENPOSITION } from '../../graphql/mutations/openPosition'
import { MEQUERY } from '../../graphql/queries/me'
import { GETPAIRS } from '../../graphql/queries/getPairs'

const OpenShortPosition = ({
  fc,
  tc,
  pairData,
  bidPrice,
  setBidPrice,
  showModal,
  setShowModal
}) => {
  const [ openPosition, { data, loading, error }] = useMutation(OPENPOSITION, {
    variables: {
      pair: `${fc}/${tc}`, 
      lotSize: 100000, 
      openedAt: bidPrice, 
      position: 'short' 
    },
    update: cache => {
      const user = cache.readQuery({ query: MEQUERY })
      user.me.bankroll -= 100000
      cache.writeQuery({
        query: MEQUERY,
        data: { me: user.me }
      })
    },
    refetchQueries: [{ query: GETPAIRS }]
  })

  if(loading) return <p>Loading...</p>
  if(error) return <p>{ error.message }</p>

  return openPosition && (
    <>
      <button onClick={ async () => {
        await setBidPrice(+pairData.currencyPairInfo.bidPrice)
        alert('Are you sure you want to sell short?')
        await openPosition()
        setShowModal(true) 
      }}>
        Sell
      </button> 
      { data && data.openPosition.message && showModal && ( 
        <div className='modal'>
          <button onClick={() => setShowModal(false)}>x</button>
          <p>{ data && data.openPosition.message }</p>
          <p>Currency Pair: { data.openPosition.pair.pair }</p>
          <p>Lot Size: { data.openPosition.pair.lotSize.toLocaleString() }.00</p>
          <p>Opened At: { data.openPosition.pair.openedAt }</p>
          <p>Position: { data.openPosition.pair.position }</p>
          <Link to={{ pathname: '/account', state: { data } }}>
            <button>Details</button>
          </Link>
        </div>
      )}
    </>
  )
}

export default OpenShortPosition
Enter fullscreen mode Exit fullscreen mode

Here we do the exact same thing we did in OpenLongPosition except we pass bidPrice instead of askPrice and position: short instead of position: long as arguments.

Back in Landing.js replace the 'Sell' button with our newly created OpenShortPosition component.

// Landing.js

import OpenShortPosition from '../components/positions/OpenShortPosition'

{ user.data.me && (
  <OpenShortPosition
    fc={fc}
    tc={tc}
    pairData={data}
    bidPrice={bidPrice}
    setBidPrice={setBidPrice}
    showModal={showModal}
    setShowModal={setShowModal}
/>)}
Enter fullscreen mode Exit fullscreen mode

With that our users are able to sell short. We still need to create our Account component. Let's get to it! In the pages folder create Account.js and add the below code:

// Account.js

import React, { useState } from 'react'
import { useQuery } from '@apollo/react-hooks'
import { Link, Redirect } from 'react-router-dom'

import { GETPAIRS } from '../graphql/queries/getPairs'
import { MEQUERY } from '../graphql/queries/me'

const Account = props => {
  const [ open, setOpen ] = useState(true),
        user = useQuery(MEQUERY),
        { data, loading, error } = useQuery(GETPAIRS)

  if(user.error) return <Redirect to='/login' />
  if(!user.data || !user.data.me) return <p>A man has no name.</p>
  if(loading) return <p>Loading...</p>
  if(!data) return <p>Nothing to show!</p>
  if(error) return <p>{ error.message }</p>

  return (
    <section>
      <h2>{ user.me.name }</h2>
      <div>
        <p><span>Available Balance: </span>{ user.me.bankroll.toLocaleString() }.00</p> 
      </div>
      <br />
      { props.location.state &&  (
        <div>
          <h3>New Position</h3>
          <div className='pair_divs'>
            <p><span>Pair: </span>{ props.location.state.data.openPosition.pair.pair }</p>
            <p><span>Lot Size: </span>{ props.location.state.data.openPosition.pair.lotSize.toLocaleString() }.00</p>
            <p><span>Pip Dif: </span>{ props.location.state.data.openPosition.pair.openedAt }</p>
            <p><span>Position: </span>{ props.location.state.data.openPosition.pair.position }</p>
          </div>
        </div>
      )}
      <br />
      <h3>Currency Pairs</h3>
      <button onClick={() => setOpen(true)}>open</button>
      <button onClick={() => setOpen(false)}>closed</button>
      <div>
      { data.getPairs && data.getPairs.map(pair => pair.open && open && (
        <div className='pair_divs' key={pair.id}>
          <Link to={{ pathname: '/pair', state: { pair, me: user.me } }}>
            { pair.pair && <p><span>Currency Pair: </span>{ pair.pair }</p> }
            { pair.lotSize && <p><span>Lot Size: </span>{ pair.lotSize.toLocaleString() }.00</p> }
            { pair.position && <p><span>Position: </span>{ pair.position }</p> }
            { pair.openedAt && <p><span>Opened At: </span>{ pair.openedAt.toFixed(4) }</p> }
            { pair.createdAt && <p><span>Created At: </span>{ new Date(+pair.createdAt).toLocaleString() }</p> }
            { pair.updatedAt && <p><span>Updated At: </span>{ new Date(+pair.updatedAt).toLocaleString() }</p> }
          </Link>
        </div>
      ))}
      { data.getPairs && data.getPairs.map(pair => !pair.open && !open && (
        <div className='pair_divs' key={ pair.id }>
          <div>
            { pair.pair && <p><span>Currency Pair: </span>{ pair.pair }</p> }
            { pair.lotSize && <p><span>Lot Size: </span>{ pair.lotSize.toLocaleString() }.00</p> }
            { pair.position && <p><span>Position: </span>{ pair.position }</p> }
            { pair.openedAt && <p><span>Opened At: </span>{ pair.openedAt.toFixed(4) }</p> }
            { pair.closedAt && <p><span>Closed At: </span>{ pair.closedAt.toFixed(4) }</p> }
            { <p><span>Pip Dif: </span>{ pair.pipDif || 0 }</p> }
            { <p><span>Profit/Loss: </span>{ pair.profitLoss.toFixed(2) || 0 }</p> }
            { pair.createdAt && <p><span>Created At: </span>{ new Date(+pair.createdAt).toLocaleString() }</p> }
            { pair.updatedAt && <p><span>Updated At: </span>{ new Date(+pair.updatedAt).toLocaleString() }</p> }
          </div>
        </div>
      ))}
      </div>
    </section>
  )
}

export default Account
Enter fullscreen mode Exit fullscreen mode

React Router's Link component allows us to pass state when navigating a user to another view. This is convienient if we wanted to render unique views when coming from certain routes. We use this to display the new position that the user just opened — if any. You could get creative here but we'll keep it simple and just display some data about the new position.

Under the new position (if there is one), we display all of the users positions. Open positions are shown by default, but we provide a button to toggle between open and closed. If the position is open, the user can click on the currency pair. This will navigate them to /pair (which we need to create) and provide further options. This component is a bit verbose. We'll refactor in a moment.

Let's import Account.js into App.js and create its Route.

// App.js

import React from 'react'
import { Route, Switch } from 'react-router-dom'

import Landing from './pages/Landing'
import Navbar from './components/navbar/Navbar'
import Register from './components/auth/Register'
import Login from './components/auth/Login'
import Account from './pages/Account'

const App = () => (
  <Switch>
    <Route path='/login' component={ Login } />
    <Route path='/' render={() => (
      <main>
        <div className='navbar'><Navbar /></div>
        <Route exact path='/' component={ Landing } />
        <Route path='/register' component={ Register } />
        <Route path='/account' component={ Account } />
      </main>
    )} />
  </Switch>
)

export default App
Enter fullscreen mode Exit fullscreen mode

We'll also want Account to be accessible from the Navbar when a user is logged in.

// Navbar.js

return (
  <ul>
    <li><NavLink to='/account'>Account</NavLink></li>
    <li><Logout /></li>
  </ul>
)
Enter fullscreen mode Exit fullscreen mode

When navigating to /account from the navbar you'll notice 'New Position' isn't being displayed. Cool! Now let's refactor Account.js and add some functionality. Inside of components/pairs create a new file named NewPosition.js. Cut the following code from Account.js and insert it into your newly created file.

// components/pairs/NewPosition.js

import React from 'react'

export default function NewPosition({ state }) {
  return (
    <div>
      <h3>New Position</h3>
      <div className='pair_divs' style={{ textAlign: 'center' }}>
        <p><span>Pair: </span>{ state.data.openPosition.pair.pair }</p>
        <p><span>Lot Size: </span>{ state.data.openPosition.pair.lotSize.toLocaleString() }.00</p>
        <p><span>Pip Dif: </span>{ state.data.openPosition.pair.openedAt }</p>
        <p><span>Position: </span>{ state.data.openPosition.pair.position }</p>
      </div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

In the same folder create a new file and name it Pairs.js. Cut the following code from Account.js and add it to this file.

// components/pairs/Pairs.js

import React from 'react'
import { Link } from 'react-router-dom'

const Pairs = ({ data, open, user }) => (
  <div>
    { data.getPairs && data.getPairs.map(pair => pair.open && open && (
      <div className='pair_divs' key={ pair.id }>
        <Link to={{ pathname: '/pair', state: { pair, me: user.data.me } }}>
          { pair.pair && <p><span>Currency Pair: </span>{ pair.pair }</p> }
          { pair.lotSize && <p><span>Lot Size: </span>{ pair.lotSize.toLocaleString() }.00</p> }
          { pair.position && <p><span>Position: </span>{ pair.position }</p> }
          { pair.openedAt && <p><span>Opened At: </span>{ pair.openedAt.toFixed(4) }</p> }
          { pair.createdAt && <p><span>Created At: </span>{ new Date(+pair.createdAt).toLocaleString() }</p> }
          { pair.updatedAt && <p><span>Updated At: </span>{ new Date(+pair.updatedAt).toLocaleString() }</p> }
        </Link>
      </div>
    ))}
    { data.getPairs && data.getPairs.map(pair => !pair.open && !open && (
      <div className='pair_divs' key={ pair.id }>
        <div>
          { pair.pair && <p><span>Currency Pair: </span>{ pair.pair }</p> }
          { pair.lotSize && <p><span>Lot Size: </span>{ pair.lotSize.toLocaleString() }.00</p> }
          { pair.position && <p><span>Position: </span>{ pair.position }</p> }
          { pair.openedAt && <p><span>Opened At: </span>{ pair.openedAt.toFixed(4) }</p> }
          { pair.closedAt && <p><span>Closed At: </span>{ pair.closedAt.toFixed(4) }</p> }
          { <p><span>Pip Dif: </span>{ pair.pipDif || 0 }</p> }
          { <p><span>Profit/Loss: </span>{ pair.profitLoss.toFixed(2) || 0 }</p> }
          { pair.createdAt && <p><span>Created At: </span>{ new Date(+pair.createdAt).toLocaleString() }</p> }
          { pair.updatedAt && <p><span>Updated At: </span>{ new Date(+pair.updatedAt).toLocaleString() }</p> }
        </div>
      </div>
    ))}
  </div>
)

export default Pairs
Enter fullscreen mode Exit fullscreen mode

Okay. We should implement an addFunds button while we're working on Account.js. Create a new file named addFunds.js inside of graphql/mutations and insert the following:

// graphql/mutations/addFunds.js

import gql from 'graphql-tag'

export const ADDFUNDS = gql`
  mutation ($amount: Int!) {
    addFunds(amount: $amount) {
      success
      message
      bankroll
    }
  }
`
Enter fullscreen mode Exit fullscreen mode

In the components/pairs folder create a new file named AddFunds.js and add the below code:

// components/pairs/AddFunds.js

import React, { useState } from 'react'
import { useMutation } from '@apollo/react-hooks'

import { ADDFUNDS } from '../../graphql/mutations/addFunds'

export default function AddFunds() {
  const [ showModal, setShowModal ] = useState(false),
        [ addFunds, { data, loading, error } ] = useMutation(ADDFUNDS, {
          variables: { amount: 1000000 }
        })

  if(loading) return <p>Loading...</p>
  if(error) return <p>{ error.message }</p>

  return addFunds && (
    <>
      <button onClick={ async () => {
        alert('Are you sure?')
        await addFunds()
        setShowModal(true)
      }}>Add Funds</button>
      { data && data.addFunds.message && showModal && (
        <div className='modal'>
          <button onClick={() => setShowModal(false)}>x</button>
          <p>{ data.addFunds.message }</p>
        </div>
      )}
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

Usually the user would have some say over how much they deposit. That said, who's making a fuss when we're just out here handing out milli's?

It's high time we got back to Account.js.

// Account.js

import React, { useState } from 'react'
import { useQuery } from '@apollo/react-hooks'
import { Redirect } from 'react-router-dom'

import { GETPAIRS } from '../graphql/queries/getPairs'
import { MEQUERY } from '../graphql/queries/me'
import AddFunds from '../components/pairs/AddFunds'
import Pairs from '../components/pairs/Pairs'
import NewPosition from '../components/pairs/NewPosition'

export default function Account(props) {
  const [ open, setOpen ] = useState(true),
        user = useQuery(MEQUERY),
        { data, loading, error } = useQuery(GETPAIRS)

  if(user.error) return <Redirect to='/login' />
  if(!user.data || !user.data.me) return <p>A man has no name.</p>
  if(loading) return <p>Loading...</p>
  if(!data) return (
    <section>
      <h2>{ user.data.me.name }</h2>
      <div>
        <p><span>Available Balance: </span>{ user.data.me.bankroll.toLocaleString() }.00</p>
        <AddFunds />
      </div>
    </section>
  )
  if(error) return <p>{ error.message }</p>

  return (
    <section>
      <h2>{ user.data.me.name }</h2>
      <div>
        <p><span>Available Balance: </span>{ user.data.me.bankroll.toLocaleString() }.00</p>
        <AddFunds />
      </div>
      { props.location.state && <NewPosition state={ props.location.state } /> }
      <h3>Currency Pairs</h3>
      <button onClick={() => setOpen(true)}>open</button>
      <button onClick={() => setOpen(false)}>closed</button>
      <Pairs data={ data } open={ open } user={ user } />
    </section>
  )
}
Enter fullscreen mode Exit fullscreen mode

First, we handle our imports. Next, we implement useQuery to find out about the user. If there's no getPair data we just display information about the user and the AddFunds button else we display all the data.

Our users can now open positions and add money to their account. Let's allow them to close positions. Once again this starts with a mutation. In graphql/mutations create closePosition.js and add the following:

// graphql/mutations/closePosition.js

import gql from 'graphql-tag'

export const CLOSEPOSITION = gql`
  mutation ClosePosition($id: ID!, $closedAt: Float!) {
    closePosition(id: $id, closedAt: $closedAt) {
      success
      message
      pair {
        id
        user
        pair
        lotSize
        position
        openedAt
        closedAt
        pipDif
        profitLoss
        open
        createdAt
        updatedAt
      }
    }
  }
`
Enter fullscreen mode Exit fullscreen mode

When a user clicks on an open position, they get navigated to /pair. This is where they'll be able to close their positions. In the pages folder, create Pair.js and adjust it such that it resembles the below code:

// Pair.js

import React from 'react'
import { useQuery } from '@apollo/react-hooks'

import { CURRENCY_PAIR_INFO } from '../graphql/queries/currencyPairInfo'
import ClosePosition from '../components/positions/ClosePosition'
import PairDetails from '../components/pairs/PairDetails'

export default function Pair(props) {
  const { createdAt, lotSize, openedAt, pair, position, id } = props.location.state.pair,
        { bankroll, name } = props.location.state.me,
        [ fc, tc ] = pair.split('/'),
        { data, loading, error, refetch } = useQuery(CURRENCY_PAIR_INFO, {
          variables: { fc, tc }
        })

  if(loading) return <p>Loading...</p>
  if(error) return <p>{ error.message }</p>

  const { bidPrice, lastRefreshed, askPrice } = data.currencyPairInfo,
        pipDifLong = (bidPrice - openedAt).toFixed(4),
        pipDifShort = (openedAt - askPrice).toFixed(4),
        potentialProfitLoss = position === 'long'
          ? pipDifLong * lotSize
          : pipDifShort * lotSize,
        date = new Date(lastRefreshed + ' UTC')

  return data && (
    <section>
      <div className='landing_pair_data'>
        <h3>Pair Details</h3>
        <div>
          <p>{ name } your available balance is { bankroll.toLocaleString() }.00</p> 
          <div>
            <button onClick={() => refetch()}>Refresh</button>
            <ClosePosition 
              id={id} 
              bidPrice={bidPrice} 
              askPrice={askPrice} 
              position={position} 
            />
          </div>
        </div>
        <PairDetails
          pair={pair} 
          lotSize={lotSize}
          openedAt={openedAt}
          position={position}
          createdAt={createdAt}
          askPrice={askPrice}
          bidPrice={bidPrice}
          lastRefreshed={date.toLocaleString()}
          pipDifLong={pipDifLong}
          pipDifShort={pipDifShort}
          potentialProfitLoss={potentialProfitLoss}
        />
      </div>
    </section>
  )
}
Enter fullscreen mode Exit fullscreen mode

Once we have our state we pass in the query variables to currencyPairInfo. The response provides the data required to complete our closePosition mutation. Depending on whether the position is long or short, we use either the askPrice or bidPrice to calculate the difference in price since the initial purchase. This difference in price is what we're calling the pip difference (pipDif).

As described by dailyfx.com:

PIP - which stands for Point in Percentage - is the unit of measure used by forex traders to define the smallest change in value between two currencies. This is usually represented by a single digit move in the fourth decimal place. The pip value is calculated by multiplying one pip(0.0001) by the specific lot/contract size. For standard lots this entails 100,000 units of the base currency and for mini lots, this is 10,000 units. For example, looking at EUR/USD, a one pip movement in a standard contract is equal to $10(0.0001 x 100,000).

Each currency pair has its own relative relationship, so we calculate profit/loss by simply comparing the openedAt price to the closedAt price. We calculate the pipDif by first figuring out if the position is long or short. If the position is long we subtract the openedAt price from the bidPrice. Conversely, if the position is short, we subtract the askPrice from the openedAt price. This will provide our pipDif. Once we have the difference in price, we multiply it by the lotSize.

You can see how easily this is calculated once demonstrated visually. For a standard lot (100,000 units) each pip (usually fourth decimal place) movement equates to 10 currency units of profilt/loss.

pipdif

For a mini lot (10,000 units) we do the same but every pip movement equates to 1 currency unit profit/loss.

pipdif minilot

It's important to understand that we're not converting one currency to another. We're just betting on which currency will be worth more relative to the other. For clarity, if you wanted to buy (or long) EUR against USD, you'd sell EUR/USD or buy USD/EUR. Conversely, to long USD against the EUR, you'd buy EUR/USD or sell USD/EUR. Rollover (interest) and margin are outside the scope of this tutorial so we'll focus exclusively on the pipDif.

We need to create ClosePosition and PairDetails. Inside of components/positions, create ClosePosition.js and add the following:

// components/positions/ClosePosition.js

import React, { useState } from 'react'
import { useQuery, useMutation } from '@apollo/react-hooks'
import { Link } from 'react-router-dom'

import { CLOSEPOSITION } from '../../graphql/mutations/closePosition'
import { MEQUERY } from '../../graphql/queries/me'
import { GETPAIRS } from '../../graphql/queries/getPairs'

export default function ClosePosition({ id, bidPrice, askPrice, position }) {
  const [ showModal, setShowModal ] = useState(false),
        { refetch  } = useQuery(MEQUERY),
        [ closePosition, { data, loading, error } ] = useMutation(CLOSEPOSITION, {
          variables: position === 'long'
            ? { id, closedAt: +bidPrice } 
            : { id, closedAt: +askPrice },
          refetchQueries: [{ query: GETPAIRS }]
        })

  if(loading) return <p>Loading...</p>
  if(error) return <p>{ error.message }</p>

  return closePosition && (
    <>
      <button onClick={ async () => {
        alert(`Are you sure you want to close your ${
          position === 'long' ? 'long' : 'short' } position?`) 
        await closePosition()
        setShowModal(true)
        refetch()
      }}>
        { position === 'long' ? 'Sell' : 'Buy' }
      </button> 

      { data && data.closePosition.message && showModal && ( 
        <div className='modal'>
          <button onClick={() => setShowModal(false)}>x</button>
          <p>{ data.closePosition.message }</p>
          <Link to='/account'><button>Account</button></Link>
        </div>
      )}
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

All this file is doing is deciphering whether the position is long or short and providing the closePosition mutation the appropriate variables (pair id and bidPrice/askPrice). The closePosition response message will be displayed via a modal.

We're using the useQuery hook to gain access to the me queries refetch function. We add the refetch method to our button such that after the closePosition mutation runs, refetch will refresh the users data. If we didn't use refetchQueries here, after our mutation runs the open pairs would be up-to-date, but the closed pairs wouldn't be.

In the components folder create PairDetails.js and add the code below:

// components/PairDetails.js

import React from 'react'

const PairDetails = ({
  pair,
  lotSize,
  openedAt,
  position,
  createdAt,
  askPrice,
  bidPrice,
  lastRefreshed,
  pipDifLong,
  pipDifShort,
  potentialProfitLoss
}) => (
  <div>
    <p><span>Currency Pair: </span>{pair}</p>
    <p><span>Lot Size: </span>{lotSize.toLocaleString()}.00</p>
    <p><span>Opened At: </span>{(+openedAt).toFixed(4)}</p>
    <p><span>Position: </span>{position}</p>
    <p><span>Created At: </span>{new Date(+createdAt).toLocaleString()}</p>
    { position === 'long' 
      ? (
        <>
          <br />
          <p><span>Current Bid Price: </span>{(+bidPrice).toFixed(4)}</p>
          <p><span>Last Refreshed: </span>{lastRefreshed}</p>
          <p><span>Current Pip Difference: </span>{pipDifLong}</p>
          <p><span>Potential PL: </span>
            {potentialProfitLoss.toLocaleString()}.00
          </p>
        </> ) 
      : (
        <>
          <br />
          <p><span>Current Ask Price: </span>{(+askPrice).toFixed(4)}</p>
          <p><span>Last Refreshed: </span>{lastRefreshed}</p>
          <p><span>Current Pip Difference: </span>{pipDifShort}</p>
          <p><span>Potential PL: </span>
            {potentialProfitLoss.toLocaleString()}.00
          </p>
        </>
      )
    }
  </div>
)

export default PairDetails
Enter fullscreen mode Exit fullscreen mode

We display the open positions data. We also display the current askPrice/bidPrice and the potentialProfitLoss that closing the position would provide.

Import Pair.js into App.js and create its Route.

// App.js

import React from 'react'
import { Route, Switch } from 'react-router-dom'

import Landing from './pages/Landing'
import Navbar from './components/navbar/Navbar'
import Register from './components/auth/Register'
import Login from './components/auth/Login'
import Account from './pages/Account'
import Pair from './pages/Pair'

const App = () => (
  <Switch>
    <Route path='/login' component={ Login } />
    <Route path='/' render={() => (
      <main>
        <div className='navbar'><Navbar /></div>
        <Route exact path='/' component={ Landing } />
        <Route path='/register' component={ Register } />
        <Route path='/account' component={ Account } />
        <Route path='/pair' component={ Pair } />
      </main>
    )} />
  </Switch>
)

export default App
Enter fullscreen mode Exit fullscreen mode

If you navigate to /account as a result of opening a new position, you should see the following:

account

Click on an open pair and take a good gander at the browser.

pair details

And with that a user can close positions. Best we don't just rest on our laurels. Time to implement our chart! We'll start with the query. In graphql/queries create a new file and name it monthlyTimeSeries.js. Insert the following:

// graphql/queries/monthlyTimeSeries.js

import gql from 'graphql-tag' 

export const MONTHLYTIMESERIES = gql`
  query MonthlyTimeSeries($fc: String, $tc: String) {
    monthlyTimeSeries(fc: $fc, tc: $tc) {
      timesArray
      valuesArray
    }
  }
`
Enter fullscreen mode Exit fullscreen mode

In the pages folder create a new file named Chart.js and add the below code:

// Chart.js

import React, { useState } from 'react'
import { Line } from 'react-chartjs-2'
import { useQuery } from '@apollo/react-hooks'

import { MONTHLYTIMESERIES } from '../graphql/queries/monthlyTimeSeries'

export default function Chart() {
  const [ fc, setFc ] = useState('EUR'),
        [ tc, setTc ] = useState('USD'), 
        [ fromCurrency, setFromCurrency ] = useState('EUR'), 
        [ toCurrency, setToCurrency ] = useState('USD'),
        { data, error, loading, refetch } = useQuery(MONTHLYTIMESERIES, {
          variables: { fc, tc }
        })

  if(loading) return <p>loading...</p>
  if(error) return <button onClick={() => {
    refetch({ fc: 'EUR', tc: 'USD' })
    window.location.href = '/chart'
  }}>retry</button>

  const labels = data && data.monthlyTimeSeries.timesArray,
        chartData = data && data.monthlyTimeSeries.valuesArray

  return (
    <div className='chartData'>
      <form onSubmit={e => {
        e.preventDefault()
        setFc(fromCurrency)
        setTc(toCurrency) 
      }}>
        <input 
          name='fromCurrency'
          value={fromCurrency}
          placeholder='From Currency'
          onChange={e => setFromCurrency(e.target.value.toUpperCase())}
        />
        <input 
          name='toCurrency'
          value={toCurrency}
          placeholder='To Currency'
          onChange={e => setToCurrency(e.target.value.toUpperCase())}
        />
        <button>submit</button>
      </form>
      <Line data={{
        labels,
        datasets: [
          {
            label: `${fc}/${tc} Time Series FX (Monthly)`,
            fill: true,
            lineTension: 0.1,
            backgroundColor: 'rgb(55, 131, 194)',
            borderColor: 'white',
            borderCapStyle: 'butt',
            borderDash: [],
            borderDashOffset: 0.0,
            borderJoinStyle: 'miter',
            pointBorderColor: 'white',
            pointBackgroundColor: '#fff',
            pointBorderWidth: 1,
            pointHoverRadius: 5,
            pointHoverBackgroundColor: 'white',
            pointHoverBorderColor: 'rgba(220,220,220,1)',
            pointHoverBorderWidth: 2,
            pointRadius: 1,
            pointHitRadius: 10,
            data: chartData
          }
        ]
      }} />
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

We use our monthlyTimeSeries query to fetch our chart data. We provide a couple inputs so that the user can choose which currency pair they'd like to investigate. If the user enters an incorrect pair we present them with a refresh button. The refetch function accepts arguments to its associated query. onClick we use this function to display EUR/USD again. We feed the Line component that we get curtesy of react-chartjs-2 the two arrays from our query: labels and chartData. Finally, we add some styling and return our chart.

We'll need to import Chart.js into App.js and give it a path in Navbar.js. Let's start with App.js:

// App.js

import React from 'react'
import { Route, Switch } from 'react-router-dom'

import Landing from './pages/Landing'
import Navbar from './components/navbar/Navbar'
import Register from './components/auth/Register'
import Login from './components/auth/Login'
import Account from './pages/Account'
import Pair from './pages/Pair'
import Chart from './pages/Chart'

const App = () => (
  <Switch>
    <Route path='/login' component={ Login } />
    <Route path='/' render={() => (
      <main>
        <div className='navbar'><Navbar /></div>
        <Route exact path='/' component={ Landing } />
        <Route path='/register' component={ Register } />
        <Route path='/account' component={ Account } />
        <Route path='/pair' component={ Pair } />
        <Route path='/chart' component={ Chart } />
      </main>
    )} />
  </Switch>
)

export default App
Enter fullscreen mode Exit fullscreen mode

Navbar.js:

// Navbar.js

import React from 'react'
import { NavLink, Redirect } from 'react-router-dom'
import { useQuery } from '@apollo/react-hooks'

import { MEQUERY } from '../../graphql/queries/me'
import Logout from '../auth/Logout'
import './Navbar.css'

const Navbar = () => {
  const { data, loading, error } = useQuery(MEQUERY)

  if(loading) return <p>Loading....</p>
  if(error) return <Redirect to='/login' />
  if(!data) return <p>This is unfortunate</p>

  return (
    <div className='navigation'>
      <header><NavLink exact to='/'>Forex</NavLink></header>
      { !data.me ? (
        <ul>
          <li><NavLink exact to='/login'>Login</NavLink></li>
          <li><NavLink exact to='/register'>SignUp</NavLink></li>
        </ul> ) 
      : (
        <ul>
          <li><NavLink to='/chart'>Chart</NavLink></li>
          <li><NavLink to='/account'>Account</NavLink></li>
          <li><Logout /></li>
        </ul>
      )}
    </div>
  )
}

export default Navbar
Enter fullscreen mode Exit fullscreen mode

Once you save your files our app will be complete and should resemble the video below:

forex demo

You'll notice that the chart is fully responsive and not so bad on the old spectacles.

BEHOLD! We've created a currency exchange and hopefully learned a little something along the way. I know I did.


Reach out: Twitter | Medium | GitHub

Top comments (1)

Collapse
 
benjamin6kruger profile image
Benjamin6Kruger • Edited

Very interesting information, thank you! I've been trading on the forex market for a lot of time, you can say that I'm not a beginner, but finding a good broker is always a problem. Therefore, I try to try everything and a little bit to find my own. It seems to me that just recently I found a great broker where I can instant commission affiliate program forex and get good money. I am very happy!