Designing the Quiz App Experience and Building It with Gatsby + Airtable

Caye Borreo
10 min readJun 8, 2021
What prescribemebooks.caye.codes is made of

The necessity to polish my portfolio resulted in this revelation: I had little to no side projects. Most of my developer work was made under my previous employer, and now that I’ve parted ways with them (alongside all my previous projects), it felt like I had nothing left to show as my own work.

Now that I have time to spice things up a bit, I decided I wanted to do a quiz website and highlight it as one of my selected works. After brainstorming with a friend, I ended up with a “Prescribe Me Books” quiz app.

The Idea

This is how the website is supposed to go: You visit Dr Libby’s clinic, tell her your “chief complaint” or what you’re currently feeling. Dr Libby will then proceed to do some “history-taking”, which is mostly probing your current situation. After two to three questions, you will be given a prescription of none other than… books. Read at least five pages once or twice daily, that kind of thing.

I thought it was an interesting concept, since we took the traditional medical consultation and decided to put a bookworm twist to it. And if it involves books, I’m automatically in. I could talk people’s ears off when it comes to my favorite reads.

So I thought—well, this sounds exciting. Let’s get to it.

The User Experience

I decided this app will have elements of a role-playing game. I wanted to replicate the experience of visiting a clinic, filling out a form, and actually waiting in line as you anticipate your turn in the doctor’s office.

So if I were to map it out:

  1. Enter the clinic
  2. Fill out a form
  3. Wait in line to be called by doctor
  4. Start the quiz aka probing
  5. Receive the prescription (of books)

The defining process to me is what information should the form from Step #2 ask for, because it kicks off the probing process. Your doctor will probe you based on your “chief complaint”, so that felt like a too-vital piece of information to be a free I-will-answer-this-in-an-essay field. I had to give the user defined options.

Name: ___________

Chief Complaint:
Lately, I’ve been feeling __________.

To know what options I can offer, I had to narrow down what universal experiences I’d be able to “prescribe” for, and that ultimately depends on the limited set of books I’m willing to recommend.

So I listed around 30 titles, then grouped them into “moods” — what am I feeling that would make me more likely to reach out for this book and read it?

I ended up with three categories:

  1. Lost in life
  2. Grief-stricken
  3. Tired

I know the distinctions are questionable — it is a Venn diagram of sorts — so I had most of the books end up in more than one category.

I went for the divide-and-conquer method after this, and focused on developing one quiz algorithm at a time. I started with the Lost in life path and tried figuring out how my data should look like.

The Data Structure

As with any other quiz, I have three major entities here: Questions, Answers, and then Results.

For every Question, I have many possible Answers, and for every Answer, it either 1) refers to a follow-up Question, or 2) refers to a Result. (So many relationships!)

Side note: My other option was a points system, but it was faster for me to map out a quiz algorithm this way. To game devs out there — I don’t know how you do it. Probably better than my buzzed up framework, but hey, I need an MVP.

So I roughly have an idea of how my data should be structured and related to each other, and how my quiz should flow. The next question is where do I intend to put all of it.

The Stack

My frontend is unquestionably GatsbyJS designed with Bulma components and deployed via Netlify, as I’ve shipped so many side projects with them before, so this was quite a no-brainer to me.

For my backend though, I need a content management system where I can freely change my data without having to push code every now and then, and from experience I have two easily workable choices: Prismic and Airtable.

With Prismic, however, I had to make a repeatable Question document that contains the multiple Answers. And one Question = one Document. That’s too much to work with. I’d really rather see everything in one workspace.

All that is solved with Airtable, not to mention its powerful handling of data relationships.

Right there I decided: Airtable’s perfect for my use case then.

The Base

I created the Book Prescription base and mapped it out as initially visualized — Questions, Answers, and Results became my tables.

Filling it out was actually a roadblock. It took me a good while to phrase the probing Questions and the possible Answers. For example, in the Lost path, I have seven major books I’d like to recommend:

Books I’m willing to recommend for those “feeling lost”

From here, I further grouped them together, so I made sure the questions were pushing the user into distinct enough paths. For example: For those who feel “lost”, I figured sometimes they either need time to process where they currently are, or they already know which direction to take but just don’t know how to take charge yet. From that perspective, you can already see which books are for which path.

I finished the Lost path like so:

Quiz algorithm for Lost in life path

So I populated my base with the following data.

To get started with frontend and work on pulling all these entered data, I created a new Gatsby project using the famous default starter, installed Bulma and other dependencies, and finally installed gatsby-source-airtable. After some configuration, I was able to pull what I entered from Airtable.

GraphQL!

Now that we have our Questions, Answers, and Results, we’re ready to get our hands dirty on the web pages themselves.

The Flow

There are three phases to the user experience: the Pre-consult, the Probing, and the Prescription (hey, three P’s!).

Here are the wireframes I used as a guide.

Wireframes for the Pre-consult phase

The Probing and Prescription parts would look the same, in that Dr Libby has a conversation box, and the user-related components will be below it.

Wireframes for the Probing/Prescription phases

Here’s what I laid out regarding the links:

  1. Pre-consult is going to happen at the index page. Right off the bat, it’s what the user first sees.
  2. Probing will be at /probing/{path}, where path can either be ls (lost), gf (grief-stricken), or td (tired). This will depend on which “chief complaint” the user chooses.
  3. Prescription will be at /prescription.

Creating pages for #1 and #3 is standard. As for #2, I added the Paths table in my Airtable base so I can use the File System Route API to automagically generate the different probing pages.

I decided to use Context for state management, since it looks like I’m gonna need variables across different components, not just within them. My Probing page will need to know the patient’s answers from Pre-consult, and Prescription needs to know the Probing answers to display results.

With that decided, I began going through the phases one by one.

Pre-consult

The Pre-consult is just a series of steps based on the user’s current status.

Wireframes for the Pre-consult phase

So every time the user clicks on a call-to-action button, it’s a matter of changing the variable we’ll call preconsultStatus.

Here’s the Preconsult page. All five screens above are in one component, and it’s a matter of detecting which one to render.

const Preconsult = () => {
const { state, dispatch } = useContext(AppContext)
const RenderPreconsultComponent = () => {
switch (state?.preConsult?.status) {
case "VISIT_CLINIC":
default:
return <VisitClinic dispatch={dispatch} />
case "FILL_OUT_FORM":
return <FillOutForm state={state} dispatch={dispatch} />
case "QUEUED":
return <Queued state={state} dispatch={dispatch} />
case "IN_CONSULT":
return <InConsult state={state} dispatch={dispatch} />
case "PROCEED_PROBING":
return <ProceedProbing state={state} dispatch={dispatch} />
}
return (<RenderPreconsultComponent />)
}
export default Preconsult

For each Pre-consult component, the CTA dispatches a change in preConsult status—except the ProceedProbing one, which is a Link to /probing/{path}.

Probing

In my /probing/{path} page, I filter the Questions where path = user’s chief complaint. It gets passed on to my ProbingPage component as the questions prop.

const Probing = props => {
const { state, dispatch } = useContext(AppContext)
const questions = props?.data?.allAirtableQuestions?.nodes
return (
<Layout>
<Seo title="Probing" />
<ProbingPage questions={questions} state={state} dispatch={dispatch} />
</Layout>
)
}
export default Probingexport const query = graphql`
query ($id: [String] = "$id") {
allAirtableQuestions(
filter: { data: { path: { elemMatch: { id: { in: $id } } } } }
) {
nodes {
recordId
data {
answers {
data {
isFinal
isDisabled
label
resultNotes {
childMarkdownRemark {
html
}
}
followUpQuestion {
recordId
}
mainPrescription {
data {
author
goodreadsURL
administration
subtitle
title
}
}
otherRecommendations {
data {
author
goodreadsURL
administration
subtitle
title
}
}
}
recordId
}
order
label
}
}
}
}
`

I eventually designed the ProbingPage to look like this (thanks Bulma for the easily customizable components and Canva for the avatars):

Here, we’re displaying the currentQuestion label (“Can you tell me why…”), then enumerating currentQuestion’s Answers as buttons below it.

The important mechanism is knowing what happens when any of the Answers get clicked. But in a nutshell:

  1. If the Answer has a follow-up question, we call setCurrentQuestion to display the new Question. (Quiz goes on as usual.)
  2. If the Answer directs to a result, we get that Answer’s Result and navigate user to /prescription.
const handleAnswerClick = event => {
const target = event.currentTarget
// If the answer leads to a follow-up question, setCurrentQuestion
if (target.name === "followUpQuestion") {
const followUpQuestion = getRecordById(questions, target.id)
setCurrentQuestion(followUpQuestion)
} else {

// Otherwise, dispatch answer details to state > /prescription
const results = getRecordById(currentQuestion?.data?.answers, target.id)
if (results?.data?.isFinal) {
dispatch({
type: "UPDATE_PRESCRIPTION_RESULTS",
payload: results?.data,
})
navigate("/prescription")
} else { // Error handling only
dispatch({
type: "UPDATE_PRECONSULT_STATUS",
payload: "VISIT_CLINIC",
})
navigate("/")
}
}
}

This is actually the heart of the quiz app. Once this little mechanism worked, the website was practically done—I just had to populate the Questions, Answers, and Results.

Prescription

At this point, what was left was designing the prescription itself. So I based this off of an actual prescription’s layout, then populated it with data from Results afterwards.

Sample prescription

The Website

And so, here it is!

Tada!

For the final touches, I named the receptionist Guy and made his and Dr Libby’s avatars from Canva, as well as the “logo”.

That’s it!

Here are some things I learned while making this website:

  1. Quiz algorithms are the heart of this app. It actually took me longer to finish the three paths than to code the totality of this website! I minded how my questions were phrased, and in a way it was sort of therapy speak.
  2. The File System Route API is so handy, and I got to filter my questions based on the route chosen. It suited so well with the divide-and-conquer attitude I had with making this quiz app.
  3. It’s my first time to render rich formatting columns from Airtable, and it was all through gatsby-source-airtable’s mapping config + markdown docs as well as gatsby-transformer-remark. Dr Libby’s probe results have text in bold/italics now because of this.
  4. In the interest of doing modular imports for my stylesheets, I learned that SCSS files starting with underscores (e.g. __variables.scss) are called partial files and don’t get translated to CSS. It’s best practice to import them all in one main SCSS file.
  5. As I was updating my dependencies to all their latest versions, I learned that using “/” for division is going to be deprecated soon, and we should all use math.div in our stylesheets instead 😅 (Bulma has an open issue about it too)

So that was fun 😆 I should probably do side projects like these every now and then to keep up with various tech tools as well.

I hope this was insightful! Feel free to comment what you would have done—let’s learn from each other.

Oh, of course: Don’t forget to drop by Dr Libby’s clinic any time 🤓

📚 Visit prescribemebooks.caye.codes for your dose of reads!

--

--