How to build a restaurant listing UI for Strapi using GC & GraphQL
Published: 2021-06-10 17:07:17

## Intro

This article is dedicated for a web developer who appreciates design freedom, yet who'd like to code less in a setup-free web-based development environment.

It's a "how to" integrate with [Strapi](https://strapi.io/) using [GlueCodes Studio](https://www.glue.codes) - the tool powering your every-day work in the ways you haven't seen elsewhere. It's for somebody who'd be pleased with loads of automation to deliver an extremely fast and scalable code i.e. build-time diffed JSX using [SolidJS](http://solidjs.com/) and organised around an implicit uni-directional data flow. Obviously you can use it for FREE. Without further "context drawing", let's begin.

## What are we building?

We're going to use [Strapi](https://strapi.io/) as a headless CMS. It comes with a hosted demo for an imaginary Food Advisor site and it's already seeded with restaurant data. You can request your instance [here](https://strapi.io/demo). After filling in a form, you'll receive an email with few URLs. Mine looked like these:

```
Demo URL: https://api-hi2zm.strapidemo.com/admin

API restaurants URL: https://api-hi2zm.strapidemo.com/restaurants

GraphQL URL: https://api-hi2zm.strapidemo.com/graphql

Credentials: john@doe.com / welcomeToStrapi123
```

> Don't try to be a smartass, the URLs won't work longer that the demo duration you provided in the demo form.

I won't be covering how to use Strapi, just explore it yourself if you like. For our tutorial all you'll need is these two URLs:

```
GraphQL:
https://api-{someHash}.strapidemo.com/graphql

Image Server:
https://api-{someHash}.strapidemo.com
```
 
Our app will have the following features:

- grid of restaurants with names, description, category and image
- filtering by category
- filtering by neighborhood 
- filtering by language
- pagination

The app will apply the filters without the browser hard-reload, meaning it'll be SPA. In Part 1, we will focus on the Strapi integration and leave pagination and mobile responsiveness for Part 2. I'll leave any styling improvements to you as it isn't a CSS tutorial. It'll look like this:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iqx1am10w03t1ugn3tzf.png)

## Coding

First, you'll need go to: [GlueCodes Studio](https://ide.glue.codes/?openedTab=demos). You'll be asked to sign up via Google or Github. No worries, it won't require any of your details. Once you're in the project manager, choose "Strapi Food Advisor" template. You'll be asked to choose a directory where the project suppose to be stored. Just choose one and you should be redirected to IDE.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/85n3i1tcyv856oc1i94b.png)

You might be welcomed with some introjs walk-through(s) guiding you around something like this:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jcvt4m53h8dxdynxk3va.png)

As mentioned above, you'll need two URLs:

```
GraphQL:
https://api-{someHash}.strapidemo.com/graphql

Image Server:
https://api-{someHash}.strapidemo.com
```

Let's add them to Global Variables as `GQL_URL` and `IMAGE_BASE_URL`:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dlayx7lnfl63hkwg8lou.png)

Now you can click "Preview" to see the working app.

### App data flow design

We'll need a list of restaurants pulled from Strapi's GraphQL API. [GlueCodes Studio](https://www.glue.codes) has a built-in data flow management. Your business logic is spread across app actions which store their returned/resolved values in a single object store. The data changes flow in one direction and UI reacts to changes of the store, updating the only affected parts. The DOM diffing happens in-compilation time and is powered by [SolidJS](http://solidjs.com/).

There are two types of actions; the ones that supply data before rendering called providers and those triggered by a user called commands. Their both returned/resolved values are accessible from a single object store by their own names. In your UI, you get access to global variables: `actions` and `actionResults`. The variable `actions` is an object of Commands you can call to perform an action e.g. to return/resolve fetched data. You can read more in [docs](https://www.glue.codes/docs-javaScriptInGlueCodesStudio-appStateManagement.html). It's really easier done than said so bear with me.

The API call we're going use returns restaurants along with categories. Our app also needs a list of neighborhoods and parse URL query parameters to affect the GraphQL call. We'll also need some basic data transformations before passing it to our UI. Based on this information, I decided to have the following providers:

- fetchRestaurantData
- getCategories
- getLanguages
- getNeighborhoods
- getRestaurants
- parseUrlQueryParams

For filtering, we'll need the following commands:

- changeCategory
- changeLanguage
- changeNeighborhood

I'll walk you through them one by one but before, you need to understand the mechanism of providers a bit further. Note that providers, when returning they implicitly write to a single object store by their own names. Then, a snapshot of this store is passed from one provider to another. It means you can access results of the previously called providers. It also means you need to set their execution order. It's done by navigating to a particular provider and clicking "Run After" button and in its corresponding pane, choose which providers need to be executed before. You can expect something like this:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l2fnoh1c064am2smx1up.png)

We want to achieve the following pipeline:

The `fetchRestaurantData` uses a result of `parseUrlQueryParams`.

The `getRestaurants` and `getCategories` use a result of  fetchRestaurantData.

It can look like this:

1. getNeighborhoods
2. parseUrlQueryParams
3. fetchRestaurantData
4. getRestaurants
5. getLanguages
6. getCategories

OK, let's dive into functions now.

### Actions

`providers/fetchRestaurantData`:

```javascript
export default async (actionResults) => {
  const { category, district, locale } = actionResults.parseUrlQueryParams 
  
  const where = {
    locale: 'en'
  }
  
  if (category !== 'all') {
    where.category = category
  }
  
  if (district !== 'all') {
    where.district = district
  }
  
  if (locale) {
    where.locale = locale
  }
  
  const query = `
    query ($limit: Int, $start: Int, $sort: String, $locale: String, $where: JSON) {
      restaurants(limit: $limit, start: $start, sort: $sort, locale: $locale, where: $where) {
        id
        description
        district
        cover {
          url
        }
        category {
          name
        }
        name
        locale
        localizations {
          id
          locale
        }
        note
        price
        reviews {
          note
          content
        }
      }
      restaurantsConnection(where: $where) {
        aggregate {
          count
        }
      }
      categories {
        id
        name
      }
    }
  `

  const records = await (await fetch(global.GQL_URL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      query,
      variables: {
        limit: 15,
        start: actionResults.parseUrlQueryParams.start || 0,
        sort: 'name:ASC',
        locale: 'en',
        where
      }
    })
  })).json()
  
  return records.data
}

```

Notes:

- `actionResults.parseUrlQueryParams` accesses the query URL params
- `global.GQL_URL` accesses the `GQL_URL` global variable

`providers/getCategories`:

```javascript
export default (actionResults) => {
  return [
    {
      id: 'all',
      name: 'All'
    },
    ...actionResults.fetchRestaurantData.categories  
  ]
}
```

Notes:

- `actionResults.fetchRestaurantData.categories` accesses the categories which are part of `fetchRestaurantData` result

`providers/getLanguages`:

```javascript
export default () => {
  return [
    {
      id: 'en',
      name: 'En'
    },
    {
      id: 'fr',
      name: 'Fr'
    }
  ]
}
```
`providers/getNeighborhoods`:

```javascript
export default () => {
  return [
    { name: 'All', id: 'all' },
    { name: '1st', id: '_1st' },
    { name: '2nd', id: '_2nd' },
    { name: '3rd', id: '_3rd' },
    { name: '4th', id: '_4th' },
    { name: '5th', id: '_5th' },
    { name: '6th', id: '_6th' },
    { name: '7th', id: '_7th' },
    { name: '8th', id: '_8th' },
    { name: '9th', id: '_9th' },
    { name: '10th', id: '_10th' },
    { name: '11th', id: '_11th' },
    { name: '12th', id: '_12th' },
    { name: '13th', id: '_13th' },
    { name: '14th', id: '_14th' },
    { name: '15th', id: '_15th' },
    { name: '16th', id: '_16th' },
    { name: '17th', id: '_17th' },
    { name: '18th', id: '_18th' },
    { name: '19th', id: '_19th' },
    { name: '20th', id: '_20th' }
  ]
}
```

`providers/getRestaurants`:

```javascript
export default (actionResults) => {
  return actionResults.fetchRestaurantData.restaurants
    .map((record) => ({
      id: record.id,
      name: record.name,
      description: record.description,
      category: record.category.name,
      district: record.district,
      thumbnail: record.cover[0].url
    }))
}
```

Notes:

- `actionResults.fetchRestaurantData.restaurants` accesses the restaurants which are part of `fetchRestaurantData` result

`providers/parseUrlQueryParams`:

```javascript
export default (actionResults) => {
  return imports.parseUrlQueryParams()
}
```

Notes:

- `imports.parseUrlQueryParams` accesses an external dependency function. 

> In [GlueCodes Studio](https://www.glue.codes) you can use any UMD-bundled modules including those in [UNPKG](https://unpkg.com/). Just click on Dependencies icon and edit the JSON file to look like:

```json
{
  "css": {
    "bootstrap": "https://unpkg.com/bootstrap@4.5.2/dist/css/bootstrap.min.css",
    "fa": "https://unpkg.com/@fortawesome/fontawesome-free@5.14.0/css/all.min.css"
  },
  "js": {
    "modules": {
      "parseUrlQueryParams": "https://ide.glue.codes/repos/df67f7a82cbdc5efffcb31c519a48bf6/basic/reusable-parseUrlQueryParams-1.0.4/index.js",
      "setUrlQueryParam": "https://ide.glue.codes/repos/df67f7a82cbdc5efffcb31c519a48bf6/basic/reusable-setUrlQueryParam-1.0.4/index.js"
    },
    "imports": {
      "parseUrlQueryParams": {
        "source": "parseUrlQueryParams",
        "importedName": "default"
      },
      "setUrlQueryParam": {
        "source": "setUrlQueryParam",
        "importedName": "default"
      }
    }
  }
}
```

`commands/changeCategory`:

```javascript
export default (categoryId) => {
  imports.setUrlQueryParam({ name: 'category', value: categoryId })
}
```

Notes: 

- `imports.setUrlQueryParam` accesses an external dependency function

`commands/changeLanguage`:

```javascript
export default (languageId) => {
  imports.setUrlQueryParam({ name: 'locale', value: languageId })
}
```

`commands/changeNeighborhood`:

```javascript
export default (neighborhoodId) => {
  imports.setUrlQueryParam({ name: 'district', value: neighborhoodId })
}
```

### Structure

In [GlueCodes Studio](https://www.glue.codes) each page is split into logical UI pieces to help you keep your UI modular. A single slot has its scoped CSS which means it can be styled by classes which only affect a given slot and their names can be duplicated in other slots. In the exported code, slots will be extracted to dedicated files making them more maintainable.

To make your HTML dynamic, you can use attribute directives as you would in modern web frameworks. When typing most of them, you'll get notified to auto-create (if don't exist) required commands, providers or to install a widget. The vocabulary is quite simple, attribute `[gc-as]` tells what it is and other `[gc-*]` attributes are parameters. Note: For any naming attributes use camelcase e.g. for a slot you would use `[gc-name="myAwesomeSlot"]`.

Here is a slightly stripped-out index page HTML:

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <meta gc-as="navName" gc-name="Home">
  <title>FoodAdvisor</title>
<body>
  <div gc-as="layout">
    <div class="container-fluid">
      <div gc-as="slot" gc-name="header"></div>
      <div class="d-flex">
        <div gc-as="slot" gc-name="filters"></div>
        <div gc-as="slot" gc-name="content">
          <div class="contentWrapper">
            <h1 class="heading">Best restaurants in Paris</h1>
            <div class="grid">
              <div gc-as="listItemPresenter" gc-provider="getRestaurants" class="card">
                <img-x class="card-img-top thumbnail" alt="Card image cap">
                  <script>
                    props.src = `${global.IMAGE_BASE_URL}${getRestaurantsItem.thumbnail}`
                  </script>
                </img-x>
                <div class="card-body">
                  <h4 gc-as="listFieldPresenter" gc-provider="getRestaurants" gc-field="name" class="name">restaurant name</h4>
                  <h5 gc-as="listFieldPresenter" gc-provider="getRestaurants" gc-field="category" class="category">restaurant category</h5>
                  <p gc-as="listFieldPresenter" gc-provider="getRestaurants" gc-field="description" class="card-text">restuarant description</p>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
      <div gc-as="slot" gc-name="footer"></div>
    </div>
  </div>
</body>
</html>
```

Notes:

- `<div gc-as="layout">` is the app wrapper.
- `<div gc-as="slot" gc-name="content">` is a logical UI piece which has its scoped CSS and is extracted to dedicated file. It requires a unique (within page) camelcase gc-name. Whatever is in slot gets access to a store, commands and other useful variables. You can learn more [here](https://www.glue.codes/docs-templating-htmlDirectives.html).
- `<div gc-as="slot" gc-name="filters"></div>` is a reusable slot. Similar to a slot however it can be used across multiple pages. Reusable slots can be understood as partials. You'll be editing reusable slots in a dedicated HTML editor and injecting them in pages using empty slot directive.
- `<div gc-as="listItemPresenter" gc-provider="getRestaurants" class="card">` repeats this div over an array returned by `getRestaurants` provider.
- `<h4 gc-as="listFieldPresenter" gc-provider="getRestaurants" gc-field="name" class="name">restaurant name</h4>` displays a property `name` of an item while looping over `getRestaurants` provider.

Let's take a look at this once more:

```html
<img-x class="card-img-top thumbnail" alt="Card image cap">
  <script>
    props.src = `${global.IMAGE_BASE_URL}${getRestaurantsItem.thumbnail}`
  </script>
</img-x>
```

Static HTML has no built-in way to make it reactive. Hence [GlueCodes Studio](https://www.glue.codes) has a concept called extended tags which is named like: `tagName + '-x'` and has an embedded `<script>` included. Its code is sandboxed allowing you to access variables which are available inside other directive like slots or list item presenters. The scripts can assign to `props` variable to change props/attributes of the extended tag.

> Note that when an extended tag is placed inside a list item presenter you get access to a variable called like: `providerName + Item`, in our case `getRestaurantsItem` which is an item while looping over `getRestaurants` provider. You could also access `getRestaurantsIndex` for a numeric index in the array.

Other Templates:

`reusableSlots/filters`:

```html
<div class="wrapper">
  <h2 class="heading">Categories</h2>
  <ul class="filterSet">
    <li gc-as="listItemPresenter" gc-provider="getCategories" class="filterItem">
      <label>
        <input-x type="radio">
          <script>
            props.name = 'category'
            props.value = getCategoriesItem.id
            props.checked = getCategoriesItem.id === (actionResults.parseUrlQueryParams.category || 'all')
            props.onChange = (e) => {
              actions.changeCategory(e.target.value)
              actions.reload()
            }
          </script>
        </input-x>
        <span gc-as="listFieldPresenter" gc-provider="getCategories" gc-field="name" class="label">category name</span>
      </label>
    </li>
  </ul>
  <h2 class="heading">Neighborhood</h2>
  <ul class="filterSet">
    <li gc-as="listItemPresenter" gc-provider="getNeighborhoods" class="filterItem">
      <label>
        <input-x type="radio">
          <script>
            props.name = 'neighborhood'
            props.value = getNeighborhoodsItem.id
            props.checked = getNeighborhoodsItem.id === (actionResults.parseUrlQueryParams.district || 'all')
            props.onChange = (e) => {
              actions.changeNeighborhood(e.target.value)
              actions.reload()
            }
          </script>
        </input-x>
        <span gc-as="listFieldPresenter" gc-provider="getNeighborhoods" gc-field="name" class="label">neighborhood name</span>
      </label>
    </li>
  </ul>
  <h2 class="heading">Language</h2>
  <ul class="filterSet">
    <li gc-as="listItemPresenter" gc-provider="getLanguages" class="filterItem">
      <label>
        <input-x type="radio">
          <script>
            props.name = 'languages'
            props.value = getLanguagesItem.id
            props.checked = getLanguagesItem.id === (actionResults.parseUrlQueryParams.locale || 'en')
            props.onChange = (e) => {
              actions.changeLanguage(e.target.value)
              actions.reload()
            }
          </script>
        </input-x>
        <span gc-as="listFieldPresenter" gc-provider="getLanguages" gc-field="name" class="label">language name</span>
      </label>
    </li>
  </ul>
</div>
```

`reusableSlots/footer`:

```html
<footer class="wrapper">
  <p>Try <a href="https://www.glue.codes" class="link">GlueCodes Studio</a> now!</p>
  <ul class="nav">
    <li class="navItem">
      <a href="https://www.facebook.com/groups/gluecodesstudio" class="navLink"><i class="fab fa-facebook"></i></a>
    </li>
    <li class="navItem">
      <a href="https://www.youtube.com/channel/UCDtO8rCRAYyzM6pRXy39__A/featured?view_as=subscriber" class="navLink"><i class="fab fa-youtube"></i></a>
    </li>
    <li class="navItem">
      <a href="https://www.linkedin.com/company/gluecodes" class="navLink"><i class="fab fa-linkedin-in"></i></a>
    </li>
  </ul>
</footer>
```

`reusableSlots/header`:

```html
<nav class="navbar navbar-light bg-light wrapper">
  <a class="navbar-brand link" href="/">
    <img-x width="30" height="30" alt="FoodAdvisor" class="logo">
      <script>
        props.src = mediaFiles['logo.png'].src
      </script>
    </img-x> FoodAdvisor
  </a>
</nav>
```

> You can access any images or videos you drop in the studio via `mediaFiles` variable which is an object where file names are the keys. Implicitly there is a [Webpack Responsive Loader](https://github.com/dazuaz/responsive-loader) involved which gives you `src` and `placeholder`.

### Styles

For styling, although it feels like coding oldschool HTML and CSS, you'll be implicitly using [CSS Modules](https://github.com/css-modules/css-modules). [GlueCodes Studio](https://www.glue.codes) gives you a beautiful balance between scoped and global styling. So, you can theme your app globally and at the same time style chosen parts of the UI in isolation. You'll simply be using CSS classes and because of the implicit scoping you can safely duplicate class names among different slots.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xdof1lqr67kuasf9uzql.png)

> Notice a rather unusual `@import` statements. It's a way of importing third-party CSS from dependencies or global styles. The names must match the ones in Dependencies JSON or name of a global stylesheet.

`pages/index/This Page CSS`

```css
@import 'bootstrap';
```

`pages/index/Content Slot CSS`

```css
@import 'bootstrap';
@import 'fa';
@import 'theme';

.contentWrapper {
  padding: 0 20px;
}

.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-gap: 30px;
  margin-top: 40px;
}

.heading {
  margin-bottom: 0;
  font-size: 32px;
}

.thumbnail {
  transition: transform 0.3s;
}

.thumbnail:hover {
  transform: translateY(-4px); 
}

.name {
  font-weight: 700;
  font-size: 16px;
  color: rgb(25, 25, 25);
}

.category {
  font-size: 13px;
  color: #666;
}
```

`reusableSlots/filters`:

```css
.wrapper {
  padding: 0 20px;
  padding-top: 75px;
  min-width: 250px;
}

.filterSet, .filterItem {
  margin: 0;
  padding: 0;
}

.filterSet {
  margin-bottom: 30px;
}

.filterItem {
  list-style: none;
}

.filterItem label {
  cursor: pointer;
}

.label {
  padding-left: 4px;
}

.heading {
  padding-bottom: 15px;
  font-weight: 700;
  font-size: 16px;
  color: rgb(25, 25, 25);
}
```

`reusableSlots/footer`:

```css
@import 'fa';

.wrapper {
  margin-top: 70px;
  padding: 20px;
  background-color: #1C2023;
  color: white;
}

.link {
  color: white;
}

.link:hover {
  color: #219F4D;
  text-decoration: none;
}

.nav {
  display: flex;
  margin: 0;
  padding: 0;
}

.navItem {
  list-style: none;  
}

.navLink {
  display: inline-block;
  margin-right: 2px;
  width: 40px;
  height: 40px;
  line-height: 40px;
  text-align: center;
  font-size: 18px;
  border-radius: 50%;
  background-color: #272a2e;
}

.navLink,
.navLink:hover,
.navLink:active,
.navLink.visited {
  text-decoration: none;
  color: white;
}

.navLink:hover {
  background-color: #219F4D;
}
```

`reusableSlots/header`:

```css
.wrapper {
  padding: 20px;
  background: #1C2023;
  margin-bottom: 30px;
}

.link {
  color: white;
  font-size: 18px;
  font-weight: 700;
}

.link,
.link:hover,
.link:active,
.link:visited {
  color: white;
  text-decoration: none;
}

.logo {
  margin-right: 3px;
}
```

## What's next?

As you may have noticed there is a tone of details which hopefully is reasonably absorbable. I'll share a direct link to the project soon after releasing this article. Enjoy building your custom CMSs with [GlueCodes Studio](https://www.glue.codes) and Strapi.

Let me know whether I should write Part 2 or if there is some other integration you'd love to see. 

Also, join our [Facebook Forum](https://www.facebook.com/groups/gluecodesstudio)