Welcome to Decktopia

Welcome to Decktopia!

In this post I hope to demonstrate my understanding/application of React development fundamentals by employment of the "GARP Stack" (GARP isn't really a "thing", but we love acronyms don't we?).

GARP, eh?

A little Background



What is Magic the Gathering

Magic the gathering is a fantasy trading card game that was born in 1993 and continues to this day. In it's most simple form, two players face off against each other with custom built decks of around 60 cards, with the intention to bring the opposing players life total to zero, from a starting total of 20, through various card interactions.

Why make this application?

Previously, Magic the Gathering was confined to physical cards, but recently, the makers of Magic have released an online version of the game called Magic Arena.

The digital format introduces a problem: deck building. Given a digital collection, the player is not bound by the having a limited number of cards to combine. Digitally, you can combine your collection into an infinite number of 60 card decks.

There are already some pretty fancy applications that exist to do this, but their access/membership is open to the world and they tend to contain too many bells and whistles which convolute the experience.

In making my own application, I hope to cut away unnecessary functionality as well as support smaller, curated membership.
The application I am working on is will allow users to:
  • Signup and authenticate using Jason Web Token.
  • Create Deck to share with other users, view other users created decks for inspiration and variety, and upvote their favorites.
  • Copy decks they like to the clipboard in a format suitable for import into Magic Arena.

I intend to add a lot more functionality (messaging, upvoting decks, tournament brackets...), and, in doing so, hone my skills as a developer

Visit the rudimentary deployment:


Be advised, the application is still very much in it's early stage and styling has not been prioritized. At present, I'm focused on getting routing, data querying/display, and authentication on solid footing.

Back End Repo

Front End Repo

Deployed Application


You can login with the following credentials or create your own account.
email: candy
password: candy
Note, there is currently no validation on the email so it can be any text you would like, it doesn't have to be your email.
When you 'export' a deck from the Magic Arena game, the following format is copied to your clipboard. The "addDeck" route on my server is equipped to handle this format of input via regex operations. You can check out the logic here:

Add Deck Resolver Source Code

If you would like to try the 'add a deck' feature on the "/add" route, you will need to use valid input for the deck list, you can use any number of entries (rows) from the following:

4 Overgrown Tomb (GRN) 253
1 Island (RIX) 193
4 Drowned Catacomb (XLN) 253
4 Watery Grave (GRN) 259
2 Forest (RIX) 196
4 Breeding Pool (RNA) 246
4 Woodland Cemetery (DAR) 248
1 Jadelight Ranger (RIX) 136
1 Assassin's Trophy (GRN) 152
2 Cast Down (DAR) 81
2 Vraska's Contempt (XLN) 129
4 Hydroid Krasis (RNA) 183
3 Hostage Taker (XLN) 223
1 Frilled Mystic (RNA) 174
2 Find // Finality (GRN) 225
4 Incubation Druid (RNA) 131
3 Thought Erasure (GRN) 206
3 Vivien Reid (M19) 208
4 Thief of Sanity (GRN) 205
4 Llanowar Elves (DAR) 168
1 Vraska, Relic Seeker (XLN) 232
2 Hinterland Harbor (DAR) 240


Stack Technicalities




Step 1: Define Data-model

The Prisma client is what interacts with your database, in my case, it's a MySQL database, but there are others to choose from. Once you define your data-model, Prisma takes care of preparing your tables and setting up the relations between data. It can be visualized:

[Client (React app)] --> [GraphQL Server (node)] --> [Prisma Server] --> [Databaes (MySQL)]

Please note, the data-model shown below is simplified from what you will find in my server repo. I have many features I want to implement, but in order to explain how Prisma works concisely, only a few are shown here:
type User {
  id: ID! @unique
  email: String! @unique
  name: String!
  password: String!
  decks: [Deck!]!
  votes: [Vote!]!
  createdAt: DateTime!
  updatedAt: DateTime!
}

type Deck {
  id: ID! @unique
  author: User!
  deckList: String!
  deckDetails: String!
  deckName: String! @default(value: "default deck name")
  score: Int! @default(value: "0" )
}

type Vote {
  id: ID! @unique
  deck: Deck!
  author: User!
  quality: Boolean!
}


Things to note:
  • type User has a "decks" property, which is an array of Decks.
  • type Deck has an "author" propety which is of type User
  • The @unique directive makes it so, when a user registers, no repeat emails with throw an error.

Voila! A relation between two data types is set!
Once you have defined your datamodel in the datamodel.prisma file, you run the prisma deploy command via the prisma-cli.

prisma deploy triggers the prisma-cli to update the tables and relations in your database, as well as generate the 'prisma client' utility function, which, due to magic of introspection, knows all the possible CRUD functionality that you could ever want to perform on your database.

Step 2: Create the GraphQL Server

require('dotenv').config()
const { prisma } = require('../generated/prisma-client')
const { GraphQLServer } = require('graphql-yoga')
const resolvers = require('./resolvers')

const main = async () => {

  const server = new GraphQLServer({
    typeDefs: 'src/schema.graphql',
    resolvers,
    context: req => ({
      ...req,
      prisma
    }),
  })

  server.start(() => console.log(`
    ##  Server is running on http://localhost:4000  ##
`))

}
main();

Things to note:
  • While the prisma client exposes every single possible operation you might perform on the data, it is the graphql server that allows you to expose only the functionality you choose.
  • You specify the exposed functionality in schema.grapqhql, and pass that to the server. You can think of the schema.graphql as your "routes".
  • We also pass the resolvers to the server, which contain the logic that gets executed when any given route is accessed, i.e. create a user, validate authentication, etc.
  • Lastly, the request is passed to the server's context and is available in any of the resolvers. We also pass prisma to the context to assist in database manipulation.


userSignup Mutation Resolver

const bcrypt = require('bcryptjs')
const jwt = require('jsonwebtoken')
const { getUserId, clearLog } = require('../../utils')

function createToken(userId) {
  return jwt.sign({ userId, expiresIn: "7d" }, process.env.APP_SECRET)
}

async function userSignup (parent, args, ctx) {

    const password = await bcrypt.hash(args.password, 5)

    const user = await ctx.prisma.createUser({ ...args, password })

    const token = createToken(user.id)

    return {
      token,
      user,
    }
};

module.exports = {
  userSignup,
}
Things to note:
  • Each resolver receives an args argument, which contains all the variable info included in the request from the front end.
  • We first hash the password so it is encrypted on in the database.
  • Next, we leverage the auto-generated prisma object we passed into the context when instantiating the server. This creates the User entry into the databases User table and returns a User.
  • Lastly, we return (to the client, i.e. React application) the User object along with a JWT auth token.

Note:

What we return from a resolver must be consistent with what is specified in schema.graphql. The userSignup mutation expects to return a UserAuthPayload type, which is defined in schema.graphql.
type Mutation {
  userSignup(
    name: String!
    email: String!
    isAdmin: Boolean!
    password: String!
    ): UserAuthPayload!
}

type UserAuthPayload {
  token: String!
  user: User!
}

Authenticating Requests

When a User logs into the React application, they receive a JWT which is saved to the session storage. This JWT is sent along with every request to the GraphQL server and authentication takes place as demonstrated in the "me" query resolver below, which returns a specific User's information to be displayed, or, if the JWT is invalid, an error.

const bcrypt = require('bcryptjs')
const jwt = require('jsonwebtoken')

function getUserId(ctx) {
  let token = '';
  const Authorization = ctx.request.get('Authorization')
  token = Authorization.replace('Bearer ', '')

  if (token) {
    const { userId } = jwt.verify(token, process.env.APP_SECRET)
    return userId
  }

  throw new AuthError()
}

async function me(parent, args, ctx) {

  const id = getUserId(ctx)

  const meUser = await ctx.prisma.user({ id })

  return meUser
};

module.exports = {
  me,
}


Moving on To The Front-End React App

The React application interacts with the GraphQL Server via Apollo Client.

First, we configure the Apollo Client. Most of the config is boilerplate, the most important part of the config/apolloClient.js file is the authLink, which where the JWT is added to the requests Headers to be validated by the server before responding.
import { InMemoryCache } from "apollo-cache-inmemory";
import { ApolloClient } from "apollo-client";
import { ApolloLink } from "apollo-link";
import { HttpLink } from "apollo-link-http";
import { setContext } from "apollo-link-context";
import { onError } from "apollo-link-error";

const cache = new InMemoryCache();

const tempMeToken = "nope";

const errorLink = onError(({ graphQLErrors, networkError, operation }) => {
  if (graphQLErrors) {
    graphQLErrors.forEach(({ message, path }) =>
      console.log(`[GraphQL error]: Message: ${message}, Path: ${path}`)
    );
  }

  if (networkError) {
    console.log(
      `[Network error ${operation.operationName}]: ${networkError.message}`
    );
  }
});

const authLink = setContext((_, { headers }) => {
  const myToken = sessionStorage.getItem("bumtoken") || tempMeToken;

  const context = {
    headers: {
      ...headers,
      authorization: `Bearer ${myToken}`
    }
  };
  return context;
});

const httpLink = new HttpLink({ uri: process.env.REACT_APP_GRAPHQL_SERVER });

const client = new ApolloClient({
  link: ApolloLink.from([errorLink, authLink, httpLink]),
  cache
});

export default client;

Next, in App.js we need to wrap the component tree with the provider for Apollo Client, which works very similarly to Redux's provider.

Since I'm using React Router, Material-Ui, Redux, and a new library react-apollo-hooks that allows you to interact with the Apollo Client following the new React hooks pattern, all of those providers wrap the component tree as well.
import React, { Component } from "react";
import { ApolloProvider } from "react-apollo";
import { ApolloProvider as ApolloProviderHooks } from "react-apollo-hooks";
import { BrowserRouter } from "react-router-dom";
// redux
import { createStore } from "redux";
import { Provider as ReduxProvider } from "react-redux";
import rootReducer from "./store/reducers";
// locals
import apolloClient from "./config/apolloClient";
import Routes from "./config/Routes";
import Layout from "./layout/Layout";
// material ui theme
import { MuiThemeProvider, createMuiTheme } from "@material-ui/core/styles";

const store = createStore(
  rootReducer,
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);

const theme = createMuiTheme({
  palette: {
    type: "dark"
  }
});

const MyLayout = () => {
  return (
    <MuiThemeProvider theme={theme}>
      <Layout>
        <Routes />
      </Layout>
    </MuiThemeProvider>
  );
};

class App extends Component {
  render() {
    return (
      <ReduxProvider store={store}>
        <ApolloProvider client={apolloClient}>
          <ApolloProviderHooks client={apolloClient}>
            <BrowserRouter>
              <MyLayout />
            </BrowserRouter>
          </ApolloProviderHooks>
        </ApolloProvider>
      </ReduxProvider>
    );
  }
}

export default App;

In the comps/Login.js file, I used the new React hooks api to execute a login 'mutation'. Then, if successful, the inside the update callback, the JWT is set to session storage and the user is routed to the home page. A conditional error message is displayed in the event of an unsuccessful login.
import React, { useState } from "react";
import { useMutation } from "react-apollo-hooks";
// redux
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import { setAuthTrue } from "../store/actions/auth";
// graphql
import LOGIN_MUTATION from "../graphql/m/LOGIN_MUTATION";
// material ui
import TextField from "@material-ui/core/TextField";
import Button from "@material-ui/core/Button";

function Login(props) {
  const { setAuthTrueAction } = props;

  const setToken = token => {
    sessionStorage.setItem("bumtoken", token);
  };

  const [values, setValues] = useState({
    email: "b",
    password: "b",
    didLoginFail: false
  });

  const { email, password, didLoginFail } = values;

  const handleChange = name => event => {
    setValues({ ...values, [name]: event.target.value });
  };

  const loginMutation = useMutation(LOGIN_MUTATION, {
    variables: {
      ...values
    },
    update: async (proxy, result) => {

      const isSuccess = !!result.data.login.payload;

      if (isSuccess) {
        props.history.push("/home");
        setToken(result.data.login.payload.token);
        setAuthTrueAction();
      } else {
        setValues({
          ...values,
          didLoginFail: true
        });
      }
    }
  });

  return (
    <div style={{ width: 500, display: "flex", flexDirection: "column" }}>
      <TextField
        label="Email"
        value={email}
        onChange={handleChange("email")}
        margin="normal"
        variant="filled"
      />
      <TextField
        label="Password"
        value={password}
        onChange={handleChange("password")}
        margin="normal"
        variant="filled"
      />
      <br/>
      <Button variant="outlined" onClick={() => loginMutation()}>
        LOGIN
      </Button>
      {didLoginFail && <h2>incorrect email or password</h2>}
    </div>
  );
}

const mapDispatchToProps = dispatch => {
  return bindActionCreators(
    {
      setAuthTrueAction: setAuthTrue
    },
    dispatch
  );
};

export default connect(
  null,
  mapDispatchToProps
)(Login);

On the Home.js route, you can see how querying data works with Apollo Client. It's very much like Redux in that you wrap the component with a Higher Order Component and the results of the query are fed into the component via props.
The query returns a loading property which you can use to display a 'spinner' while the async data fetch occurs. Then when loading is false the view components can display the data.

import React from "react";
import { graphql, compose } from "react-apollo";
import ALL_DECKS_QUERY from "../graphql/q/ALL_DECKS_QUERY";
// material-ui
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import ListItemText from "@material-ui/core/ListItemText";
import Divider from "@material-ui/core/Divider";
import FolderIcon from "@material-ui/icons/Folder";
// utils
import utils from "../utils";
// router
import { withRouter } from "react-router";

function CommunityDecks(props) {
  const viewDeck = (id, deck) => {
    props.history.push(`/view-deck/${id}`, {
      ...deck,
      deckList: JSON.parse(deck.deckList)
    });
  };

  const {
    allDecksQuery: { loading }
  } = props;

  if (loading) return <h1>Loading...</h1>;

  if (!props.allDecksQuery.allDecks) {
    return <h1>Something went wrong</h1>;
  }

  const {
    allDecksQuery: { allDecks }
  } = props;

  const { truncate } = utils;

  return (
    <div style={{ marginRight: 10 }}>
      <List dense={false}>
        {allDecks.map(d => {
          const { deckName, deckDetails, score, id } = d;
          return (
            <div key={id}>
              <ListItem
                onClick={() => {
                  viewDeck(id, d);
                }}
              >
                <ListItemIcon>
                  <FolderIcon />
                </ListItemIcon>
                <ListItemText
                  primary={deckName}
                  secondary={truncate(deckDetails)}
                />
                <ListItemText primary="Score" secondary={score} />
              </ListItem>
              <Divider />
            </div>
          );
        })}
      </List>
    </div>
  );
}

export default compose(
  withRouter,
  graphql(ALL_DECKS_QUERY, {
    name: "allDecksQuery",
    options: {
      pollInterval: 5000
    }
  })
)(CommunityDecks);

As I approach the 630th line of the markdown file for this post, I realize there's just too much to write about everything.

What I would most like to convey is that I understand all the development principles being deployed in both the front end and back end repositories.

Please feel free to have a look and ask me any questions you like.

Links

Back End Repo

Front End Repo

Deployed Application

arrow_back

Previous

Boiler Post

Next

React Use Effect
arrow_forward