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 .
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
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')
)
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
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
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
}
}
`
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
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
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
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
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
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);
}
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;
}
These two files represent the entirety of our CSS. Save your files and take a look at the browser.
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)
}
`
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>
)
}
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
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
}
}
`
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
}
}
`
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>
)
}
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
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
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
}
`
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)
When a user clicks Logout
three things happen:
The
logout
mutation destroys the session on thereq
Object.We update the cache such that the
me
query returnsnull
.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
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
}
}
}
`
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
}
}
`
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
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
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}
/>)}
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
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
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>
)
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>
)
}
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
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
}
}
`
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>
)}
</>
)
}
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>
)
}
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
}
}
}
`
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>
)
}
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.
For a mini lot (10,000 units) we do the same but every pip movement equates to 1 currency unit profit/loss.
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>
)}
</>
)
}
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
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
If you navigate to /account as a result of opening a new position, you should see the following:
Click on an open pair and take a good gander at the browser.
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
}
}
`
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>
)
}
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
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
Once you save your files our app will be complete and should resemble the video below:
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.
Top comments (1)
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!