This article was originally published on my garden.richardhaines.dev
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.
mkdir harry-potter
cd harry-potter
yarn init- y
mkdir src/pages/
cd src/pages && touch index.js
mkdir src/components
touch gatsby-config.js
touch gatsby-browser.js
touch gatsby-ssr.js
touch .gitignore
mkdir functions/graphql
cd functions/graphql && touch schema.gql graphql.js db-connection.js
We'll also need to add some packages.
yarn add gatsby react react-dom theme-ui gatsby-plugin-theme-ui faunadb isomorphic-fetch dotenv
Add the following to your newly created .gitignore file:
.netlify
node_modules
.cache
public
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
type Query {
allCharacters: [Character]!
allSpells: [Spell]!
}
type Character {
name: String!
house: String
patronus: String
bloodStatus: String
role: String
school: String
deathEater: Boolean
dumbledoresArmy: Boolean
orderOfThePheonix: Boolean
ministryOfMagic: Boolean
alias: String
wand: String
boggart: String
animagus: String
}
type Spell {
effect: String
spell: String
type: String
}
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.
require("dotenv").config();
const faunadb = require("faunadb");
const query = faunadb.query;
function createClient() {
if (!process.env.FAUNA_ADMIN) {
throw new Error(`No FAUNA_ADMIN key in found, please check your fauna dashboard or create a new key.`);
}
const client = new faunadb.Client({
secret: process.env.FAUNA_ADMIN
});
return client;
}
exports.client = createClient();
exports.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
// ...other stuff
.env.*
.env
FAUNA_ADMIN=xxxxxxxxxxxxxxxxxxxxxxxxxxx
SERVER_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxx
POTTER_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
const fetch = require("isomorphic-fetch");
const { client, query } = require("./functions/graphql/db");
const q = query;
const potterEndPoint = `https://www.potterapi.com/v1/characters/?key=${process.env.POTTER_KEY}`;
fetch(potterEndPoint)
.then(res => res.json())
.then(res => {
console.log({ res });
const characterArray = res.map((char, index) => ({
_id: char._id,
name: char.name,
house: char.house,
patronus: char.patronus,
bloodStatus: char.blood,
role: char.role,
school: char.school,
deathEater: char.deathEater,
dumbledoresArmy: char.dumbledoresArmy,
orderOfThePheonix: char.orderOfThePheonix,
ministryOfMagic: char.ministryOfMagic,
alias: char.alias,
wand: char.wand,
boggart: char.boggart,
animagus: char.animagus
}));
client
.query(
q.Map(characterArray, q.Lambda("character", q.Create(q.Collection("Character"), { data: q.Var("character") })))
)
.then(console.log("Wrote potter characters to FaunaDB"))
.catch(err => console.log("Failed to add characters to FaunaDB", err));
});
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.
node 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.
const fetch = require("isomorphic-fetch");
const { client, query } = require("./functions/graphql/db");
const q = query;
const potterEndPoint = `https://www.potterapi.com/v1/spells/?key=${process.env.POTTER_KEY}`;
fetch(potterEndPoint)
.then(res => res.json())
.then(res => {
console.log({ res });
const spellsArray = res.map((char, index) => ({
_id: char._id,
effect: char.effect,
spell: char.spell,
type: char.type
}));
client
.query(q.Map(spellsArray, q.Lambda("spell", q.Create(q.Collection("Spell"), { data: q.Var("spell") }))))
.then(console.log("Wrote potter spells to FaunaDB"))
.catch(err => console.log("Failed to add spells to FaunaDB", err));
});
node 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
require("dotenv").config();
const { createHttpLink } = require("apollo-link-http");
const { ApolloServer, makeRemoteExecutableSchema, introspectSchema } = require("apollo-server-micro");
const fetch = require("isomorphic-fetch");
const link = createHttpLink({
uri: "https://graphql.fauna.com/graphql",
fetch,
headers: {
Authorization: `Bearer ${process.env.SERVER_KEY}`
}
});
const schema = makeRemoteExecutableSchema({
schema: introspectSchema(link),
link
});
const server = new ApolloServer({
schema,
introspection: true
});
exports.handler = server.createHandler({
cors: {
origin: "*",
credentials: true
}
});
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:
yarn add @apollo/client apollo-link-context
const React = require("react");
const { ApolloProvider, ApolloClient, InMemoryCache } = require("@apollo/client");
const { setContext } = require("apollo-link-context");
const { createHttpLink } = require("apollo-link-http");
const fetch = require("isomorphic-fetch");
const httpLink = createHttpLink({
uri: "https://graphql.fauna.com/graphql",
fetch
});
const authLink = setContext((_, { headers }) => {
return {
headers: {
...headers,
authorization: `Bearer ${process.env.SERVER_KEY}`
}
};
});
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache()
});
export 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...
mkdir -p src/layouts/index.js
cd src/components && touch header.js
cd src/components && touch main.js
cd src/components && touch footer.js
yarn add gatsby-plugin-layout
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:
module.exports = {
plugins: [
{
resolve: "gatsby-plugin-google-fonts",
options: {
fonts: ["Muli", "Open Sans", "source sans pro:300,400,400i,700"]
}
},
{
resolve: "gatsby-plugin-layout",
options: {
component: require.resolve("./src/layouts/index.js")
}
},
"gatsby-plugin-theme-ui"
]
};
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.
/** @jsx jsx */
import { jsx } from "theme-ui";
import React from "react";
import { Global, css } from "@emotion/core";
import Header from "./../components/site/header";
const Layout = ({ children, location }) => {
return (
<>
<Global
styles={css`
* {
margin: 0;
padding: 0;
box-sizing: border-box;
scroll-behavior: smooth;
/* width */
::-webkit-scrollbar {
width: 10px;
}
/* Track */
::-webkit-scrollbar-track {
background: #fff;
border-radius: 20px;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: #000;
border-radius: 20px;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: #000;
}
}
body {
scroll-behavior: smooth;
overflow-y: scroll;
-webkit-overflow-scrolling: touch;
width: 100%;
overflow-x: hidden;
height: 100%;
}
`}
/>
<Header location={location} />
{children}
</>
);
};
export 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/
cd 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.
export default {
fonts: {
body: "Open Sans",
heading: "Muli"
},
fontWeights: {
body: 300,
heading: 400,
bold: 700
},
lineHeights: {
body: "110%",
heading: 1.125,
tagline: "100px"
},
letterSpacing: {
body: "2px",
text: "5px"
},
colors: {
text: "#FFFfff",
background: "#121212",
primary: "#000010",
secondary: "#E7E7E9",
secondaryDarker: "#545455",
accent: "#DE3C4B"
},
breakpoints: ["40em", "56em", "64em"]
};
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:
<p
sx={{
fontSize: ["0.7em", "0.8em", "1em"]
}}
>
Some text here...
</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.
yarn add @emotion/styled
cd src/components
mkdir site && touch header.js
header.js
/** @jsx jsx */
import { jsx } from "theme-ui";
import HarryPotterLogo from "../../assets/svg-silhouette-harry-potter-4-transparent.svg.svg";
import { Link } from "gatsby";
import styled from "@emotion/styled";
const PageLink = styled(Link)`
color: #fff;
&:hover {
background-image: linear-gradient(
90deg,
rgba(127, 9, 9, 1) 0%,
rgba(255, 197, 0, 1) 12%,
rgba(238, 225, 23, 1) 24%
);
background-size: 100%;
background-repeat: repeat;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: bold;
}
`;
const Header = ({ location }) => {
return (
<section
sx={{
gridArea: "header",
justifyContent: "flex-start",
alignItems: "center",
width: "100%",
height: "100%",
display: location.pathname === "/" ? "none" : "flex"
}}
>
<Link to="/">
<HarryPotterLogo
sx={{
height: "100px",
width: "100px",
padding: "1em"
}}
/>
</Link>
<PageLink
sx={{
fontFamily: "heading",
fontSize: "2em",
color: "white",
marginRight: "2em"
}}
to="/houses"
>
houses
</PageLink>
<PageLink
sx={{
fontFamily: "heading",
fontSize: "2em",
color: "white"
}}
to="/spells"
>
Spells
</PageLink>
</section>
);
};
export 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.
export const HousesSpellsPhoneTemplateAreas = `
'header'
'main'
'main'
`;
export const HousesSpellsTabletTemplateAreas = `
'header header header header'
'main main main main'
`;
export const HousesSpellsDesktopTemplateAreas = `
'header header header header'
'main main main main'
`;
export const HomePhoneTemplateAreas = `
'logo'
'logo'
'logo'
'author'
'author'
'author'
'author'
`;
export const HomeTabletTemplateAreas = `
'logo . . '
'logo author author'
'logo author author'
'. . . '
`;
export const HomeDesktopTemplateAreas = `
'logo . . '
'logo author author'
'logo author author'
'. . . '
`;
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:
/** @jsx jsx */
import { jsx } from "theme-ui";
import React from "react";
import { HomePhoneTemplateAreas, HomeTabletTemplateAreas, HomeDesktopTemplateAreas } from "./../window/index";
import LogoSection from "./../components/site/logo-section";
import AuthorSection from "../components/site/author-section";
export default () => {
return (
<div
sx={{
width: "100%",
height: "100%",
maxWidth: "1200px",
margin: "1em"
}}
>
<div
sx={{
display: "grid",
gridTemplateColumns: ["1fr", "500px 1fr", "500px 1fr"],
gridAutoRows: "100px 1fr",
gridTemplateAreas: [HomePhoneTemplateAreas, HomeTabletTemplateAreas, HomeDesktopTemplateAreas],
width: "100%",
height: "100vh",
background: "#1E2224",
maxWidth: "1200px"
}}
>
<LogoSection />
<AuthorSection />
</div>
</div>
);
};
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
/** @jsx jsx */
import { jsx } from "theme-ui";
import HarryPotterLogo from "../assets/svg-silhouette-harry-potter-4-transparent.svg.svg";
export const Logo = () => (
<HarryPotterLogo
sx={{
height: ["200px", "300px", "500px"],
width: ["200px", "300px", "500px"],
padding: "1em",
position: "relative"
}}
/>
);
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
/** @jsx jsx */
import { jsx } from "theme-ui";
import { Logo } from "../logo";
const LogoSection = () => {
return (
<section
sx={{
gridArea: "logo",
display: "flex",
alignItems: "center",
justifyContent: ["start", "center", "center"],
position: "relative",
width: "100%"
}}
>
<Logo />
</section>
);
};
export 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
/** @jsx jsx */
import { jsx } from "theme-ui";
import { Link } from "gatsby";
import { houseEmoji, spellsEmoji } from "./../../helpers/helpers";
import styled from "@emotion/styled";
import { wizardEmoji } from "./../../helpers/helpers";
const InternalLink = styled(Link)`
color: #fff;
&:hover {
background-image: linear-gradient(
90deg,
rgba(127, 9, 9, 1) 0%,
rgba(255, 197, 0, 1) 12%,
rgba(238, 225, 23, 1) 24%
);
background-size: 100%;
background-repeat: repeat;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: bold;
}
`;
const ExternalLink = styled.a`
color: #fff;
&:hover {
background-image: linear-gradient(
90deg,
rgba(127, 9, 9, 1) 0%,
rgba(255, 197, 0, 1) 12%,
rgba(238, 225, 23, 1) 24%,
rgba(0, 0, 0, 1) 36%,
rgba(13, 98, 23, 1) 48%,
rgba(170, 170, 170, 1) 60%,
rgba(0, 10, 144, 1) 72%,
rgba(148, 119, 45, 1) 84%
);
background-size: 100%;
background-repeat: repeat;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: bold;
}
`;
const AuthorSection = () => {
return (
<section
sx={{
gridArea: "author",
position: "relative",
margin: "0 auto"
}}
>
<h1
sx={{
fontFamily: "heading",
color: "white",
letterSpacing: "text",
fontSize: ["3em", "3em", "5em"]
}}
>
Serverless Potter
</h1>
<div
sx={{
display: "flex",
justifyContent: "start",
alignItems: "flex-start",
width: "300px",
marginTop: "3em"
}}
>
<InternalLink
sx={{
fontFamily: "heading",
fontSize: "2.5em",
// color: 'white',
marginRight: "2em"
}}
to="/houses"
>
Houses
</InternalLink>
<InternalLink
sx={{
fontFamily: "heading",
fontSize: "2.5em",
color: "white"
}}
to="/spells"
>
Spells
</InternalLink>
</div>
<p
sx={{
fontFamily: "heading",
letterSpacing: "body",
fontSize: "2em",
color: "white",
marginTop: "2em",
width: ["300px", "500px", "900px"]
}}
>
This is a site that goes with the tutorial on creating a jamstack site with serverless functions and FaunaDB I
decided to use the potter api as i love the world of harry potter {wizardEmoji}
</p>
<p
sx={{
fontFamily: "heading",
letterSpacing: "body",
fontSize: "2em",
color: "white",
marginTop: "1em",
width: ["300px", "500px", "900px"]
}}
>
Built with Gatsby, Netlify functions, Apollo and FaunaDB. Data provided via the Potter API.
</p>
<p
sx={{
fontFamily: "heading",
letterSpacing: "body",
fontSize: "2em",
color: "white",
marginTop: "1em",
width: ["300px", "500px", "900px"]
}}
>
Select <strong>Houses</strong> or <strong>Spells</strong> to begin exploring potter stats!
</p>
<div
sx={{
display: "flex",
flexDirection: "column"
}}
>
<ExternalLink
sx={{
fontFamily: "heading",
letterSpacing: "body",
fontSize: "2em",
color: "white",
marginTop: "1em",
width: ["300px", "500px", "900px"]
}}
href="your-personal-website"
>
author: your name here!
</ExternalLink>
<ExternalLink
sx={{
fontFamily: "heading",
letterSpacing: "body",
fontSize: "2em",
color: "white",
marginTop: "1em",
width: "900px"
}}
href="your-github-repo-for-this-project"
>
github: the name you gave this project
</ExternalLink>
</div>
</section>
);
};
export 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.
cd src
mkdir helpers && touch helpers.js
helpers.js
export const gryffindorColors = "linear-gradient(90deg, rgba(127,9,9,1) 27%, rgba(255,197,0,1) 61%)";
export const hufflepuffColors = "linear-gradient(90deg, rgba(238,225,23,1) 35%, rgba(0,0,0,1) 93%)";
export const slytherinColors = "linear-gradient(90deg, rgba(13,98,23,1) 32%, rgba(170,170,170,1) 69%)";
export const ravenclawColors = "linear-gradient(90deg, rgba(0,10,144,1) 32%, rgba(148,107,45,1) 69%)";
export const houseEmoji = `🏡`;
export const spellsEmoji = `💫`;
export const wandEmoji = `💫`;
export const patronusEmoji = `✨`;
export const deathEaterEmoji = `🐍`;
export const dumbledoresArmyEmoji = `⚔️`;
export const roleEmoji = `📖`;
export const bloodStatusEmoji = `🧙🏾♀️ 🤵🏾`;
export const orderOfThePheonixEmoji = `🦄`;
export const ministryOfMagicEmoji = `📜`;
export const boggartEmoji = `🕯`;
export const aliasEmoji = `👨🏼🎤`;
export const wizardEmoji = `🧙🏼♂️`;
export const gryffindorEmoji = `🦁`;
export const hufflepuffEmoji = `🦡`;
export const slytherinEmoji = `🐍`;
export const ravenclawEmoji = `🦅`;
export function checkNull(value) {
return value !== null ? value : "unknown";
}
export function checkDeathEater(value) {
if (value === false) {
return "no";
}
return "undoubtedly";
}
export function checkDumbledoresArmy(value) {
if (value === false) {
return "no";
}
return `undoubtedly ${wizardEmoji}`;
}
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
/** @jsx jsx */
import { jsx } from "theme-ui";
import React from "react";
import { gql, useQuery } from "@apollo/client";
import MainSection from "./../components/site/main-section";
import { HousesPhoneTemplateAreas, HousesTabletTemplateAreas, HousesDesktopTemplateAreas } from "../window";
const GET_CHARACTERS = gql`
query GetCharacters {
allCharacters {
data {
_id
name
house
patronus
bloodStatus
role
school
deathEater
dumbledoresArmy
orderOfThePheonix
ministryOfMagic
alias
wand
boggart
animagus
}
}
}
`;
const Houses = () => {
const { loading: characterLoading, error: characterError, data: characterData } = useQuery(GET_CHARACTERS);
const [selectedHouse, setSelectedHouse] = React.useState([]);
React.useEffect(() => {
const gryffindor =
!characterLoading &&
!characterError &&
characterData.allCharacters.data.filter(char => char.house === "Gryffindor");
setSelectedHouse(gryffindor);
}, [characterLoading, characterData]);
const getHouse = house => {
switch (house) {
case "gryffindor":
setSelectedHouse(
!characterLoading &&
!characterError &&
characterData.allCharacters.data.filter(char => char.house === "Gryffindor")
);
break;
case "hufflepuff":
setSelectedHouse(
!characterLoading &&
!characterError &&
characterData.allCharacters.data.filter(char => char.house === "Hufflepuff")
);
break;
case "slytherin":
setSelectedHouse(
!characterLoading &&
!characterError &&
characterData.allCharacters.data.filter(char => char.house === "Slytherin")
);
break;
case "ravenclaw":
setSelectedHouse(
!characterLoading &&
!characterError &&
characterData.allCharacters.data.filter(char => char.house === "Ravenclaw")
);
break;
default:
setSelectedHouse(
!characterLoading &&
!characterError &&
characterData.allCharacters.data.filter(char => char.house === "Gryffindor")
);
break;
}
};
return (
<div
sx={{
gridArea: "main",
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(250px, auto))",
gridAutoRows: "auto",
gridTemplateAreas: [
HousesSpellsPhoneTemplateAreas,
HousesSpellsTabletTemplateAreas,
HousesSpellsDesktopTemplateAreas
],
width: "100%",
height: "100%",
position: "relative"
}}
>
<MainSection house={selectedHouse} getHouse={getHouse} />
</div>
);
};
export 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
/** @jsx jsx */
import { jsx } from "theme-ui";
import React from "react";
import Card from "../cards/card";
import SearchBar from "./searchbar";
import { useSearchBar } from "./useSearchbar";
import Loading from "./loading";
import HouseSection from "./house-section";
const MainSection = React.memo(({ house, getHouse }) => {
const { members, handleSearchQuery } = useSearchBar(house);
return house.length ? (
<div
sx={{
gridArea: "main",
height: "100%",
position: "relative"
}}
>
<div
sx={{
color: "white",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
fontFamily: "heading",
letterSpacing: "body",
fontSize: "2em",
position: "relative"
}}
>
<h4>
{house[0].house} Members - {house.length}
</h4>
<SearchBar handleSearchQuery={handleSearchQuery} />
<HouseSection getHouse={getHouse} />
</div>
<section
sx={{
margin: "0 auto",
width: "100%",
display: "grid",
gridAutoRows: "auto",
gridTemplateColumns: "repeat(auto-fill, minmax(auto, 500px))",
gap: "1.5em",
justifyContent: "space-evenly",
marginTop: "1em",
position: "relative",
height: "100vh"
}}
>
{members.map((char, index) => (
<Card key={char._id} index={index} {...char} />
))}
</section>
</div>
) : (
<Loading />
);
});
export 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
/** @jsx jsx */
import { jsx } from "theme-ui";
const SearchBar = ({ handleSearchQuery }) => {
return (
<div
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
margin: "2em"
}}
>
<input
sx={{
color: "greyBlack",
fontFamily: "heading",
fontSize: "0.8em",
fontWeight: "bold",
letterSpacing: "body",
border: "1px solid",
borderColor: "accent",
width: "300px",
height: "50px",
padding: "0.4em"
}}
type="text"
id="members-searchbar"
placeholder="Search members.."
onChange={handleSearchQuery}
/>
</div>
);
};
export 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
import React from "react";
export const useSearchBar = data => {
const emptyQuery = "";
const [searchQuery, setSearchQuery] = React.useState({
filteredData: [],
query: emptyQuery
});
const handleSearchQuery = e => {
const query = e.target.value;
const members = data || [];
const filteredData = members.filter(member => {
return member.name.toLowerCase().includes(query.toLowerCase());
});
setSearchQuery({ filteredData, query });
};
const { filteredData, query } = searchQuery;
const hasSearchResult = filteredData && query !== emptyQuery;
const members = hasSearchResult ? filteredData : data;
return { members, handleSearchQuery };
};
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
/** @jsx jsx */
import { jsx } from "theme-ui";
import { gryffindorColors, hufflepuffColors, slytherinColors, ravenclawColors } from "./../../helpers/helpers";
import styled from "@emotion/styled";
import { motion } from "framer-motion";
const House = styled.a`
color: #fff;
&:hover {
background-image: ${props => props.house};
background-size: 100%;
background-repeat: repeat;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: bold;
}
`;
const HouseSection = ({ getHouse }) => {
return (
<section
sx={{
width: "100%",
position: "relative"
}}
>
<ul
sx={{
listStyle: "none",
cursor: "crosshair",
fontFamily: "heading",
fontSize: "1em",
display: "flex",
flexDirection: ["column", "row", "row"],
alignItems: "center",
justifyContent: "space-evenly",
position: "relative"
}}
>
<motion.li
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{
type: "spring",
stiffness: 200,
damping: 20,
delay: 0.2
}}
>
<House onClick={() => getHouse("gryffindor")} house={gryffindorColors}>
Gryffindor
</House>
</motion.li>
<motion.li
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{
type: "spring",
stiffness: 200,
damping: 20,
delay: 0.4
}}
>
<House onClick={() => getHouse("hufflepuff")} house={hufflepuffColors}>
Hufflepuff
</House>
</motion.li>
<motion.li
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{
type: "spring",
stiffness: 200,
damping: 20,
delay: 0.6
}}
>
<House onClick={() => getHouse("slytherin")} house={slytherinColors}>
Slytherin
</House>
</motion.li>
<motion.li
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{
type: "spring",
stiffness: 200,
damping: 20,
delay: 0.8
}}
>
<House onClick={() => getHouse("ravenclaw")} house={ravenclawColors}>
Ravenclaw
</House>
</motion.li>
</ul>
</section>
);
};
export 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
/** @jsx jsx */
import { jsx } from "theme-ui";
import {
checkNull,
checkDeathEater,
checkDumbledoresArmy,
hufflepuffColors,
ravenclawColors,
gryffindorColors,
slytherinColors,
houseEmoji,
wandEmoji,
patronusEmoji,
bloodStatusEmoji,
ministryOfMagicEmoji,
boggartEmoji,
roleEmoji,
orderOfThePheonixEmoji,
deathEaterEmoji,
dumbledoresArmyEmoji,
aliasEmoji
} from "./../../helpers/helpers";
import { motion } from "framer-motion";
const container = {
hidden: { scale: 0 },
show: {
scale: 1,
transition: {
delayChildren: 1
}
}
};
const item = {
hidden: { scale: 0 },
show: { scale: 1 }
};
const Card = ({
_id,
name,
house,
patronus,
bloodStatus,
role,
deathEater,
dumbledoresArmy,
orderOfThePheonix,
ministryOfMagic,
alias,
wand,
boggart,
animagus,
index
}) => {
return (
<motion.div variants={container} initial="hidden" animate="show">
<motion.div
variants={item}
sx={{
border: "solid 2px",
borderImageSource:
house === "Gryffindor"
? gryffindorColors
: house === "Hufflepuff"
? hufflepuffColors
: house === "Slytherin"
? slytherinColors
: house === "Ravenclaw"
? ravenclawColors
: null,
borderImageSlice: 1,
display: "flex",
flexDirection: "column",
padding: "1em",
margin: "1em",
minWidth: ["250px", "400px", "500px"]
}}
>
<h2
sx={{
color: "white",
fontFamily: "heading",
letterSpacing: "body",
fontSize: "2.5em",
borderBottom: "solid 2px",
borderColor: "white"
}}
>
{name}
</h2>
<div
sx={{
display: "grid",
gridTemplateColumns: "1fr 1fr",
gridTemplateRows: "auto",
gap: "2em",
marginTop: "2em"
}}
>
<p
sx={{
color: "white",
fontFamily: "heading",
letterSpacing: "body",
fontSize: "1.5em"
}}
>
<strong>house:</strong> {house} {houseEmoji}
</p>
<p
sx={{
color: "white",
fontFamily: "heading",
letterSpacing: "body",
fontSize: "1.5em"
}}
>
<strong>wand:</strong> {checkNull(wand)} {wandEmoji}
</p>
<p
sx={{
color: "white",
fontFamily: "heading",
letterSpacing: "body",
fontSize: "1.5em"
}}
>
<strong>patronus:</strong> {checkNull(patronus)} {patronusEmoji}
</p>
<p
sx={{
color: "white",
fontFamily: "heading",
letterSpacing: "body",
fontSize: "1.5em"
}}
>
<strong>boggart:</strong> {checkNull(boggart)} {boggartEmoji}
</p>
<p
sx={{
color: "white",
fontFamily: "heading",
letterSpacing: "body",
fontSize: "1.5em"
}}
>
<strong>blood:</strong> {checkNull(bloodStatus)} {bloodStatusEmoji}
</p>
<p
sx={{
color: "white",
fontFamily: "heading",
letterSpacing: "body",
fontSize: "1.5em"
}}
>
<strong>role:</strong> {checkNull(role)} {roleEmoji}
</p>
<p
sx={{
color: "white",
fontFamily: "heading",
letterSpacing: "body",
fontSize: "1.5em"
}}
>
<strong>order of the pheonix:</strong> {checkNull(orderOfThePheonix)} {orderOfThePheonixEmoji}
</p>
<p
sx={{
color: "white",
fontFamily: "heading",
letterSpacing: "body",
fontSize: "1.5em"
}}
>
<strong>ministry of magic:</strong> {checkDeathEater(ministryOfMagic)} {ministryOfMagicEmoji}
</p>
<p
sx={{
color: "white",
fontFamily: "heading",
letterSpacing: "body",
fontSize: "1.5em"
}}
>
<strong>death eater:</strong> {checkDeathEater(deathEater)} {deathEaterEmoji}
</p>
<p
sx={{
color: "white",
fontFamily: "heading",
letterSpacing: "body",
fontSize: "1.5em"
}}
>
<strong>dumbledores army:</strong> {checkDumbledoresArmy(dumbledoresArmy)} {dumbledoresArmyEmoji}
</p>
<p
sx={{
color: "white",
fontFamily: "heading",
letterSpacing: "body",
fontSize: "1.5em"
}}
>
<strong>alias:</strong> {checkNull(alias)} {aliasEmoji}
</p>
</div>
</motion.div>
</motion.div>
);
};
export 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
[build]
command = "yarn build"
functions = "functions"
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 @studio_hungry 😇
Top comments (0)