Author: Richard Haines

Posted: 16 Jun 2020

take me there

In this article we will create a Jamstack website powered by Gatsby, Netlify Functions, Apollo and FaunaDB. Our site will use the Harry Potter API for its data that will be stored in a FaunaDB database. The data will be accessed using serverless functions and Apollo. Finally we will display our data in a Gatsby site styled using Theme-ui.

This finished site will look a little something like this: serverless-graphql-potter.netlify.app/

We will begin by focusing on what these technologies are and why, as frontend developers, we should be leveraging them. We will then begin our project and create our schema.

The Jamstack

Jamstack is a term often used to describe sites that are served as static assets to a CDN, of course this is nothing new, anyone who has made a simple site with HTML and CSS and published it has served a static site. To walk away thinking that the only purpose of Jamstack sites are to serve static files would be doing it a great injustice and miss some of the awesome things this "new" way of building web apps provides.

A few of the benefits of going Jamstack

  • High security and more secure. Fewer points of attack due to static files and external APIs served over CDN
  • Cheaper hosting and easier scalability with serverless functions
  • Fast! Pre-built assets served from a CDN instead of a server

A popular way of storing the data your site requires, apart from as markdown files, is the use of a headless CMS (Content Management System). These CMSs have adopted the term headless as they don't come with their own frontend that displays the data stored, like Wordpress for example. Instead they are headless, they have no frontend.

A headless CMS can be set up so that once a change to the data is made in the CMS a new build is triggered via a webhook (just one way of doing it, you could trigger rebuilds other ways) and the site will be deployed again with the new data.

As an example we could have some images stored in our CMS that are pulled into our site via a graphql query and shown on our site. If we wanted to change one of our images we could do so via our CMS which would then trigger a new build on publish and the new image would then be visible on our site.

There are many great options to choose from when considering which CMS to use:

  • Netlify CMS
  • Contenful
  • Sanity.io
  • Tina CMS
  • Butter CMS

The potential list is so long i will point you in the direction of a great site that lists most of them headlesscms.org!

For more information and a great overview of what the Jamstack is and some more of its benefits i recommend checking out jamstack.org.

Just because our site is served as static assets, that doesn't mean we cant work in a dynamic way and have the benefits of dynamic data! We wont be diving deep into all of its benefits, but we will be looking at how we can take our static site and make it dynamic by way of taking a serverless approach to handling our data through AWS Lambda functions, which we will use via Netlify and FaunaDB.

Serverless

Back in the old days, long long ago before we spread our stack with jam, we had a website that was a combination of HTML markup, CSS styling and JavaScript. Our website gave our user data to access and manipulate and our data was stored in a database which was hosted on a server. If we hosted this database ourselves we were responsible for keeping it going and maintaining it and all of its stored data. Our database could hold only a certain amount of data which meant that if we were lucky enough to get a lot of traffic it would soon struggle to handle all of the requests coming its way and so our end users might experience some downtime or no data at all.

If we paid for a hosted server then we were paying for the up time even when no requests were being sent.

To counter these issues serverless computing was introduced. Now, lets cut through all the magic this might imply and simply state that serverless still involves servers, the big difference is that they are hosted in the cloud and execute some code for us.

Providing the requested resources as a simple function they only run when that request is made. This means that we are only charged for the resources and time the code is running for. With this approach we have done away with the need to pay a server provider for constant up time, which is one of the big plus points of going serverless.

Being able to scale up and down is also a major benefit of using serverless functions to interact with our data stores. In a nutshell this means that as multiple requests come in via our serverless functions, our cloud provider can create multiple instances of the same function to handle those requests and run them in parallel. One downside to this is the concept of cold starts where because our functions are spun up on demand they need a small amount of time to start up which can delay our response. However, once up if multiple requests are received our serverless functions will stay open to requests and handle them before closing down again.

FaunaDB

FaunaDB is a global serverless database that has native graphql support, is multi tenancy which allows us to have nested databases and is low latency from any location. Its also one of the only serverless databases to follow the ACID transactions which guarantee consistent reads and writes to the database.

Fauna also provides us with a High Availability solution with each server globally located containing a partition of our database, replicating our data asynchronously with each request with a copy of our database or the transaction made.

Some of the benefits to using Fauna can be summarized as:

  • Transactional
  • Multi-document
  • Geo-distributed

In short, Fauna frees the developer from worry about single or multi-document solutions. Guarantees consistent data without burdening the developer on how to model their system to avoid consistency issues. To get a good overview of how Fauna does this see this blog post about the FaunaDB distributed transaction protocol.

There are a few other alternatives that one could choose instead of using Fauna such as:

  • Firebase
  • Cassandra
  • MongoDB

But these options don't give us the ACID guarantees that Fauna does, compromising scaling.

ACID

  • Atomic - all transactions are a single unit of truth, either they all pass or none. If we have multiple transactions in the same request then either both are good or neither are, one cannot fail and the other succeed.
  • Consistent - A transaction can only bring the database from one valid state to another, that is, any data written to the database must follow the rules set out by the database, this ensures that all transactions are legal.
  • Isolation - When a transaction is made or created, concurrent transactions leave the state of the database the same as is they would be if each request was made sequentially.
  • Durability - Any transaction that is made and committed to the database is persisted in the the database, regardless of down time of the system or failure.

Now that we have a good overview of the stack we will be using lets get to the code!

Setup project

We'll create a new folder to house our project, initialize it with yarn and add some files and folders to that we will be working with throughout.

At the projects root create a functions folder with a nested graphql folder. In that folder we will create three files, our graphql schema which we will import into Fauna, our serverless function which will live in graphql.js and create the link to and use the schema from Fauna and our database connection to Fauna.

1mkdir harry-potter
2cd harry-potter
3yarn init- y
4mkdir src/pages/
5cd src/pages && touch index.js
6mkdir src/components
7touch gatsby-config.js
8touch gatsby-browser.js
9touch gatsby-ssr.js
10touch .gitignore
11
12mkdir functions/graphql
13cd functions/graphql && touch schema.gql graphql.js db-connection.js

We'll also need to add some packages.

1yarn add gatsby react react-dom theme-ui gatsby-plugin-theme-ui faunadb isomorphic-fetch dotenv

Add the following to your newly created .gitignore file:

1.netlify
2node_modules
3.cache
4public

Serverless setup

Lets begin with our schema. We are going to take advantage of an awesome feature of Fauna. By creating our schema and importing it into Fauna we are letting it take care of a lot of code for us by auto creating all the classes, indexes and possible resolvers.

schema.gql

1type Query {
2 allCharacters: [Character]!
3 allSpells: [Spell]!
4 }
5
6 type Character {
7 name: String!
8 house: String
9 patronus: String
10 bloodStatus: String
11 role: String
12 school: String
13 deathEater: Boolean
14 dumbledoresArmy: Boolean
15 orderOfThePheonix: Boolean
16 ministryOfMagic: Boolean
17 alias: String
18 wand: String
19 boggart: String
20 animagus: String
21 }
22
23 type Spell {
24 effect: String
25 spell: String
26 type: String
27 }

Our schema is defining the shape of the data that we will soon be seeding into the data from the Potter API. Our top level query will return two things, an array of Characters and an array of Spells. We have then defined our Character and Spell types. We don't need to specify an id here as when we seed the data from the Potter API we will attach it then.

Now that we have our schema we can import it into Fauna. Head to your fauna console and navigate to the graphql tab on the left, click import schema and find the file we just created, click import and prepare to be amazed!

Once the import is complete we will be presented with a graphql playground where we can run queries against our newly created database using its schema. Alas, we have yet to add any data, but you can check the collections and indexes tabs on the left of the console and see that fauna has created two new collections for us, Character and Spell.

A collection is a grouping of our data with each piece of data being a document. Or a table with rows if you are coming from an SQL background. Click the indexes tab to see our two new query indexes that we specified in our schema, allCharacters and allSpells. db-connection.js

Inside db-connection.js we will create the Fauna client connection, we will use this connection to seed data into our database.

1require("dotenv").config();
2const faunadb = require("faunadb");
3const query = faunadb.query;
4
5function createClient() {
6 if (!process.env.FAUNA_ADMIN) {
7 throw new Error(`No FAUNA_ADMIN key in found, please check your fauna dashboard or create a new key.`);
8 }
9 const client = new faunadb.Client({
10 secret: process.env.FAUNA_ADMIN
11 });
12 return client;
13}
14exports.client = createClient();
15exports.query = query;

Here we are creating a function which will check to see if we have an admin key from our Fauna database, if none is found we are returning a helpful error message to the console. If the key is found we are creating a connection to our Fauna database and exporting that connection from file. We are also exporting the query variable from Fauna as that will allow us to use some FQL (Fauna Query Language) when seeding our data.

Head over to your Fauna console and click the security tab, click new key and select admin from the role dropdown. The admin role will allow us to manage the database, in our case, seed data into it. Choose the name FAUNA_ADMIN and hit save. We will need to create another key for use in using our stored schema from Fauna. Select server for the role of this key and name it SERVER_KEY. Don't forget to make a note of the keys before you close the windows as you wont be able to view them again!

That’s a great start. Next up we will seed our data and begin implementing our frontend!

Now that we have our keys its time to grab one more, from the Potter API, it's as simple as hitting the get key button in the top right hand corner of the page, make a note of it and head back to your code editor.

We don't want our keys getting into the wrong wizards hands so lets store them as environment variables. Create a .env file at the projects root and add add them. Also add the .env path to the .gitignore file.

.gitignore

1// ...other stuff
2.env.*

.env

1FAUNA_ADMIN=xxxxxxxxxxxxxxxxxxxxxxxxxxx
2SERVER_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxx
3POTTER_KEY=xxxxxxxxxxxxxxxxxxxxxxxx

Our database isn't much good if it doesn't have any data in it, lets change that! Create a file at the projects root and name it seed.js

1const fetch = require("isomorphic-fetch");
2const { client, query } = require("./functions/graphql/db");
3const q = query;
4const potterEndPoint = `https://www.potterapi.com/v1/characters/?key=${process.env.POTTER_KEY}`;
5
6fetch(potterEndPoint)
7 .then(res => res.json())
8 .then(res => {
9 console.log({ res });
10 const characterArray = res.map((char, index) => ({
11 _id: char._id,
12 name: char.name,
13 house: char.house,
14 patronus: char.patronus,
15 bloodStatus: char.blood,
16 role: char.role,
17 school: char.school,
18 deathEater: char.deathEater,
19 dumbledoresArmy: char.dumbledoresArmy,
20 orderOfThePheonix: char.orderOfThePheonix,
21 ministryOfMagic: char.ministryOfMagic,
22 alias: char.alias,
23 wand: char.wand,
24 boggart: char.boggart,
25 animagus: char.animagus
26 }));
27
28 client
29 .query(
30 q.Map(characterArray, q.Lambda("character", q.Create(q.Collection("Character"), { data: q.Var("character") })))
31 )
32 .then(console.log("Wrote potter characters to FaunaDB"))
33 .catch(err => console.log("Failed to add characters to FaunaDB", err));
34 });

There is quite a lot going on here so lets break it down.

  • We are importing fetch to do a post against the potter endpoint
  • We import our Fauna client connection and the query variable which holds the functions need to create the documents in our collection.
  • We call the potter endpoint and map over the result, adding all the data we require (which also corresponds to the schema we create earlier).
  • Using our Fauna client we use FQL to first map over the new array of characters, we then call a lambda function (an anonymous function) and choose a variable name for each row instance and create a new document in our Character collection.
  • If all was successful we return a message to the console, if unsuccessful we return the error.

From the projects root run our new script.

1node seed.js

If you now take a look inside the collections tab in the Fauna console you will see that the database has populated with all the characters from the potterverse! Click on one of the rows (documents) and you can see the data.

We will create another seed script to get our spells data into our database. Run the script and check out the Spell collections tab to view all the spells.

1const fetch = require("isomorphic-fetch");
2const { client, query } = require("./functions/graphql/db");
3const q = query;
4const potterEndPoint = `https://www.potterapi.com/v1/spells/?key=${process.env.POTTER_KEY}`;
5
6fetch(potterEndPoint)
7 .then(res => res.json())
8 .then(res => {
9 console.log({ res });
10 const spellsArray = res.map((char, index) => ({
11 _id: char._id,
12 effect: char.effect,
13 spell: char.spell,
14 type: char.type
15 }));
16
17 client
18 .query(q.Map(spellsArray, q.Lambda("spell", q.Create(q.Collection("Spell"), { data: q.Var("spell") }))))
19 .then(console.log("Wrote potter spells to FaunaDB"))
20 .catch(err => console.log("Failed to add spells to FaunaDB", err));
21 });
1node seed-spells.js

Now that we have data in our database its time to create our serverless function which will pull in our schema from Fauna.

graphql.js

1require("dotenv").config();
2const { createHttpLink } = require("apollo-link-http");
3const { ApolloServer, makeRemoteExecutableSchema, introspectSchema } = require("apollo-server-micro");
4const fetch = require("isomorphic-fetch");
5
6const link = createHttpLink({
7 uri: "https://graphql.fauna.com/graphql",
8 fetch,
9 headers: {
10 Authorization: `Bearer ${process.env.SERVER_KEY}`
11 }
12});
13
14const schema = makeRemoteExecutableSchema({
15 schema: introspectSchema(link),
16 link
17});
18
19const server = new ApolloServer({
20 schema,
21 introspection: true
22});
23
24exports.handler = server.createHandler({
25 cors: {
26 origin: "*",
27 credentials: true
28 }
29});

Lets go through what we just did.

  • We created a link to Fauna using the createHttpLink function which takes our Fauna graphql endpoint and attaches our server key to the header. This will fetch the graphql results from the endpoint over an http connection.
  • We then grab our schema from Fauna using the makeRemoteExecutableSchema function by passing the link to the introspectSchema function, we also provide the link.
  • A new ApolloServer instance is then created and our schema passed in.
  • Finally we export our handler as Netlify requires us to do when writing serverless functions.
  • Note that we might, and most probably will, run into CORS issues when trying to fetch our data so we pass our createHandler function the cors option, setting its origin to anything and credentials as true.

Using our data!

Before we can think about displaying our data we must first do some tinkering. We will be using some handy hooks from Apollo for querying our (namely useQuery) and for that to work we must first set up our provider, which is similar to Reacts context provider. We will wrap our sites root with this provider and pass in our client, thus making it available throughout our site. To wrap the root element in a Gatsby site we must use the gatsby-browser.js and gatsby-ssr.js files. The implementation will be identical in both.

gatsby-browser.js && gatsby-ssr.js

We will have to add a few more packages at this point:

1yarn add @apollo/client apollo-link-context
1const React = require("react");
2const { ApolloProvider, ApolloClient, InMemoryCache } = require("@apollo/client");
3const { setContext } = require("apollo-link-context");
4const { createHttpLink } = require("apollo-link-http");
5const fetch = require("isomorphic-fetch");
6
7const httpLink = createHttpLink({
8 uri: "https://graphql.fauna.com/graphql",
9 fetch
10});
11
12const authLink = setContext((_, { headers }) => {
13 return {
14 headers: {
15 ...headers,
16 authorization: `Bearer ${process.env.SERVER_KEY}`
17 }
18 };
19});
20
21const client = new ApolloClient({
22 link: authLink.concat(httpLink),
23 cache: new InMemoryCache()
24});
25
26export const wrapRootElement = ({ element }) => <ApolloProvider client={client}>{element}</ApolloProvider>;

There are other ways of setting this up, i had originally just created an ApolloClient instance and passed in the Netlify functions url as a http link then passed that down to the provider but i was encountering authorization issues, with a helpful message stating that the request lacked authorization headers. The solution was to send the authorization along with a header on every http request.

Lets take a look at what we have here:

  • Created a new http link much the same as we did before when creating our server instance.
  • Create an auth link which returns the headers to the context so the http link can read them. Here we pass in our Fauna key with server rights.
  • Then we create the client to be passed to the provider with the link now set as the auth link.

Now that we have the nuts and bolts all setup we can move onto some frontend code!

Make it work then make it pretty!

We'll also want to create some base components. We'll be using a Gatsby layout plugin to make life easier for us. We'll also utilize some google fonts via a plugin. Stay with me...

1mkdir -p src/layouts/index.js
2 cd src/components && touch header.js
3 cd src/components && touch main.js
4 cd src/components && touch footer.js
5 yarn add gatsby-plugin-layout
6 yarn add gatsby-plugin-google-fonts

Now we need to add the theme-ui, layout and google fonts plugins to our gatsby-config.js file:

1module.exports = {
2 plugins: [
3 {
4 resolve: "gatsby-plugin-google-fonts",
5 options: {
6 fonts: ["Muli", "Open Sans", "source sans pro:300,400,400i,700"]
7 }
8 },
9 {
10 resolve: "gatsby-plugin-layout",
11 options: {
12 component: require.resolve("./src/layouts/index.js")
13 }
14 },
15 "gatsby-plugin-theme-ui"
16 ]
17};

We'll begin with our global layout. This will include a css reset and render our header component and any children, which in our case is the rest of the applications pages/components.

1/** @jsx jsx */
2import { jsx } from "theme-ui";
3import React from "react";
4import { Global, css } from "@emotion/core";
5import Header from "./../components/site/header";
6
7const Layout = ({ children, location }) => {
8 return (
9 <>
10 <Global
11 styles={css`
12 * {
13 margin: 0;
14 padding: 0;
15 box-sizing: border-box;
16 scroll-behavior: smooth;
17
18 /* width */
19 ::-webkit-scrollbar {
20 width: 10px;
21 }
22
23 /* Track */
24 ::-webkit-scrollbar-track {
25 background: #fff;
26 border-radius: 20px;
27 }
28
29 /* Handle */
30 ::-webkit-scrollbar-thumb {
31 background: #000;
32 border-radius: 20px;
33 }
34
35 /* Handle on hover */
36 ::-webkit-scrollbar-thumb:hover {
37 background: #000;
38 }
39 }
40 body {
41 scroll-behavior: smooth;
42 overflow-y: scroll;
43 -webkit-overflow-scrolling: touch;
44 width: 100%;
45 overflow-x: hidden;
46 height: 100%;
47 }
48 `}
49 />
50 <Header location={location} />
51 {children}
52 </>
53 );
54};
55
56export default Layout;

Because we are using gatsby-plugin-layout our layout component will be wrapped around all of our pages so that we can skip importing it ourselves. For our site its a trivial step as we could just as easily import it but for more complex layout solutions this can come in real handy.

To provide an easy way to style our whole site through changing just a few variables we can utilize gatsby-plugin-theme-ui.

This article wont cover the specifics of how to use theme-ui, for that i suggest going over another tutorial i have written which covers the hows and whys how-to-make-a-gatsby-ecommerce-theme-part-1/

1cd src && mkdir gatsby-plugin-theme-ui && touch index.js

In this file we will create our sites styles which we will be able to access via the theme-ui sx prop.

1export default {
2 fonts: {
3 body: "Open Sans",
4 heading: "Muli"
5 },
6 fontWeights: {
7 body: 300,
8 heading: 400,
9 bold: 700
10 },
11 lineHeights: {
12 body: "110%",
13 heading: 1.125,
14 tagline: "100px"
15 },
16 letterSpacing: {
17 body: "2px",
18 text: "5px"
19 },
20 colors: {
21 text: "#FFFfff",
22 background: "#121212",
23 primary: "#000010",
24 secondary: "#E7E7E9",
25 secondaryDarker: "#545455",
26 accent: "#DE3C4B"
27 },
28 breakpoints: ["40em", "56em", "64em"]
29};

Much of this is self explanatory, the breakpoints array is used to allow us to add responsive definitions to our inline styles via the sx prop. For example:

1<p
2 sx={{
3 fontSize: ["0.7em", "0.8em", "1em"]
4 }}
5>
6 Some text here...
7</p>

The font size array indexes corresponded to our breakpoints array set in our theme-ui index file. Next we'll create our header component. But before we do we must install another package, i'll explain why once you see the component.

1yarn add @emotion/styled
2 cd src/components
3 mkdir site && touch header.js

header.js

1/** @jsx jsx */
2import { jsx } from "theme-ui";
3import HarryPotterLogo from "../../assets/svg-silhouette-harry-potter-4-transparent.svg.svg";
4import { Link } from "gatsby";
5import styled from "@emotion/styled";
6
7const PageLink = styled(Link)`
8 color: #fff;
9
10 &:hover {
11 background-image: linear-gradient(
12 90deg,
13 rgba(127, 9, 9, 1) 0%,
14 rgba(255, 197, 0, 1) 12%,
15 rgba(238, 225, 23, 1) 24%
16 );
17 background-size: 100%;
18 background-repeat: repeat;
19 -webkit-background-clip: text;
20 -webkit-text-fill-color: transparent;
21 font-weight: bold;
22 }
23`;
24
25const Header = ({ location }) => {
26 return (
27 <section
28 sx={{
29 gridArea: "header",
30 justifyContent: "flex-start",
31 alignItems: "center",
32 width: "100%",
33 height: "100%",
34 display: location.pathname === "/" ? "none" : "flex"
35 }}
36 >
37 <Link to="/">
38 <HarryPotterLogo
39 sx={{
40 height: "100px",
41 width: "100px",
42 padding: "1em"
43 }}
44 />
45 </Link>
46
47 <PageLink
48 sx={{
49 fontFamily: "heading",
50 fontSize: "2em",
51 color: "white",
52 marginRight: "2em"
53 }}
54 to="/houses"
55 >
56 houses
57 </PageLink>
58 <PageLink
59 sx={{
60 fontFamily: "heading",
61 fontSize: "2em",
62 color: "white"
63 }}
64 to="/spells"
65 >
66 Spells
67 </PageLink>
68 </section>
69 );
70};
71
72export default Header;

Lets understand our imports first.

  • We have imported and used the jsx pragma from theme-ui to allow to to style our elements and components inline with the object syntax
  • The HarryPotterLogo is a logo i found via google which was placed in a folder named assets inside of our src folder. Its an svg which we alter the height and width of using the sx prop.
  • Gatsby link is needed for us to navigate between pages in our site.

You may be wondering why we have installed emotion/styled when we could just use the sx prop, like we have done else where... Well the answer lies in the affect we are using on the page links.

The sx prop doesn’t seem to have access to, or i should say perhaps that its doesn't have in its definitions, the -webkit-background-clip property which we are using to add a cool linear-gradient affect on hover. For this reason we have pulled the logic our into a new component called PageLink which is a styled Gatsby Link. With styled components we can use regular css syntax and as such have access to the -webkit-background-clip property.

The header component is taking the location prop provided by @reach/router which Gatsby uses under the hood for its routing. This is used to determine which page we are on. Due to the fact that we have a different layout for our main home page and the rest of the site we simply use the location object to check if we are on the home page, if we are we set a display none to hide the header component.

The last thing we need to do is set our grid areas which we will be using in later pages. This is just my preferred way of doing it, but i like the separation. Create a new folder inside of src called window and add an index.js file.

1export const HousesSpellsPhoneTemplateAreas = `
2 'header'
3 'main'
4 'main'
5 `;
6
7export const HousesSpellsTabletTemplateAreas = `
8 'header header header header'
9 'main main main main'
10 `;
11
12export const HousesSpellsDesktopTemplateAreas = `
13 'header header header header'
14 'main main main main'
15 `;
16
17export const HomePhoneTemplateAreas = `
18 'logo'
19 'logo'
20 'logo'
21 'author'
22 'author'
23 'author'
24 'author'
25 `;
26
27export const HomeTabletTemplateAreas = `
28 'logo . . '
29 'logo author author'
30 'logo author author'
31 '. . . '
32 `;
33
34export const HomeDesktopTemplateAreas = `
35 'logo . . '
36 'logo author author'
37 'logo author author'
38 '. . . '
39 `;

Cool, now we have our global layout complete, lets move onto our home page. Open up the index.js file inside of src/pages and add the following:

1/** @jsx jsx */
2import { jsx } from "theme-ui";
3import React from "react";
4import { HomePhoneTemplateAreas, HomeTabletTemplateAreas, HomeDesktopTemplateAreas } from "./../window/index";
5import LogoSection from "./../components/site/logo-section";
6import AuthorSection from "../components/site/author-section";
7
8export default () => {
9 return (
10 <div
11 sx={{
12 width: "100%",
13 height: "100%",
14 maxWidth: "1200px",
15 margin: "1em"
16 }}
17 >
18 <div
19 sx={{
20 display: "grid",
21 gridTemplateColumns: ["1fr", "500px 1fr", "500px 1fr"],
22 gridAutoRows: "100px 1fr",
23 gridTemplateAreas: [HomePhoneTemplateAreas, HomeTabletTemplateAreas, HomeDesktopTemplateAreas],
24 width: "100%",
25 height: "100vh",
26 background: "#1E2224",
27 maxWidth: "1200px"
28 }}
29 >
30 <LogoSection />
31 <AuthorSection />
32 </div>
33 </div>
34 );
35};

This is the first page our visitors will see. We are using a grid to compose our layout of the page and utilizing the responsive array syntax in our grid-template-columns and areas properties. To recap how this works we can take a closer look at the gridTemplateAreas property and see that the first index is for phone (or mobile if you will) with the second being tablet and the third desktop. We could add more if we so wished but these will suffice for our needs.

Lets move on to creating our logo section. In src/components/site create two new files called logo.js and logo-section.js

logo.js

1/** @jsx jsx */
2import { jsx } from "theme-ui";
3import HarryPotterLogo from "../assets/svg-silhouette-harry-potter-4-transparent.svg.svg";
4export const Logo = () => (
5 <HarryPotterLogo
6 sx={{
7 height: ["200px", "300px", "500px"],
8 width: ["200px", "300px", "500px"],
9 padding: "1em",
10 position: "relative"
11 }}
12 />
13);

Our logo is the Harry Potter svg mentioned earlier. You can of course choose whatever you like as your sites logo. This one is merely “HR” in a fancy font.

logo-section.js

1/** @jsx jsx */
2import { jsx } from "theme-ui";
3import { Logo } from "../logo";
4const LogoSection = () => {
5 return (
6 <section
7 sx={{
8 gridArea: "logo",
9 display: "flex",
10 alignItems: "center",
11 justifyContent: ["start", "center", "center"],
12 position: "relative",
13 width: "100%"
14 }}
15 >
16 <Logo />
17 </section>
18 );
19};
20export default LogoSection;

Next up is our author section which will site next to our logo section Create a new file inside of src/components/site called author-section.js

author-section.js

1/** @jsx jsx */
2import { jsx } from "theme-ui";
3import { Link } from "gatsby";
4import { houseEmoji, spellsEmoji } from "./../../helpers/helpers";
5import styled from "@emotion/styled";
6import { wizardEmoji } from "./../../helpers/helpers";
7
8const InternalLink = styled(Link)`
9 color: #fff;
10 &:hover {
11 background-image: linear-gradient(
12 90deg,
13 rgba(127, 9, 9, 1) 0%,
14 rgba(255, 197, 0, 1) 12%,
15 rgba(238, 225, 23, 1) 24%
16 );
17 background-size: 100%;
18 background-repeat: repeat;
19 -webkit-background-clip: text;
20 -webkit-text-fill-color: transparent;
21 font-weight: bold;
22 }
23`;
24
25const ExternalLink = styled.a`
26 color: #fff;
27 &:hover {
28 background-image: linear-gradient(
29 90deg,
30 rgba(127, 9, 9, 1) 0%,
31 rgba(255, 197, 0, 1) 12%,
32 rgba(238, 225, 23, 1) 24%,
33 rgba(0, 0, 0, 1) 36%,
34 rgba(13, 98, 23, 1) 48%,
35 rgba(170, 170, 170, 1) 60%,
36 rgba(0, 10, 144, 1) 72%,
37 rgba(148, 119, 45, 1) 84%
38 );
39 background-size: 100%;
40 background-repeat: repeat;
41 -webkit-background-clip: text;
42 -webkit-text-fill-color: transparent;
43 font-weight: bold;
44 }
45`;
46
47const AuthorSection = () => {
48 return (
49 <section
50 sx={{
51 gridArea: "author",
52 position: "relative",
53 margin: "0 auto"
54 }}
55 >
56 <h1
57 sx={{
58 fontFamily: "heading",
59 color: "white",
60 letterSpacing: "text",
61 fontSize: ["3em", "3em", "5em"]
62 }}
63 >
64 Serverless Potter
65 </h1>
66 <div
67 sx={{
68 display: "flex",
69 justifyContent: "start",
70 alignItems: "flex-start",
71 width: "300px",
72 marginTop: "3em"
73 }}
74 >
75 <InternalLink
76 sx={{
77 fontFamily: "heading",
78 fontSize: "2.5em",
79 // color: 'white',
80 marginRight: "2em"
81 }}
82 to="/houses"
83 >
84 Houses
85 </InternalLink>
86 <InternalLink
87 sx={{
88 fontFamily: "heading",
89 fontSize: "2.5em",
90 color: "white"
91 }}
92 to="/spells"
93 >
94 Spells
95 </InternalLink>
96 </div>
97 <p
98 sx={{
99 fontFamily: "heading",
100 letterSpacing: "body",
101 fontSize: "2em",
102 color: "white",
103 marginTop: "2em",
104 width: ["300px", "500px", "900px"]
105 }}
106 >
107 This is a site that goes with the tutorial on creating a jamstack site with serverless functions and FaunaDB I
108 decided to use the potter api as i love the world of harry potter {wizardEmoji}
109 </p>
110 <p
111 sx={{
112 fontFamily: "heading",
113 letterSpacing: "body",
114 fontSize: "2em",
115 color: "white",
116 marginTop: "1em",
117 width: ["300px", "500px", "900px"]
118 }}
119 >
120 Built with Gatsby, Netlify functions, Apollo and FaunaDB. Data provided via the Potter API.
121 </p>
122 <p
123 sx={{
124 fontFamily: "heading",
125 letterSpacing: "body",
126 fontSize: "2em",
127 color: "white",
128 marginTop: "1em",
129 width: ["300px", "500px", "900px"]
130 }}
131 >
132 Select <strong>Houses</strong> or <strong>Spells</strong> to begin exploring potter stats!
133 </p>
134 <div
135 sx={{
136 display: "flex",
137 flexDirection: "column"
138 }}
139 >
140 <ExternalLink
141 sx={{
142 fontFamily: "heading",
143 letterSpacing: "body",
144 fontSize: "2em",
145 color: "white",
146 marginTop: "1em",
147 width: ["300px", "500px", "900px"]
148 }}
149 href="your-personal-website"
150 >
151 author: your name here!
152 </ExternalLink>
153 <ExternalLink
154 sx={{
155 fontFamily: "heading",
156 letterSpacing: "body",
157 fontSize: "2em",
158 color: "white",
159 marginTop: "1em",
160 width: "900px"
161 }}
162 href="your-github-repo-for-this-project"
163 >
164 github: the name you gave this project
165 </ExternalLink>
166 </div>
167 </section>
168 );
169};
170export default AuthorSection;

This component outlines what the project is, displays links to the other pages and the projects repository. You can change the text I’ve added, this was just for demo purposes. As you can see, we are again using emotion/styled as we are making use of the -webkit-background-clip property on our cool linear-gradient links. We have two here, one for external links, which uses the a tag, and another for internal link which uses Gatsby Link. Note that you should always use the traditional HTML a tag for external links and the Gatsby Link to configure your internal routing.

You may also notice that there is an import from a helper file what exports some emojis. Lets take a look at that. Create a new folder inside of src.

1cd src
2 mkdir helpers && touch helpers.js

helpers.js

1export const gryffindorColors = "linear-gradient(90deg, rgba(127,9,9,1) 27%, rgba(255,197,0,1) 61%)";
2export const hufflepuffColors = "linear-gradient(90deg, rgba(238,225,23,1) 35%, rgba(0,0,0,1) 93%)";
3export const slytherinColors = "linear-gradient(90deg, rgba(13,98,23,1) 32%, rgba(170,170,170,1) 69%)";
4export const ravenclawColors = "linear-gradient(90deg, rgba(0,10,144,1) 32%, rgba(148,107,45,1) 69%)";
5
6export const houseEmoji = `🏡`;
7export const spellsEmoji = `💫`;
8export const wandEmoji = `💫`;
9export const patronusEmoji = ``;
10export const deathEaterEmoji = `🐍`;
11export const dumbledoresArmyEmoji = `⚔️`;
12export const roleEmoji = `📖`;
13export const bloodStatusEmoji = `🧙🏾‍♀️ 🤵🏾`;
14export const orderOfThePheonixEmoji = `🦄`;
15export const ministryOfMagicEmoji = `📜`;
16export const boggartEmoji = `🕯`;
17export const aliasEmoji = `👨🏼‍🎤`;
18export const wizardEmoji = `🧙🏼‍♂️`;
19export const gryffindorEmoji = `🦁`;
20export const hufflepuffEmoji = `🦡`;
21export const slytherinEmoji = `🐍`;
22export const ravenclawEmoji = `🦅`;
23
24export function checkNull(value) {
25 return value !== null ? value : "unknown";
26}
27export function checkDeathEater(value) {
28 if (value === false) {
29 return "no";
30 }
31 return "undoubtedly";
32}
33export function checkDumbledoresArmy(value) {
34 if (value === false) {
35 return "no";
36 }
37 return `undoubtedly ${wizardEmoji}`;
38}

The emojis were taken from a really cool site called Emoji Clipboard, it lets you search and literally copy paste the emojis! We’ll be using these emojis in our cards to display the characters from Harry Potter. As well as the emojis we have some utility functions that will also be used in the cards. Each house in Harry Potter has a set of colors that sets them apart form the other houses. These we have exported as linear-gradients for later use.

Nice! We are nearly there but we haven’t quite finished yet! Next we will use our data and display it to the user of our site!

We have done quite a bit of setup but haven’t yet had a chance to use our data that we have saved in our Fauna database. Now’s the time to bring in Apollo and put together a page that shows all the characters data for each house. We are also going to implement a simple searchbar to allow the user to search the characters of each house!

Inside src/pages create a new file called houses.js and add the following:

houses.js

1/** @jsx jsx */
2import { jsx } from "theme-ui";
3import React from "react";
4import { gql, useQuery } from "@apollo/client";
5import MainSection from "./../components/site/main-section";
6import { HousesPhoneTemplateAreas, HousesTabletTemplateAreas, HousesDesktopTemplateAreas } from "../window";
7
8const GET_CHARACTERS = gql`
9 query GetCharacters {
10 allCharacters {
11 data {
12 _id
13 name
14 house
15 patronus
16 bloodStatus
17 role
18 school
19 deathEater
20 dumbledoresArmy
21 orderOfThePheonix
22 ministryOfMagic
23 alias
24 wand
25 boggart
26 animagus
27 }
28 }
29 }
30`;
31
32const Houses = () => {
33 const { loading: characterLoading, error: characterError, data: characterData } = useQuery(GET_CHARACTERS);
34 const [selectedHouse, setSelectedHouse] = React.useState([]);
35
36 React.useEffect(() => {
37 const gryffindor =
38 !characterLoading &&
39 !characterError &&
40 characterData.allCharacters.data.filter(char => char.house === "Gryffindor");
41 setSelectedHouse(gryffindor);
42 }, [characterLoading, characterData]);
43
44 const getHouse = house => {
45 switch (house) {
46 case "gryffindor":
47 setSelectedHouse(
48 !characterLoading &&
49 !characterError &&
50 characterData.allCharacters.data.filter(char => char.house === "Gryffindor")
51 );
52 break;
53 case "hufflepuff":
54 setSelectedHouse(
55 !characterLoading &&
56 !characterError &&
57 characterData.allCharacters.data.filter(char => char.house === "Hufflepuff")
58 );
59 break;
60 case "slytherin":
61 setSelectedHouse(
62 !characterLoading &&
63 !characterError &&
64 characterData.allCharacters.data.filter(char => char.house === "Slytherin")
65 );
66 break;
67 case "ravenclaw":
68 setSelectedHouse(
69 !characterLoading &&
70 !characterError &&
71 characterData.allCharacters.data.filter(char => char.house === "Ravenclaw")
72 );
73 break;
74 default:
75 setSelectedHouse(
76 !characterLoading &&
77 !characterError &&
78 characterData.allCharacters.data.filter(char => char.house === "Gryffindor")
79 );
80 break;
81 }
82 };
83 return (
84 <div
85 sx={{
86 gridArea: "main",
87 display: "grid",
88 gridTemplateColumns: "repeat(auto-fit, minmax(250px, auto))",
89 gridAutoRows: "auto",
90 gridTemplateAreas: [
91 HousesSpellsPhoneTemplateAreas,
92 HousesSpellsTabletTemplateAreas,
93 HousesSpellsDesktopTemplateAreas
94 ],
95 width: "100%",
96 height: "100%",
97 position: "relative"
98 }}
99 >
100 <MainSection house={selectedHouse} getHouse={getHouse} />
101 </div>
102 );
103};
104export default Houses;

We are using @apollo/client from which we import gql to construct our graphql query and the useQuery hook which will take care of handling the state of the returned data for us. This handy hook returns three states:

  • loading - Is the data currently loading?
  • error - If there was an error we will get it here
  • data - The requested data

Our page will be handling the currently selected house so we use the React useState hook and initialize it with an empty array on first render. There after we use the useEffect hook to set the initial house as Gryffindor (because Gryffindor is best. Fight me!) The dependency array takes in the loading and data states.

We then have a function which returns a switch statement (I know not everyone likes these but i do and i find that they are simple to read and understand). This function checks the currently selected house and if there are no errors in the query it loads the data from that house into the selected house state array. This function is passed down to another component which uses that data to display the house characters in a grid of cards.

Lets create that component now. Inside src/components/site create a new file called main-section.js

main-section.js

1/** @jsx jsx */
2import { jsx } from "theme-ui";
3import React from "react";
4import Card from "../cards/card";
5import SearchBar from "./searchbar";
6import { useSearchBar } from "./useSearchbar";
7import Loading from "./loading";
8import HouseSection from "./house-section";
9
10const MainSection = React.memo(({ house, getHouse }) => {
11 const { members, handleSearchQuery } = useSearchBar(house);
12
13 return house.length ? (
14 <div
15 sx={{
16 gridArea: "main",
17 height: "100%",
18 position: "relative"
19 }}
20 >
21 <div
22 sx={{
23 color: "white",
24 display: "flex",
25 flexDirection: "column",
26 justifyContent: "center",
27 alignItems: "center",
28 fontFamily: "heading",
29 letterSpacing: "body",
30 fontSize: "2em",
31 position: "relative"
32 }}
33 >
34 <h4>
35 {house[0].house} Members - {house.length}
36 </h4>
37 <SearchBar handleSearchQuery={handleSearchQuery} />
38 <HouseSection getHouse={getHouse} />
39 </div>
40 <section
41 sx={{
42 margin: "0 auto",
43 width: "100%",
44 display: "grid",
45 gridAutoRows: "auto",
46 gridTemplateColumns: "repeat(auto-fill, minmax(auto, 500px))",
47 gap: "1.5em",
48 justifyContent: "space-evenly",
49 marginTop: "1em",
50 position: "relative",
51 height: "100vh"
52 }}
53 >
54 {members.map((char, index) => (
55 <Card key={char._id} index={index} {...char} />
56 ))}
57 </section>
58 </div>
59 ) : (
60 <Loading />
61 );
62});
63export default MainSection;

Our main section is wrapped in memo, which means that React will render the component and memorize the result. If the next time the props are passed in and they are the same, React will use the memorized result and skip re-rendering the component again. This is helpful as our component will be re-rendering a lot as the user changes houses or uses the searchbar, which will will soon create.

In fact, lets do do that now. We will be creating a search bar component and a custom hook to handle the search logic. Inside src/components/site create two new files. searchbar.js and useSearchbar.js

searchbar.js

1/** @jsx jsx */
2import { jsx } from "theme-ui";
3
4const SearchBar = ({ handleSearchQuery }) => {
5 return (
6 <div
7 sx={{
8 display: "flex",
9 justifyContent: "center",
10 alignItems: "center",
11 margin: "2em"
12 }}
13 >
14 <input
15 sx={{
16 color: "greyBlack",
17 fontFamily: "heading",
18 fontSize: "0.8em",
19 fontWeight: "bold",
20 letterSpacing: "body",
21 border: "1px solid",
22 borderColor: "accent",
23 width: "300px",
24 height: "50px",
25 padding: "0.4em"
26 }}
27 type="text"
28 id="members-searchbar"
29 placeholder="Search members.."
30 onChange={handleSearchQuery}
31 />
32 </div>
33 );
34};
35export default SearchBar;

Our searchbar takes in a search query function which is called when the input is used. The rest is just styling.

useSearchbar.js

1import React from "react";
2export const useSearchBar = data => {
3 const emptyQuery = "";
4 const [searchQuery, setSearchQuery] = React.useState({
5 filteredData: [],
6 query: emptyQuery
7 });
8
9 const handleSearchQuery = e => {
10 const query = e.target.value;
11 const members = data || [];
12
13 const filteredData = members.filter(member => {
14 return member.name.toLowerCase().includes(query.toLowerCase());
15 });
16
17 setSearchQuery({ filteredData, query });
18 };
19
20 const { filteredData, query } = searchQuery;
21 const hasSearchResult = filteredData && query !== emptyQuery;
22 const members = hasSearchResult ? filteredData : data;
23
24 return { members, handleSearchQuery };
25};

Our custom hook takes the selected house data as a prop. It has an internal state which holds an emptyQuery variable which we set to empty string and a filteredData array, set to empty. The function that runs in our searchbar is the following function declared in the hook. It takes the query as an event from the input, checks if the data provided to the hook has data or sets it to an empty array as a new variable called members. It then filters over the members array and checks if the query matches one of the characters names. Finally it sets the state with the returned filtered data and query.

We structure the state and create a new variable which checks if the state had any data or not. Finally returning the data, be that empty or not and the search function.

Phew! That was a lot to go over. Going back to our main section we can see that we are importing our new hook and passing in the selected house data, then destructing the members and search query function. The component checks if the house array has any length, if it does it returns the page. The page displays the current house, how many members the house has, the searchbar (which takes the search query function as a prop), a new house section which we will build and maps over the members returned from our custom hook.

In the house section we will make use of a super amazing library called Framer Motion. Lets first see how our new component looks and what it does.

In src/components/site create a new file called house-section.js

house-section.js

1/** @jsx jsx */
2import { jsx } from "theme-ui";
3import { gryffindorColors, hufflepuffColors, slytherinColors, ravenclawColors } from "./../../helpers/helpers";
4import styled from "@emotion/styled";
5import { motion } from "framer-motion";
6
7const House = styled.a`
8 color: #fff;
9 &:hover {
10 background-image: ${props => props.house};
11 background-size: 100%;
12 background-repeat: repeat;
13 -webkit-background-clip: text;
14 -webkit-text-fill-color: transparent;
15 font-weight: bold;
16 }
17`;
18
19const HouseSection = ({ getHouse }) => {
20 return (
21 <section
22 sx={{
23 width: "100%",
24 position: "relative"
25 }}
26 >
27 <ul
28 sx={{
29 listStyle: "none",
30 cursor: "crosshair",
31 fontFamily: "heading",
32 fontSize: "1em",
33 display: "flex",
34 flexDirection: ["column", "row", "row"],
35 alignItems: "center",
36 justifyContent: "space-evenly",
37 position: "relative"
38 }}
39 >
40 <motion.li
41 initial={{ scale: 0 }}
42 animate={{ scale: 1 }}
43 transition={{
44 type: "spring",
45 stiffness: 200,
46 damping: 20,
47 delay: 0.2
48 }}
49 >
50 <House onClick={() => getHouse("gryffindor")} house={gryffindorColors}>
51 Gryffindor
52 </House>
53 </motion.li>
54 <motion.li
55 initial={{ scale: 0 }}
56 animate={{ scale: 1 }}
57 transition={{
58 type: "spring",
59 stiffness: 200,
60 damping: 20,
61 delay: 0.4
62 }}
63 >
64 <House onClick={() => getHouse("hufflepuff")} house={hufflepuffColors}>
65 Hufflepuff
66 </House>
67 </motion.li>
68 <motion.li
69 initial={{ scale: 0 }}
70 animate={{ scale: 1 }}
71 transition={{
72 type: "spring",
73 stiffness: 200,
74 damping: 20,
75 delay: 0.6
76 }}
77 >
78 <House onClick={() => getHouse("slytherin")} house={slytherinColors}>
79 Slytherin
80 </House>
81 </motion.li>
82 <motion.li
83 initial={{ scale: 0 }}
84 animate={{ scale: 1 }}
85 transition={{
86 type: "spring",
87 stiffness: 200,
88 damping: 20,
89 delay: 0.8
90 }}
91 >
92 <House onClick={() => getHouse("ravenclaw")} house={ravenclawColors}>
93 Ravenclaw
94 </House>
95 </motion.li>
96 </ul>
97 </section>
98 );
99};
100export default HouseSection;

The purpose of this component is to show the user the four houses of Hogwarts, let them select a house and pass that selection back up to the main-section state. The component takes the getHouse function from main-section as a prop. We have created an internal link styled component , which takes each houses colours from our helper file, and returns the selected house on click.

Using framer motion we prepend each li with the motion tag. This allows us to add a simple scale animation by setting the initial value 0 (so it’s not visible), using the animate prop we say that it should animate in to it’s set size. The transition is specifying how the animation will work.

Back to the main-section component, we map over each member in the house and display their data in a Card component by spreading all the character data. Lets create that now.

Inside src/components/site create a new file called card.js

card.js

1/** @jsx jsx */
2import { jsx } from "theme-ui";
3import {
4 checkNull,
5 checkDeathEater,
6 checkDumbledoresArmy,
7 hufflepuffColors,
8 ravenclawColors,
9 gryffindorColors,
10 slytherinColors,
11 houseEmoji,
12 wandEmoji,
13 patronusEmoji,
14 bloodStatusEmoji,
15 ministryOfMagicEmoji,
16 boggartEmoji,
17 roleEmoji,
18 orderOfThePheonixEmoji,
19 deathEaterEmoji,
20 dumbledoresArmyEmoji,
21 aliasEmoji
22} from "./../../helpers/helpers";
23import { motion } from "framer-motion";
24
25const container = {
26 hidden: { scale: 0 },
27 show: {
28 scale: 1,
29 transition: {
30 delayChildren: 1
31 }
32 }
33};
34
35const item = {
36 hidden: { scale: 0 },
37 show: { scale: 1 }
38};
39
40const Card = ({
41 _id,
42 name,
43 house,
44 patronus,
45 bloodStatus,
46 role,
47 deathEater,
48 dumbledoresArmy,
49 orderOfThePheonix,
50 ministryOfMagic,
51 alias,
52 wand,
53 boggart,
54 animagus,
55 index
56}) => {
57 return (
58 <motion.div variants={container} initial="hidden" animate="show">
59 <motion.div
60 variants={item}
61 sx={{
62 border: "solid 2px",
63 borderImageSource:
64 house === "Gryffindor"
65 ? gryffindorColors
66 : house === "Hufflepuff"
67 ? hufflepuffColors
68 : house === "Slytherin"
69 ? slytherinColors
70 : house === "Ravenclaw"
71 ? ravenclawColors
72 : null,
73 borderImageSlice: 1,
74 display: "flex",
75 flexDirection: "column",
76 padding: "1em",
77 margin: "1em",
78 minWidth: ["250px", "400px", "500px"]
79 }}
80 >
81 <h2
82 sx={{
83 color: "white",
84 fontFamily: "heading",
85 letterSpacing: "body",
86 fontSize: "2.5em",
87 borderBottom: "solid 2px",
88 borderColor: "white"
89 }}
90 >
91 {name}
92 </h2>
93 <div
94 sx={{
95 display: "grid",
96 gridTemplateColumns: "1fr 1fr",
97 gridTemplateRows: "auto",
98 gap: "2em",
99 marginTop: "2em"
100 }}
101 >
102 <p
103 sx={{
104 color: "white",
105 fontFamily: "heading",
106 letterSpacing: "body",
107 fontSize: "1.5em"
108 }}
109 >
110 <strong>house:</strong> {house} {houseEmoji}
111 </p>
112 <p
113 sx={{
114 color: "white",
115 fontFamily: "heading",
116 letterSpacing: "body",
117 fontSize: "1.5em"
118 }}
119 >
120 <strong>wand:</strong> {checkNull(wand)} {wandEmoji}
121 </p>
122 <p
123 sx={{
124 color: "white",
125 fontFamily: "heading",
126 letterSpacing: "body",
127 fontSize: "1.5em"
128 }}
129 >
130 <strong>patronus:</strong> {checkNull(patronus)} {patronusEmoji}
131 </p>
132 <p
133 sx={{
134 color: "white",
135 fontFamily: "heading",
136 letterSpacing: "body",
137 fontSize: "1.5em"
138 }}
139 >
140 <strong>boggart:</strong> {checkNull(boggart)} {boggartEmoji}
141 </p>
142 <p
143 sx={{
144 color: "white",
145 fontFamily: "heading",
146 letterSpacing: "body",
147 fontSize: "1.5em"
148 }}
149 >
150 <strong>blood:</strong> {checkNull(bloodStatus)} {bloodStatusEmoji}
151 </p>
152 <p
153 sx={{
154 color: "white",
155 fontFamily: "heading",
156 letterSpacing: "body",
157 fontSize: "1.5em"
158 }}
159 >
160 <strong>role:</strong> {checkNull(role)} {roleEmoji}
161 </p>
162 <p
163 sx={{
164 color: "white",
165 fontFamily: "heading",
166 letterSpacing: "body",
167 fontSize: "1.5em"
168 }}
169 >
170 <strong>order of the pheonix:</strong> {checkNull(orderOfThePheonix)} {orderOfThePheonixEmoji}
171 </p>
172 <p
173 sx={{
174 color: "white",
175 fontFamily: "heading",
176 letterSpacing: "body",
177 fontSize: "1.5em"
178 }}
179 >
180 <strong>ministry of magic:</strong> {checkDeathEater(ministryOfMagic)} {ministryOfMagicEmoji}
181 </p>
182 <p
183 sx={{
184 color: "white",
185 fontFamily: "heading",
186 letterSpacing: "body",
187 fontSize: "1.5em"
188 }}
189 >
190 <strong>death eater:</strong> {checkDeathEater(deathEater)} {deathEaterEmoji}
191 </p>
192 <p
193 sx={{
194 color: "white",
195 fontFamily: "heading",
196 letterSpacing: "body",
197 fontSize: "1.5em"
198 }}
199 >
200 <strong>dumbledores army:</strong> {checkDumbledoresArmy(dumbledoresArmy)} {dumbledoresArmyEmoji}
201 </p>
202 <p
203 sx={{
204 color: "white",
205 fontFamily: "heading",
206 letterSpacing: "body",
207 fontSize: "1.5em"
208 }}
209 >
210 <strong>alias:</strong> {checkNull(alias)} {aliasEmoji}
211 </p>
212 </div>
213 </motion.div>
214 </motion.div>
215 );
216};
217export default Card;

We are importing all of those cool emojis we added earlier in our helper file. The container and item objects are for use in our animations from framer motion. We descructure our props, of which there are many, and return a div which has the framer motion object prepended to it and the item object passed to the variants prop. This is a simpler way of passing the object and all of it’s values through. For certain properties we run a null check against them to determinate what we should show.

The only thing left to do is implement the Spells page and its associated components then the implementation of this site is done! Given all we have covered I’m sure you can handle the last part!

Your final result should resemble something like this: serverless-graphql-potter.

Did you notice the cool particles? That’s a nice touch you could add to your site!

Deploy the beast!

That’s a lot of code and we haven’t even checked that it works!! (of course during development you should check how things look and work and make changes accordingly, I didn’t cover running the site as that’s common practice while developing). Lets deploy our site to Netlify and check it out!

At the projects root create a new file called netlify.toml

netlify.toml

1[build]
2 command = "yarn build"
3 functions = "functions"
4 publish = "public"

If you don’t already have an account, create a new one at netlify.com. To publish your site:

  • Click create new site, identify yourself and choose your repository
  • set your build command as yarn build and publish directory as public
  • Click site settings and change site name and…. change the name!
  • On the left tab menu find build and deploy and click that and scroll down to the environment section and add your environment variables: SERVER_KEY and FAUNA_ADMIN
  • You can add the functions path under the functions tab but Netlify will also pick this up from the netlify.toml file you created

When you first created this new site Netlify tried to deploy it. It wouldn’t have worked as we hadn’t set the environment variables yet. Go to the deploys tab at the top of the page and hit the trigger deploy dropdown and deploy site. If you encounter any issues then please drop me an email at hello@richardhaines.dev and we can try and work through it together.

And that’s it! I hope you enjoyed it and learnt something along the way. Thank you for coming to my TED talk 😅

If you liked this article feel free to give me a follow on Twitter with the blue button at the top of the page. 😇

Edit on GitHub.Previous: Notes on GSAPNext: Side project distractions