Skip to content

Commit

Permalink
Merge pull request #11 from BKWLD/v0.4
Browse files Browse the repository at this point in the history
V0.4
  • Loading branch information
weotch authored May 31, 2022
2 parents c28325d + 3324170 commit 2d2b848
Show file tree
Hide file tree
Showing 29 changed files with 518 additions and 75 deletions.
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ Set these properties within `cloak: { shopify: { ... } }` in the nuxt.config.js:
- `url` - Your public Shopify store URL, for example: https://brand.myshopify.com or https://shop.brand.com. Defaults to `process.env.SHOPIFY_URL`.
- `storefront:`
- `token` - The Storefront API token of your custom app. Defaults to `process.env.SHOPIFY_STOREFRONT_TOKEN`.
- `version` - The [Storefront API version](https://shopify.dev/api/usage/versioning) to use. Defaults to `unstable` (aka, latest).
- `version` - The [Storefront API version](https://shopify.dev/api/usage/versioning) to use. Defaults to `2022-04`.
- `language` - A Storefront API recognized [LanguageCode](https://shopify.dev/api/storefront/2022-04/enums/LanguageCode). Defaults to the 1st part of `process.env.CMS_SITE` if it is ISO-like (ex: if `en_US` or `en-US` then `EN`).
- `country` - A Storefront API recognized [CountryCode](https://shopify.dev/api/storefront/2022-04/enums/CountryCode). Defaults to the 2nd part of `process.env.CMS_SITE` if it is ISO-like (ex: if `en_US` or `en-US` then `US`).
- `injectClient` - Boolean for whether to inject the `$storefront` client globally. Defaults to `true`. You would set this to `false` when this module is a depedency of another module (like [@cloak-app/algolia](https://github.com/BKWLD/cloak-algolia)) that is creating `$storefront` a different way.
- `mocks` - An array of objects for use with [`mockAxiosGql`](https://github.com/BKWLD/cloak-utils/blob/main/src/axios.js).

Expand Down Expand Up @@ -47,10 +49,12 @@ You can make an instance of the Storefront Axios client when outside of Nuxt (li

```js
import { makeStorefrontClient } from '@cloak-app/shopify/factories'
const storefront = makeStorefrontClient({
import mergeClientHelpers from '@cloak-app/shopify/factories/merge-helpers'
const storefront = mergeClientHelpers(makeStorefrontClient({
url: process.env.SHOPIFY_URL,
token: process.env.SHOPIFY_STOREFRONT_TOKEN,
})
version: '2022-04', // Optional
}))

// Optional, inject it globally into Vue components
import Vue from 'vue'
Expand Down
38 changes: 38 additions & 0 deletions demo/components/merge-demo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<!-- Demo using mocks -->

<template lang='pug'>

ul.merge-demo: li(v-for='product in products')
| {{ product.title }}

</template>

<!-- ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– -->

<script lang='coffee'>
export default

data: -> products: []

fetch: ->
@products = await @$mergeShopifyProductCards [
{ slug: 'clay-plant-pot' }
{ slug: 'copper-light' }
]

</script>

<!-- ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– -->

<style lang='stylus' scoped>

.merge-demo
border 1px dashed currentColor
padding 1em
list-style disc

li
margin-left 1em
margin-v .25em

</style>
42 changes: 34 additions & 8 deletions demo/content/demo.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,42 @@ export default
data: -> products: []
fetch: ->
{ @products } = await @$storefront.execute query: '''
query getSomeProducts {
products(first: 5) {
edges {
node { ...product }
}
fetch: -> { @products } = await @$storefront.execute query: '''
query getSomeProducts {
products(first: 5) {
edges {
node { ...product }
}
}
''' + productFragment
}
''' + productFragment
</script>
```

## Merge Example

This uses live Shopify data and the `$mergeShopifyProductCards` helper to simulate merging Shopify data into an array of product data from another source, like Craft.

<merge-demo></merge-demo>

```vue
<template lang='pug'>
ul: li(v-for='product in products')
| {{ product.title }}
</template>
<script lang='coffee'>
export default
data: -> products: []
fetch: -> @products = await @$mergeShopifyProductCards [
{ slug: 'clay-plant-pot' }
{ slug: 'copper-light' }
]
</script>
```
19 changes: 18 additions & 1 deletion demo/nuxt.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Mock stubs
import someProducts from './stubs/some-products.json'
import productsForSsgVariants from './stubs/products-for-ssg-variants.json'
import productVariantsForSsgVariants from './stubs/product-variants-for-ssg-variants.json'

// Nuxt config
export default {
Expand All @@ -8,6 +10,7 @@ export default {
buildModules: [
'@cloak-app/boilerplate',
'@cloak-app/demo-theme',
'@cloak-app/craft',
'../nuxt',
],

Expand All @@ -30,10 +33,24 @@ export default {
{
query: 'getSomeProducts',
response: someProducts,
}
},
{
query: 'getProductVariantsForSsgVariants',
response: productVariantsForSsgVariants
},
],
},

// Mock Craft queries
craft: {
mocks: [
{
query: 'getProductsForSsgVariants',
response: productsForSsgVariants
}
]
}

},

// @nuxt/content can't be loaded from module
Expand Down
2 changes: 1 addition & 1 deletion demo/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ nuxt-content(:document='page')
<script lang='coffee'>
export default

# Get page content
# Get page content
asyncData: ({ $content }) ->
page = await $content('demo').fetch()
return { page }
Expand Down
File renamed without changes.
15 changes: 15 additions & 0 deletions demo/stubs/product-variants-for-ssg-variants.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"data": {
"product": {
"variants": {
"edges": [
{
"node": {
"id": "gid://shopify/ProductVariant/10079785100"
}
}
]
}
}
}
}
10 changes: 10 additions & 0 deletions demo/stubs/products-for-ssg-variants.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"data": {
"entries": [
{
"slug": "clay-plant-pot",
"uri": "products/clay-plant-pot"
}
]
}
}
18 changes: 18 additions & 0 deletions factories/merge-helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as queryHelpers from '../helpers/query'

/**
* Merge query query helpers into the storefront object. This is done from
* a different file from the other factory files because it results in loading
* gql files that will require transpiling. And we want to make the base
* Storefront client to be useable without transpiling.
*/
export default function ($storefront) {
Object.entries(queryHelpers).forEach(([methodName, method]) => {
$storefront[methodName] = (...args) => {
return method.apply(null, [$storefront, ...args])
}
})

// Return $storefront so this can wrap Storefront factory
return $storefront
}
31 changes: 29 additions & 2 deletions factories/storefront-client-factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import mapValues from 'lodash/mapValues'
import isPlainObject from 'lodash/isPlainObject'

// Factory method for making Storefront Axios clients
export default function (axios, { url, token, version } = {}) {
export default function (axios, {
url,
token,
version = '2022-04',
language,
country,
} = {}) {

// Make Storefront instance
const storefront = axios.create({
Expand All @@ -14,9 +20,17 @@ export default function (axios, { url, token, version } = {}) {
},
})

// Store configuration on the object, for reading out externally (or mutating)
storefront.language = language
storefront.country = country

// Add execute helper for running gql queries
storefront.execute = async payload => {

// Massage the payload
const { language, country } = storefront
payload = setInContext(payload, { language, country })

// Execute the query
const response = await storefront({
method: 'POST',
Expand All @@ -36,7 +50,7 @@ export default function (axios, { url, token, version } = {}) {
return storefront
}

// Make a custom erorr object
// Make a custom error object
export class StorefrontError extends Error {
constructor(errors, payload) {

Expand All @@ -55,6 +69,19 @@ export class StorefrontError extends Error {
}
}

// Send language and country on all requests if specified, for use with
// @inContext directive
export function setInContext(payload, { language, country }) {
return {
...payload,
variables: {
language,
country,
...(payload.variables || {})
}
}
}

// Recurse through an object and flatten eddge/node levels
function flattenEdges(obj) {

Expand Down
58 changes: 58 additions & 0 deletions helpers/cards.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
###
Helpers related to mering Shopify product data into other product objects by
slug. Intended use case is when we get product listing info from Craft, designed
for rendering in cards.
###
import memoize from 'lodash/memoize'
import { getShopifyId } from './formatting'
import getProduct from '../queries/product.gql'

# Take an array of Craft product entries and merge Shopify data with them. This
# preserves the original order of the products.
export mergeShopifyProductCards = ({ execute }, products) ->
return [] unless products.length

# Get Shopify and bundle data for products. If Shopify data isn't found,
# an exception is thrown, which we catch and use to remove that product
# from the listing.
products = await Promise.all products.map (product) ->
try await mergeShopifyProductCard { execute }, product
catch error then console.warn error

# Remove all empty products (those that errored while getting data)
return products.filter (val) -> !!val

# Merge Shopify data into a single card
export mergeShopifyProductCard = memoize ({ execute }, product) ->
return unless product

# Merge Shopify data into the product object
shopifyProduct = await getShopifyProductByHandle { execute }, product.slug
product = { ...product, ...shopifyProduct }

# Remove keys not needed for cards
delete product.description

# Return the final product
return product

# Use the slug as the memoize resolver
, (deps, product) -> product?.slug

# Get the Shopify product data given a Shopify product handle
getShopifyProductByHandle = ({ execute }, productHandle) ->

# Query Storefront API
{ product } = await execute
query: getProduct
variables: handle: productHandle
unless product then throw "No Shopify product found for #{productHandle}"

# Add URLs to each variant for easier iteration later
product.variants = product.variants.map (variant) =>
variantIdNum = getShopifyId variant.id
url = "/products/#{productHandle}/#{variantIdNum}"
return { ...variant, url }

# Return the product
return product
30 changes: 30 additions & 0 deletions helpers/formatting.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import atob from 'atob-lite'

// Get the id from a Shopify gid:// style id. This strips everything but the
// last part of the string. So gid://shopify/ProductVariant/34641879105581
// becomes 34641879105581
// https://regex101.com/r/3FIplL/3
export function getShopifyId(id) {

// Already a simple ID
if (String(id).match(/^\d+$/)) return id

// De-base64. This should only be required when migrating cart ids that were
// stored in a cookie, AKA client-side pre Storefront API version 2022-04.
if (!id.match(/^gid:\/\//)) id = atob(id)

// Return the ID
const matches = id.match(/\/([^\/?]+)(?:\?.+)?$/)
if (matches) return matches[1]
}

// Format a string or number like money, using vue-i18n if it's installed
// or falling back to USD
export function formatMoney(val) {
if (this && this.$n) return this.$n(val, 'currency')
return parseFloat(val)
.toLocaleString(process.env.LOCALE_CODE || 'en-US', {
style: 'currency',
currency: process.env.CURRENCY_CODE || 'USD'
})
}
2 changes: 2 additions & 0 deletions helpers/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Bundle simple helpers
export * from './formatting'
Loading

0 comments on commit 2d2b848

Please sign in to comment.