Build a Product Quiz app with Gadget, Shopify, and Vercel.

Time to build: approx. 1 hour

Technical Requirements

To get the most out of this tutorial, you will need:

  • A Shopify Partner account and a connected test store with the online store channel and a recently-installed Shopify-developed theme;
  • A reasonable familiarity with Shopify online store themes and are comfortable navigating theme architecture;
  • Comfort with Next.js, React, and Javascript

Introduction

Product recommendation quizzes are a powerful tool to build engaging sales experiences for shoppers on online stores by allowing them to map their problems or concerns to a product that best meets their needs. For Shopify merchants, this can be an appealing proposition - with an app that lets them build dynamic quizzes, they can present their shoppers with a tailored experience that can result in more conversions and higher satisfaction by matching the right shopper with the right products.

In under an hour, we can create a lightweight, customizable product recommendation quiz app using Gadget, connect the results to products in a Shopify merchant's store, and build both an embedded in-theme quizlet and a stand-alone quiz web application hosted on Vercel. This app will allow a merchant to create quizzes quickly and then serve a quiz or quizzes to their shoppers wherever they may be.

In our example, we will build a product recommendation quiz that recommends the appropriate skincare bundle from four options based on the shopper's answers. We can also configure the app to track the conversion state of each response to any quiz, giving the merchant rich information about the effectiveness of their quiz or quizzes.

Let's start building!

You can fork this Gadget project and try it out yourself.

You will still need to set up your Shopify Connection to a Shopify store. You can also jump to the Building the UI section to hook up your Gadget backend with two different frontends: an app hosted on Vercel, or a Liquid template-based solution.

Fork on Gadget

Getting Started with Gadget

Starting your app

Head over to app.gadget.dev and authenticate with Google, Github, or create/log in to your account. Next, Gadget will prompt you to create a new application. Click “Create App,” and Gadget will bring you into your new application.

Connecting to Shopify

To recommend products to shoppers, we'll need product data in our app that we can map to the results of a product recommendation quiz. Using Gadget's Connections feature, we can connect our app to a Shopify store and pull product data right from the shop.

What Shopify gives us

The Shopify connection provides us with access to any of the models surfaced in Shopify's Admin API, as well as an authenticated client and webhook consumption. This connection also allows us to sync data between Shopify and Gadget, both scheduled and on-demand.

Scopes and Models

To use the Shopify connection, navigate to the Connections view in Gadget via the left-hand navigation. Select the Shopify Partners connection.

The default Connection selection page for a new Gadget app

Now, we can go over to the Shopify Partner Dashboard and create a new app. Shopify prompts us for our app's name, URL, and redirection URL. Gadget provides URLs for you, as seen at the bottom of the Shopify Resources modal. Copy those values from Gadget to Shopify, and select “Create app” to save your changes and generate your API keys.

Add your app's URLs to the Shopify Partner Dashboard

Once your app is created, Shopify generates the API key and API secret key that Gadget needs to complete the connection.

Receive your API Keys from the Shopify Partner Dashboard

Copy these values back over to Gadget, and then you can select “Connect” to continue to the scope selection page.

Shopify Resources modal

Gadget allows you to select only the models you need from Shopify for your application. Each of the scopes listed allows you access to many Shopify API endpoints and synced Gadget models. When Gadget receives webhooks related to your selected resources, it converts the incoming payload into records in the corresponding model within your Gadget app. For this quiz app, we're going to choose the Products scope with write permissions, and within the scope, we need the Product, Product Image, and Product Variant models. You'll note that selecting the write permissions will automatically give us read as well.

Selected Product resources

Now that we have our scope and models selected, we can click "Confirm" to finish setting up our connection.

The last step to connecting Gadget and Shopify is to install our app on the shop we wish to sync product data from. In the Shopify Partner Dashboard, click “Select store” under “Test your app” and choose the applicable Shopify store. Follow the prompts, and you should arrive at this screen. Congrats, you're now connected!

Success!

You must be logged in to your Shopify store for authorization to work!

Now, we can trigger a manual sync between our connected Shopify store and Gadget. We need to click on "Manage Installs" on our connection and then click “Sync” on the listed connected store.

The created connection to a store on the Connections page, highlighting the Manage Installs buttonSync your connected Shopify store

You'll notice now on the left-hand side, under Models, Gadget lists all of the Shopify models you've selected. These models are perfect copies of the corresponding types and associations in Shopify. Each model comes with a CRUD API (create, read, update, delete/destroy) automatically triggered by incoming Shopify webhooks or by running syncs. These CRUD actions can also be triggered by interacting with the API, giving you control over these records right in Gadget. Additionally, if you've selected any Shopify resources, you will also have a Shopify Sync model and a Shopify Shop model in your list of models. Gadget automatically creates these last two models when you configure the Shopify connection, representing the data syncs between Shopify and Gadget and the shop the app is installed on. You can check out the Shopify connection documentation for more information on these models.

Now that we've established our connection between Shopify and Gadget and we've synced our data, we can build our models for our app.

Building our Quiz models

Proposed solution design

We need a way to create, serve, and record quiz responses in our app. The recording of responses enables us to track the conversion state we discussed above, effectively making a response to a quiz a snapshot of a session that a shopper has with our quiz. Going forward, to distinguish between the models and the concepts they represent, we'll refer to the models in the title case (e.g. a Result model vs result in reference to an outcome).

We need some models to capture the structure of our product quiz. A Quiz has many Questions, and each Question can have multiple possible Answers.

We also need models to capture user responses to our quiz. A Response belongs to a Quiz, has many Answers, and has one Result based on the selected Answers.

But how do the Answers lead to a Result? We can build a mapping interface to allow us to select which Answers link to which Result as a part of our app's admin UI.

Here's a diagram to demonstrate what relationships our models will have with each other:

The Model Relationship diagram for our app

Models outline

We need to create models for our app to represent the components of our Quiz; Questions, Answers, Results, Responses, and the Quiz itself. We need to connect these components by their relationships; Gadget's built-in relationship fields make this connection effortless. Let's start with the Quiz model.

Quiz

The Quiz model is the backbone of our application. In our concept, our app can have many instances of Quiz, each representing a unique product recommendation quiz created through the app's interface. Our Quiz model needs a couple of properties or fields to get started: a title, maybe a description or body content, and some identifying information like an ID.

Creating a new model in Gadget will take care of some of these fields for us automatically. Each model in Gadget comes with four fields: ID, State, Created At, and Updated At.

If we click the + in the Models section of the side nav, we can start our Quiz model:

Default model fields in Gadget

Up at the top of this schema view, we've named the model Quiz, and Gadget has created the API Identifier corresponding to the model's name. From here, we can add our first field, Title. Title is a String, and we cannot create an instance of Quiz without it. So, let's select “Add Field” and create our Title field:

Adding the Title field to Quiz

Again, naming the field will automatically generate the API Identifier. We can then select the type of data we're storing in this field, whether or not it has a default value, and any validations we may want to run against this field on object creation. In the case of Title, we want to select the required validation. We can also add a String length range validation to give Title a minimum and maximum length and a uniqueness validation if we want to ensure no two Quizzes have the same title. Let's add a uniqueness validation.

Adding validations to the Title

You may have noticed that adding a uniqueness validation triggered an action by Gadget to scan through any existing Quiz records for Title field data to ensure the constraints are met. This is because you can add new fields to models at any point, not just during creation; this allows you to grow and extend your models with your business needs as your app evolves. Gadget will then take care of any migrations or underlying schema changes for you behind the scenes.

Let's now add another field to represent the optional body/description text for the Quiz model:

Adding the Body field to Quiz

For simplicity's sake, we'll set the type to String with no validations.

But what is happening as we create these models and add these fields? Behind the scenes, Gadget automatically generates a CRUD API for each created model and updates this API with any new fields you add, modify, or remove. This means you can quickly test and consume your API immediately after changing your models. Gadget also creates API documentation for your API and a type-safe JavaScript client for you to consume, all in the background as you work.

With that, our Quiz model is done for now, and we can move on to Question.

Question

Let's create another new model, and call it Question. This model will represent a single Question in a given Quiz. We need just a few fields to get this model going for now: a title and a body, just like Quiz; we also will add three new fields: a sequence, an image URL, and a required field.

To start, let's add Title and Body to Question. Like Quiz, Title is a required String field, though the uniqueness constraint is unnecessary. Likewise, Body is a String field with no validations. Once added, let's move to Sequence.

The Sequence field allows us to declare where this Question will appear in the series of Questions. The Sequence field is technically optional; you could simply sort Questions based on the order they are created, alphabetically, or on another field. However, we've chosen to add a Sequence field to give you more control.

The Sequence field is going to be a Number field. When you set a field to the Number type, you can declare the number of decimals you anticipate the values stored in this field to contain. As we're just working with integers, we can enter a 0. We're going to declare the default value of this field as 1 to guard against null values in the unlikely case that Sequence may not get set. Finally, we're going to add the Required validation to prevent us from creating instances of Question without a Sequence value.

Adding a number field with the required validation

The next field we'll add is Required?, which is a Boolean field that lets us indicate if responding to the given Question is required or not.

Adding a boolean field, Required? to the Question model

Finally, we'll add the Image URL field. This field uses the URL type, which comes with a special URL validation that parses the input and ensures it is in a valid URL format.

As this field will be optional, that's the only validation we'll need.

Image URL validation for the Image URL field on the Question model

Now that Question is set, we're going to need some Answers. On to the next model!

Answer

By now, you should be getting a feel for how the Gadget schema editor works and how quickly you can build expressive models with exactly the fields and logic you need. Next on our list, our Answer model needs just two type-based fields: a Text field and a Sequence field. Our Text field will be a String type field with the Required validation, as our Answer needs to have a text body for users to identify which Answer to choose. Our Sequence field is identical to how we configured it for the Question model; a Number field with no decimals, a default value of 1, and the Required validation. Take a moment to add those two fields to our Answer model, and we can move right along to the Result model.

Result

Our Quiz now has a Question model and an Answer model, which means we can now create the Result model to represent the outcome of a given set of Answers for a Quiz. The Result model is also how we'll connect outcomes to product recommendations once we make our relationship connections. Result has just two type-based fields: a required Body String-type field to represent the outcome, and an optional Image URL URL field with the URL validation, if you wish to provide an image as part of the Result.

Adding fields to the Result model

Response

Our final model for our Quiz app is the Response model. As discussed at the beginning of this tutorial, the Response model represents an instance of taking the Quiz and allows us to track the progress of any given user who has begun taking the Quiz. It will also be a wrapper model that lets us serve a Result to a user by storing the Answers a user has selected and then calculating the appropriate Result.

We're going to add two fields to this model: an Email field to log emails for marketing purposes and a Conversion State field, which will hold what stage of the Quiz the given Response has progressed to.

Adding ConversionState to Response

As in the above screenshot, our Conversion State field is a String field, and we're going to give the field a default value of “New” and make this field required. This way, we have the state for each Response from the start through to the finish.

The Email field type, just like the URL field type, has a built-in validation to ensure the value supplied to this field is in the correct format. Therefore, we'll leave this field optional.

That's all for model building!

Thinking back to our app's relationship diagram, we know we need to link our models together to represent the conceptual connections they share. This brings us to our next step:

Bringing it all together: Relationships

Now that our models are all established, we can connect them using Relationship fields.

First, let's navigate back to the Quiz model in the schema editor. Then, let's add a Questions field to represent the connection of instances of the Question model to an instance of Quiz:

Adding the Questions field to the Quiz model

Adding a Relationship field is much like adding a Type-based field. Near the bottom of the selection list for the field type, we see Relationships listed. These Relationships and their definitions are similar to the Active Record concept of Associations. If you want to dive deeper into how Relationships work in Gadget, you can read our Relationship and Relationship Fields documentation. For now, we can move forward with the understanding that we can declare a relationship, and Gadget takes care of linking the models together for us without us needing to create and manage foreign keys.

In the case of Questions, we know already that one Quiz has many Questions. So, we can model this relationship using the HasMany Relationship field. Selecting this relationship type allows us to then select what model is the child model:

Quiz with the has many relationship type

Once we select Question as the child of Quiz, the schema editor allows us to model what the inverse of the relationship looks like, giving us finer control of the API identifier for this relationship in the generated schema. We'll just refer to the inverse of the relationship as Quiz, so the relationship is then Quiz HasMany Questions, and Question BelongsTo Quiz.

Choosing the reciprocal relationship for Quiz has many Questions

The other two relationships to model on Quiz are Result and Response. Exactly like Question, a Quiz has many Result objects through a Results field, and a Quiz has many Response through a Responses field. You can call the inverse field for both of these relationships Quiz.

If we move over to the Question model now, we'll see that Gadget has created a Quiz field on Question for us, linking a Question to one Quiz. In addition to being a child of Quiz, Question is a parent model to the Answer model. A Question can have one-to-many Answers, so we can add an Answers field to our Question model that represents this relationship. Go ahead and add this field now:

Question has many Answers

Answers, as a model, is a child of multiple models. We'll model these relationships through the parent models, so we can leave Answers as-is and proceed on to Result.

Result is another model that is both a child and a parent in our relationship mapping. We'll model the parent side of these relationships:

Result Relationship Fields

A Result has many Answer objects, as described by an Answers field, and has many Response objects through Responses. This second relationship may seem strange; if we know that Response is an object that wraps and returns Result, why is Result the parent? This allows us to model that a given Result can be linked to many Responses, as every completed instance of Response does return a Result. Otherwise, we'd have to generate a unique Result record for every Response record.

The other relationship to highlight here is a field called Product Suggestion. This field represents the link between a Result and the Shopify Product Variant we're recommending based on the Answers in a given Response. We can declare this relationship from the child side.

First, we select the BelongsTo Relationship type and find Shopify Product Variant in the Parent selection:

Adding the Product Suggestion Relationship FieldResult belongs to Shopify Product Variant as Product Suggestion

Once selected, Gadget requires us to create the inverse relationship field on the Shopify Product Variant model. For our app, we're going to pick HasMany Result via a Results field, and that will complete the connection.

Interestingly, this means we've now extended the Shopify Product Variant model beyond what Shopify provides. These additional fields on this connected model are only visible on the Gadget side of the connection and do not sync back to Shopify. Instead, these fields allow us to decorate connection-based models with whatever additional information or logic we may need for our apps, such as relationships. For more on how you can extend Shopify-provided models with Gadget, check out our guide on the Shopify connection.

Finally, let's look at the Response model's relationships. We already have two established for us, as Response BelongsTo both a Quiz and a Result. We can add one more relationship here to complete our relationship mapping: Response HasMany Answer records via Answers.

Response has many Answers

With our models all connected, the schema of our app is complete. We have all the fields and relationships needed to build out our app's UI, which we will do in a minute. First, however, is one last puzzle piece: how does a Response get a Result? To answer this, we're going to need to look at the behaviour of the Response model and employ the use of a Code Effect.

Code Effects: Calculating the Results

We discussed earlier that Gadget creates a CRUD API for you as you generate and decorate your models. While this is useful, sometimes you need more than just CRUD to build your app. Gadget allows you to extend the CRUD actions with logic through code effects, enabling these actions to unlock more functionality for your app as needed.

Looking at the sidebar menu, we'll see that our currently selected model for the schema editor has two icons: a head with gears and a server stack. The head with gears is our Behavior icon, and the server stack is our Data icon, linking to the data viewer. Let's select the Behavior icon and open the Behavior editor:

The Behavior icon

The Behavior editor has two panes: the State Machine on the left and the Actions and States menu on the right. Together, these allow us to add extra logic to standard CRUD actions or add new states and API actions to the model's interface.

The State Machine for the Response model

In our app, the Response record will update through the following user actions: starting the quiz, submitting a response to a quiz, and receiving a result. We can use the Conversion State field on the Response model to capture these different user states. We need to add some custom code that will be run every time the Update action is fired. This custom code will execute logic that attaches a Result record to the corresponding Response record. However, we only want to do this if we successfully commit this Response record to Gadget, so we should add it as a Success Effect.

Let's open up the Update action and add a Run Code Snippet effect on Success, and name it “calculateResult.js”:

Success Effects

A new page icon will appear: click that, and we'll be redirected to the code editor to build our effect.

Our code snippet will look like the following:

JavaScript
1/**
2 * Effect code for Update on Response
3 * @typedef { import("gadget-server").UpdateResponseActionContext } UpdateResponseActionContext
4 * @param {UpdateResponseActionContext} context - Everything for running this effect, like the api client, current record, params, etc
5 */
6module.exports = async ({ api, record, params, logger }) => {
7 if (record.conversionState == "quiz completed") {
8 const potentialResults = await api.answer.findMany({
9 filter: {
10 response: { isSet: true },
11 },
12 select: {
13 id: true,
14 result: {
15 id: true,
16 },
17 response: {
18 id: true,
19 },
20 },
21 });
22
23 const filteredResults = [];
24 potentialResults.forEach((p) => {
25 if (
26 p.response &&
27 parseInt(p.response.id) === parseInt(record.id) &&
28 p.result
29 ) {
30 filteredResults.push(parseInt(p.result.id));
31 }
32 });
33
34 // In the case where the mode of filteredResults is bi-modal
35 // or multi-modal, select the first result as our successful result
36 // (arbitrary selection)
37 const result = mode(filteredResults)[0];
38 if (result) {
39 const updatedRecord = await api.response.update(record.id, {
40 response: {
41 result: {
42 _link: result.toString(),
43 },
44 conversionState: "result mapped",
45 },
46 });
47 return updatedRecord;
48 }
49 }
50
51 return true;
52};
53
54function mode(numbers) {
55 // as result can be bimodal or multi-modal,
56 // the returned result is provided as an array
57 // mode of [3, 5, 4, 4, 1, 1, 2, 3] = [1, 3, 4]
58
59 const modes = [];
60 const count = [];
61 let i;
62 let number;
63 let maxIndex = 0;
64
65 for (i = 0; i < numbers.length; i += 1) {
66 number = numbers[i];
67 count[number] = (count[number] || 0) + 1;
68 if (count[number] > maxIndex) {
69 maxIndex = count[number];
70 }
71 }
72
73 for (i in count)
74 if (count.hasOwnProperty(i)) {
75 if (count[i] === maxIndex) {
76 modes.push(Number(i));
77 }
78 }
79
80 return modes;
81}

Copy and paste the above code into your calculateResult.js, and let's walk through this snippet at a high level:

  • We check that the updated record has a specific conversion state of “quiz completed.”
    • This is one of the conversion states we'll specify through the API and represents a response state where the user has selected their answers and submitted their response for a result.
  • Then, we find the applicable Answers for the given Response, and:
    • Find the most common Result ID between the Answers to declare as the Response Result.
    • We then link this Result to the Response record.
    • Then, we update the conversion state on the Response record to reflect the mapping of the Result to the Response.
    • Finally, return the updated Response record.

The mode function below our exported module is the math we use to calculate the most common Result in the list of potential Results.

With our snippet in place, our models created and relationships connected, we're ready to consume our app's API and build our front end!

Building the UI

Consuming the Gadget Client with Next.js

Part of what makes Gadget so powerful is how it automatically generates API client packages for you in both JavaScript and TypeScript, making the job of consuming and interacting with your app's backend nearly effortless. We can consume our API in two ways for our app: a freestanding app hosted on Vercel with both admin- and customer-facing views (headless) and a customer-facing UI embedded in our Shopify shop's theme.

You'll want to do the Headless build regardless of your desired outcome, as you'll need the UI to build a quiz before you can serve the quiz to shoppers.

If you're going to embed your quiz in your Shopify store, you can skip the "Deploying to Vercel" section and move right along to the "Shopify Build" section. If you're planning on keeping it completely headless, you can stop after the "Deploying to Vercel" section and skip the "Shopify Build" section entirely.

Headless Build

Getting started with the UI

We'll build our freestanding app using React and Next.js and use the Polaris library for our components. This app provides both the admin-facing and customer-facing UIs; you'll just need to take a few steps to get up and running.

We're going to use Vercel to handle the copying of our front-end project and deployment of our headless app. If you've never worked with it before, Vercel is a front-end deployment and hosting platform and is particularly useful for Next.js projects.

We can use the Vercel deploy button to fork our Gadget example into a new repository using one of GitHub, GitLab, or Bitbucket. If you do not wish to use Vercel and want to deploy on a different platform, the project can be found in Gadget's examples repo.

Deploy with Vercel

You need to give your new repository a name to start. Enter a name and press the Create button to move to the next step. The Vercel setup will create a repo for you and then prompt you for an API_KEY environment variable. You can find this in Gadget by going to Settings > API Keys. Use the Default Writer Key. You can then click the Deploy button.

Shot of the Vercel setup page prompting for a repo name and an environment variable

The initial Vercel deployment will fail, and that's okay! We need to clone the project locally and make some changes to it first! Once you've copied the app into your own empty Github repo, you'll need to update the following:

  1. In the package.json, remove the @gadget-client/alida-quiz-app-2": "^1.164.0" dependency
  2. Install the required dependencies npm install - this already includes Gadget's React bindings NPM package npm install @gadgetinc/react react
  3. Register the Gadget NPM registry for the @gadget-client package scope npm config set @gadget-client:registry https://registry.gadget.dev/npm
  4. Install your client's NPM package, npm install @gadget-client/<YOUR-GADGET-PROJECT-NAME>, and ensure it is now in the package.json as a dependency - documentation on how to install this package can be found in the Installing section of the API Reference docs
  5. Add an .env file to handle your API key locally API_KEY=[YOUR API KEY] with your app's API key for writing to production, which you can find in Gadget under Settings > API Keys
  6. In api.js, you'll need to update the client import declaration to reference your client package; import { Client } from "@gadget-client/[YOUR CLIENT HERE]"; on line 1

Once that's complete, you may want to run a quick npx next in your terminal to boot your app locally and ensure you've replaced all values as expected. Then, when the app is running locally at http://localhost:3000, we can test our app and make our first quiz!

Making our first Quiz

Now for the fun part, making a quiz! Take a moment and make a quiz with your app; you can use our demo quiz as inspiration or create your own!

Once your quiz is complete with questions, answers, and results, go ahead and take your quiz.

Now, we can look at records in Gadget and see how our front-end app connects with Gadget through the client and makes API calls against it. If we look at the Quiz data by selecting the Data icon on the Quiz model in the left-hand sidebar, we should see at least one instance of Quiz, including its ID, title, and body. We can also inspect our other records to see how our pieces work together to create our quiz experience.

When you've got a quiz that you're happy with, note the ID of the quiz, if you're planning on building the quiz right into your Shopify store. Otherwise, let's deploy our app to Vercel.

Deploying on Vercel

Good news! Vercel will handle the deployment of your app for you as you update your remote git repository. So push your changes up to your master branch and Vercel will take care of the rest!

We're now left with one final task: embedding our quiz in Shopify.

Shopify Build

Installing in the Shopify theme

While we used an NPM package to install our client into our freestanding app, we'll need another method of calling the client in our Shopify shop's theme. Gadget allows us to call our API client directly with a script tag.

We only need the client to run to serve the desired product recommendation quiz. In this case, we'll make a new template for the Page resource and then use it on a page we'll create to hold the quiz.

In your Shopify admin for your shop, head to Online Store > Themes and select Edit Code under the Actions menu for the theme you wish to edit.

The Shopify Theme Editor

Under Templates, select “Add a new template” and add a template called page.quiz.json.

Replace the generated file with the following JSON:

JSON
1{
2 "sections": {
3 "main": {
4 "type": "quiz-page",
5 "settings": {
6 }
7 }
8 },
9 "order": [
10 "main"
11 ]
12}

Next, under Sections, create a new section called quiz-page.liquid. This will be the content that the page.quiz.json file returns.

We're going to replace this page with the following code:

HTML
1<link rel="stylesheet" href="{{ 'section-main-page.css' | asset_url }}" media="print" onload="this.media='all'">
2<link rel="stylesheet" href="{{ 'component-rte.css' | asset_url }}" media="print" onload="this.media='all'">
3
4<script src="YOUR DIRECT SCRIPT TAG URL HERE"></script>
5<script src="{{ 'product-quiz.js' | asset_url }}" defer="defer"></script>
6<noscript>{{ 'section-main-page.css' | asset_url | stylesheet_tag }}</noscript>
7<noscript>{{ 'component-rte.css' | asset_url | stylesheet_tag }}</noscript>
8
9<div class="page-width page-width--narrow">
10 <h1 class="main-page-title page-title h0">
11 {{ page.title | escape }}
12 </h1>
13 <div class="rte">
14 {{ page.content }}
15 </div>
16 <div>
17 <product-quiz class="quiz">
18 <form action="post" class="form" novalidate="validate">
19 <h2 class="product-quiz__title">Loading...</h2>
20 <div class="product-quiz__body">
21 <span>
22
23 </span>
24 </div>
25 <div class="product-quiz__questions" id="questions">
26 <div class="product-quiz__question">
27 <span class="product-quiz__question-answer">
28 </span>
29 </div>
30 </div>
31 <button
32 type="submit"
33 class="product-quiz__submit button button--secondary"
34 >
35 Get my results!
36 </button>
37 </form>
38 </product-quiz>
39 </div>
40</div>
41
42{% schema %}
43{
44"name": "t:sections.quiz-page.name",
45"tag": "section",
46"class": "spaced-section"
47}
48{% endschema %}

We just need to replace the "YOUR DIRECT SCRIPT TAG URL HERE" with your app's script tag so we can use the client. Your app's script tag URL can be found in the Installing section of the API Reference docs.

Using our client with JavaScript

Under the Assets section in the sidebar, select Add a new asset and create a new JavaScript file called product-quiz.js. You can then add the following to that file, ensuring you replace "YOUR API KEY" with your API key:

JavaScript
1// initialize an API client object
2// Ensure you replaced the url for the <script/> tag YOUR DIRECT SCRIPT TAG URL HERE from the Installing page of your app's docs and replace YOUR API KEY with your API key from the Gadget editor
3const api = new Gadget({ authenticationMode: { apiKey: "YOUR API KEY" } });
4
5async function updateAnswers(answers, response) {
6 const updatedAnswers = await answers.forEach((answer) => {
7 api.mutate(
8 `
9 mutation($id: GadgetID!, $answer: UpdateAnswerInput) {
10 updateAnswer(id: $id, answer: $answer) {
11 success
12 answer {
13 id
14 response {
15 id
16 state
17 conversionState
18 createdAt
19 email
20 result {
21 id
22 state
23 body
24 createdAt
25 imageUrl
26 productSuggestion {
27 id
28 price
29 title
30 }
31 quiz {
32 id
33 state
34 body
35 createdAt
36 title
37 updatedAt
38 }
39 updatedAt
40 }
41 }
42 sequence
43 text
44 }
45 }
46 }`,
47 {
48 id: answer,
49 answer: {
50 response: {
51 _link: response.id,
52 },
53 },
54 }
55 );
56 });
57
58 return updatedAnswers;
59}
60
61async function createResponse(quiz) {
62 const response = await api.mutate(
63 `
64 mutation ( $response: CreateResponseInput) { createResponse(response: $response) {
65 success
66 errors {
67 message
68 ... on InvalidRecordError {
69 validationErrors {
70 apiIdentifier
71 message
72 }
73 }
74 }
75 response {
76 __typename
77 id
78 state
79 answers {
80 edges {
81 node {
82 id
83 state
84 createdAt
85 question {
86 id
87 state
88 body
89 createdAt
90 imageUrl
91 required
92 sequence
93 title
94 updatedAt
95 }
96 }
97 }
98 }
99 conversionState
100 createdAt
101 email
102 quiz {
103 id
104 state
105 body
106 createdAt
107 title
108 updatedAt
109 }
110 updatedAt
111 }
112 }
113 }
114`,
115 { response: { quiz: { _link: quiz.id }, conversionState: "in progress" } }
116 );
117 return response;
118}
119
120async function updateResponse(response) {
121 const updatedResponse = await api.mutate(
122 `mutation ($id: GadgetID!, $response: UpdateResponseInput) {
123 updateResponse(id: $id, response: $response) {
124 success
125 errors {
126 message
127 ... on InvalidRecordError {
128 validationErrors {
129 apiIdentifier
130 message
131 }
132 }
133 }
134 response {
135 __typename
136 id
137 state
138
139 conversionState
140 createdAt
141 email
142 quiz {
143 id
144 state
145 body
146 createdAt
147 title
148 updatedAt
149 }
150 result {
151 id
152 state
153 body
154 createdAt
155 imageUrl
156 productSuggestion {
157 id
158 price
159 title
160 product {
161 title
162 handle
163 body
164 images {
165 edges {
166 node {
167 source
168 }
169 }
170 }
171 }
172 }
173 quiz {
174 id
175 state
176 body
177 createdAt
178 title
179 updatedAt
180 }
181 updatedAt
182 }
183 updatedAt
184 }
185 }
186}
187`,
188 { id: response.id, response: { conversionState: "quiz completed" } }
189 );
190 return updatedResponse;
191}
192
193async function fetchQuiz() {
194 const quiz = await api.query(`query getOneQuiz {
195quiz (id: [YOUR QUIZ ID]) {
196 id,
197 title,
198 body,
199 questions {
200 edges {
201 node {
202 id,
203 title,
204 body,
205 imageUrl,
206 required,
207 sequence,
208 answers {
209 edges {
210 node {
211 id,
212 text,
213 sequence,
214 question {
215 id,
216 },
217 },
218 },
219 },
220 },
221 },
222 },
223 results {
224 edges {
225 node {
226 id,
227 state,
228 body,
229 imageUrl,
230 productSuggestion {
231 id,
232 price,
233 title,
234 product {
235 title,
236 handle,
237 },
238 },
239 },
240 },
241 },
242 },
243}`);
244
245 return quiz;
246}
247
248let selectedAnswers = [];
249function selectAnswer(answer) {
250 selectedAnswers.push(answer);
251 let elId = event.srcElement.id;
252 let parent = document.getElementById(elId).parentNode;
253 parent.innerHTML = "<h3>Answer selected</h3>";
254}
255
256fetchQuiz().then(function (quiz) {
257 const quizData = quiz.quiz;
258 const questions = quizData.questions.edges;
259
260 if (!customElements.get("product-quiz")) {
261 customElements.define(
262 "product-quiz",
263 class ProductQuiz extends HTMLElement {
264 constructor() {
265 super();
266
267 this.form = this.querySelector("form");
268 this.heading = this.querySelector("form h2");
269 this.heading.innerHTML = quizData.title;
270 this.body = this.querySelector(".product-quiz__body span");
271 this.body.innerHTML = quizData.body;
272 this.questions = this.querySelector(".product-quiz__questions");
273 const questionContainer = this.querySelector(".product-quiz__question");
274 const answerContainer = this.querySelector(
275 ".product-quiz__question-answer"
276 );
277
278 let renderedQuestions = questions
279 .sort((a, b) => a.node.sequence - b.node.sequence)
280 .forEach((question, i) => {
281 let clonedDiv = questionContainer.cloneNode(true);
282 clonedDiv.id = "question_" + i;
283 clonedDiv.insertAdjacentHTML(
284 "beforeend",
285 "<div><h3>" + question.node.title + "</h3><br/></div>"
286 );
287 this.questions.appendChild(clonedDiv);
288 let answers = question.node.answers.edges;
289 answers
290 .sort((a, b) => b.node.sequence - a.node.sequence)
291 .forEach((answer, j) => {
292 let clonedSpan = answerContainer.cloneNode(true);
293 clonedSpan.id = "answer_" + i + "_" + j;
294 clonedSpan.insertAdjacentHTML(
295 "beforeend",
296 '<span><a class="button answer" id="' +
297 clonedSpan.id +
298 '" onClick=(selectAnswer(' +
299 answer.node.id +
300 "))>" +
301 answer.node.text +
302 "</a><br/></span><br/> "
303 );
304 clonedDiv.appendChild(clonedSpan);
305 });
306 });
307
308 this.form.addEventListener("submit", this.onSubmitHandler.bind(this));
309 }
310
311 onSubmitHandler(evt) {
312 evt.preventDefault();
313
314 const submitButton = this.querySelector(".product-quiz__submit");
315
316 submitButton.setAttribute("disabled", true);
317 submitButton.classList.add("loading");
318
319 createResponse(quiz).then(function (response) {
320 const currentResponse = response.createResponse.response;
321
322 updateAnswers(selectedAnswers, currentResponse).then(function (results) {
323 updateResponse(currentResponse).then(function (updatedResponse) {
324 const finalResponse = updatedResponse.updateResponse.response;
325
326 if (finalResponse) {
327 const result = finalResponse.result;
328 console.log(finalResponse);
329
330 if (result) {
331 const imgUrl =
332 result.productSuggestion.product.images.edges[0].node.source;
333 const productLink = result.productSuggestion.product.handle;
334 const resultHTML =
335 `<div><h3>` +
336 result.body +
337 " - " +
338 result.productSuggestion.product.title +
339 `</h3><br/><p><img src=` +
340 imgUrl +
341 ` width="50%" height="50%"/><br/> <p>` +
342 result.productSuggestion.product.body +
343 `</p></br><a class="button" href="/products/` +
344 productLink +
345 `">Check it out!</a></div>`;
346 document.getElementById("questions").innerHTML = resultHTML;
347 submitButton.classList.remove("loading");
348 submitButton.classList.add("hidden");
349 }
350 }
351 });
352 });
353 });
354 }
355 }
356 );
357 }
358});

You'll need to make one adjustment here: in the quiz query, you just need to replace (id: [YOUR QUIZ ID]) with the ID of the quiz you want to return. Save your changes, and we're ready to go! Head over to the Pages section of the Shopify admin, and create a new page for your quiz. You can add whatever title and body content you may want for the page and then set the template to use your new quiz template.

Shopify's Page editor

Once you save this page, you're all done! View the page to see your quiz right in your Shopify store, ready to recommend products to your shoppers.

And we're done! Here's the final quiz in Shopify.

Conclusion

Today, you've learned how Gadget and Shopify can work together to create engaging buying experiences for your shoppers while providing an approachable platform to build your app in a fraction of the time it takes to do so from scratch. Feel free to expand on this app; since we have the Product Variant ID of the recommended product, we can construct a cart for the shopper on the front-end using Javascript, enabling a faster buying experience. Additionally, you could use Gadget's built-in authentication to add a login layer to the admin UI, add editing functionality to the quiz builder, and more!

Want to know more about building effortless, expressive apps with Gadget? Check out our Guides and get building today!

Need support? Join our Discord, or book office hours with our Developer Advocate team!