├── .firebaserc ├── .prettierignore ├── .prettierrc ├── src ├── typings.d.ts ├── components │ ├── atoms │ │ ├── no-ssr.tsx │ │ ├── thumbnail.tsx │ │ ├── option-picker.tsx │ │ ├── product-grid.tsx │ │ ├── link.tsx │ │ ├── product-card.tsx │ │ ├── under-the-fold.tsx │ │ └── seo.tsx │ ├── providers │ │ ├── all-products-provider.tsx │ │ ├── all-products-provider.builder.tsx │ │ └── state-provider.tsx │ ├── organisms │ │ ├── all-products │ │ │ ├── all-products.builder.ts │ │ │ └── all-products.tsx │ │ ├── latest-products │ │ │ ├── latest-products.builder.ts │ │ │ ├── latest-products.tsx │ │ │ └── latest-products-no-ssr.builder.tsx │ │ ├── dev-404.tsx │ │ └── product-page-details │ │ │ ├── product-page-details.builder.ts │ │ │ └── product-page-details.tsx │ └── molecules │ │ ├── footer │ │ ├── footer.builder.ts │ │ └── footer.tsx │ │ ├── header │ │ ├── header.builder.ts │ │ └── header.tsx │ │ ├── aware-builder-component.tsx │ │ └── layout.tsx ├── hooks │ ├── use-site-metadata.ts │ ├── use-builder-footer.ts │ ├── use-builder-header.ts │ ├── use-all-site-pages.ts │ ├── use-all-products.ts │ └── use-recent-static-products.ts ├── pages │ ├── 404.tsx │ └── cart.tsx ├── templates │ ├── page.tsx │ └── product-page.tsx ├── html.tsx ├── gatsby-plugin-theme-ui │ └── index.ts └── utils │ └── product.ts ├── .env.all ├── .env.example ├── .vscode └── settings.json ├── tsconfig.json ├── gatsby-node.js ├── LICENSE ├── firebase.json ├── .gitignore ├── gatsby-config.js ├── package.json └── README.md /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "builder-shopify-starter" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | dist 4 | .next 5 | .cache-loader 6 | .cache 7 | docs 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "trailingComma": "es5", 4 | "singleQuote": true, 5 | "arrowParens": "avoid" 6 | } 7 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg'; 2 | declare module '*.gif'; 3 | declare module '*.png'; 4 | declare module '*.jpg'; 5 | declare module '@loadable/component'; 6 | declare module '@theme-ui/preset-base' { 7 | import { Theme } from 'theme-ui'; 8 | 9 | export default base as Theme; 10 | } 11 | -------------------------------------------------------------------------------- /.env.all: -------------------------------------------------------------------------------- 1 | GATSBY_SHOP_NAME=builder-io-demo 2 | GATSBY_SHOPIFY_ACCESS_TOKEN=dd0057d1e48d2d61ca8ec27b07d3c5e6 3 | GATSBY_STALL_RETRY_LIMIT=6 4 | GATSBY_STALL_TIMEOUT=60000 5 | GATSBY_CONNECTION_RETRY_LIMIT=10 6 | GATSBY_CONNECTION_TIMEOUT=60000 7 | BUILDER_API_KEY=6d39f4449e2b4e6792a793bb8c1d9615 8 | GATSBY_BUILDER_API_KEY=6d39f4449e2b4e6792a793bb8c1d9615 9 | ENABLE_GATSBY_REFRESH_ENDPOINT=1 10 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | GATSBY_SHOP_NAME=builder-io-demo 2 | GATSBY_SHOPIFY_ACCESS_TOKEN=dd0057d1e48d2d61ca8ec27b07d3c5e6 3 | GATSBY_STALL_RETRY_LIMIT=6 4 | GATSBY_STALL_TIMEOUT=60000 5 | GATSBY_CONNECTION_RETRY_LIMIT=10 6 | GATSBY_CONNECTION_TIMEOUT=60000 7 | BUILDER_API_KEY=bc2b10075b4046a7810143f7f3238f51 8 | GATSBY_BUILDER_API_KEY=bc2b10075b4046a7810143f7f3238f51 9 | ENABLE_GATSBY_REFRESH_ENDPOINT=1 10 | -------------------------------------------------------------------------------- /src/components/atoms/no-ssr.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | 3 | const NoSSR: React.FC<{ skeleton?: React.ReactNode }> = ({ children, skeleton }) => { 4 | const [render, setRender] = useState(false); 5 | useEffect(() => setRender(true), []); 6 | if (render) { 7 | return <>{children}; 8 | } 9 | if (skeleton) { 10 | return <>{skeleton}; 11 | } 12 | return null; 13 | }; 14 | export default NoSSR; 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./node_modules/typescript/lib", 3 | 4 | "files.exclude": { 5 | "**/.git": true, 6 | "**/.DS_Store": true, 7 | "**/.cache-loader": true, 8 | "**/dist": true, 9 | "**/.next": true, 10 | "**/cache": true 11 | }, 12 | "javascript.preferences.importModuleSpecifier": "relative", 13 | "typescript.preferences.importModuleSpecifier": "relative", 14 | "editor.formatOnSave": true 15 | } 16 | -------------------------------------------------------------------------------- /src/components/providers/all-products-provider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import useAllStaticProducts from '../../hooks/use-all-products'; 3 | 4 | export const AllProductsWrapper: React.FC<{ 5 | children: (products: GatsbyTypes.ShopifyProduct[]) => React.ReactElement; 6 | }> = ({ children }) => { 7 | const products = useAllStaticProducts(); 8 | 9 | return children(products.slice()) || null; 10 | }; 11 | 12 | export default AllProductsWrapper; 13 | -------------------------------------------------------------------------------- /src/components/organisms/all-products/all-products.builder.ts: -------------------------------------------------------------------------------- 1 | import { Builder, builder } from '@builder.io/react'; 2 | import loadable from '@loadable/component'; 3 | builder.init(process.env.GATSBY_BUILDER_API_KEY!); 4 | 5 | const LazyAllProducts = loadable(() => import('./all-products')); 6 | 7 | Builder.registerComponent(LazyAllProducts, { 8 | name: 'AllProductsGridSSR', 9 | description: 'Contains all products in store, will be included on the page in SSR', 10 | }); 11 | -------------------------------------------------------------------------------- /src/components/organisms/latest-products/latest-products.builder.ts: -------------------------------------------------------------------------------- 1 | import { Builder, builder } from '@builder.io/react'; 2 | import loadable from '@loadable/component'; 3 | builder.init(process.env.GATSBY_BUILDER_API_KEY!); 4 | 5 | const LazyAllProducts = loadable(() => import('./latest-products')); 6 | 7 | Builder.registerComponent(LazyAllProducts, { 8 | name: 'LatestProductsGridSSR', 9 | description: 'Contains latest products in store, will be included on the page in SSR', 10 | }); 11 | -------------------------------------------------------------------------------- /src/hooks/use-site-metadata.ts: -------------------------------------------------------------------------------- 1 | import { useStaticQuery, graphql } from 'gatsby'; 2 | 3 | const siteMetadataStaticQuery = graphql` 4 | query metadata { 5 | site { 6 | siteMetadata { 7 | title 8 | description 9 | author 10 | } 11 | } 12 | } 13 | `; 14 | 15 | const useSiteMetadata = () => { 16 | const data = useStaticQuery(siteMetadataStaticQuery); 17 | 18 | return data.site?.siteMetadata; 19 | }; 20 | 21 | export default useSiteMetadata; 22 | -------------------------------------------------------------------------------- /src/hooks/use-builder-footer.ts: -------------------------------------------------------------------------------- 1 | import { useStaticQuery, graphql } from 'gatsby'; 2 | 3 | const builderFooterQuery = graphql` 4 | query Footer { 5 | allBuilderModels { 6 | oneFooter(options: { noTraverse: false }) { 7 | content 8 | } 9 | } 10 | } 11 | `; 12 | 13 | const useBuilderFooter = () => { 14 | const data = useStaticQuery(builderFooterQuery); 15 | 16 | return data.allBuilderModels.oneFooter?.content; 17 | }; 18 | 19 | export default useBuilderFooter; 20 | -------------------------------------------------------------------------------- /src/hooks/use-builder-header.ts: -------------------------------------------------------------------------------- 1 | import { useStaticQuery, graphql } from 'gatsby'; 2 | 3 | const builderHeaderQuery = graphql` 4 | query Header { 5 | allBuilderModels { 6 | oneHeader(options: { noTraverse: false }) { 7 | content 8 | } 9 | } 10 | } 11 | `; 12 | 13 | const useBuilderHeader = () => { 14 | const data = useStaticQuery(builderHeaderQuery); 15 | 16 | return data.allBuilderModels.oneHeader?.content; 17 | }; 18 | 19 | export default useBuilderHeader; 20 | -------------------------------------------------------------------------------- /src/hooks/use-all-site-pages.ts: -------------------------------------------------------------------------------- 1 | import { useStaticQuery, graphql } from 'gatsby'; 2 | 3 | const allSitePagesQuery = graphql` 4 | query PagesQuery { 5 | allSitePage(filter: { path: { ne: "/dev-404-page/" } }) { 6 | nodes { 7 | path 8 | component 9 | } 10 | } 11 | } 12 | `; 13 | 14 | const useAllSitePages = () => { 15 | const data = useStaticQuery(allSitePagesQuery); 16 | 17 | return data.allSitePage.nodes; 18 | }; 19 | 20 | export default useAllSitePages; 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "baseUrl": "src", 15 | "jsx": "preserve" 16 | }, 17 | 18 | "exclude": ["node_modules"], 19 | "include": [ 20 | "src/**/*" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /src/components/molecules/footer/footer.builder.ts: -------------------------------------------------------------------------------- 1 | import { Builder, builder } from '@builder.io/react'; 2 | import loadable from '@loadable/component'; 3 | 4 | builder.init(process.env.GATSBY_BUILDER_API_KEY!); 5 | 6 | const LazyFooter = loadable(() => import('./footer')); 7 | 8 | Builder.registerComponent(LazyFooter, { 9 | name: 'Footer', 10 | description: 'Used at the bottom of the page', 11 | inputs: [ 12 | { 13 | name: 'footerTitle', 14 | type: 'text', 15 | defaultValue: 'Builder.io footer', 16 | }, 17 | ], 18 | }); 19 | -------------------------------------------------------------------------------- /src/components/organisms/latest-products/latest-products.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { Box, Heading, jsx } from 'theme-ui'; 3 | import useRecentStaticProducts from '../../../hooks/use-recent-static-products'; 4 | import ProductGrid from '../../atoms/product-grid'; 5 | 6 | const LatestProducts = () => { 7 | const products = useRecentStaticProducts(); 8 | 9 | return ( 10 | 11 | Latest Products 12 | 13 | 14 | ); 15 | }; 16 | 17 | export default LatestProducts; 18 | -------------------------------------------------------------------------------- /src/components/molecules/header/header.builder.ts: -------------------------------------------------------------------------------- 1 | import { Builder, builder } from '@builder.io/react'; 2 | import loadable from '@loadable/component'; 3 | 4 | builder.init(process.env.GATSBY_BUILDER_API_KEY!); 5 | 6 | const LazyHeader = loadable(() => import('./header')); 7 | 8 | Builder.registerComponent(LazyHeader, { 9 | name: 'Header', 10 | description: 'used on top of the page, included in SSR and affects SEO', 11 | inputs: [ 12 | { 13 | name: 'siteTitle', 14 | type: 'text', 15 | defaultValue: 'Builder.io Swag Store', 16 | }, 17 | ], 18 | }); 19 | -------------------------------------------------------------------------------- /src/components/organisms/all-products/all-products.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { Box, Heading, jsx } from 'theme-ui'; 3 | import useAllStaticProducts from '../../../hooks/use-all-products'; 4 | import ProductGrid from '../../atoms/product-grid'; 5 | 6 | const AllProducts = () => { 7 | const products = useAllStaticProducts(); 8 | 9 | return ( 10 | 11 | 12 | All Products 13 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default AllProducts; 20 | -------------------------------------------------------------------------------- /src/components/molecules/footer/footer.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { Box, Styled, jsx } from 'theme-ui'; 3 | 4 | const Footer: React.FC = () => ( 5 | 6 |
7 | 8 | © {new Date().getFullYear()} Built with 9 | {` `} 10 | Builder.io 11 | {` and `} 12 | Gatsby 13 | {` and `} 14 | Shopify. 15 | 16 |
17 |
18 | ); 19 | 20 | export default Footer; 21 | -------------------------------------------------------------------------------- /gatsby-node.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | exports.createPages = ({ graphql, actions }) => { 4 | const { createPage } = actions; 5 | return graphql(` 6 | { 7 | allShopifyProduct(limit: 70) { 8 | edges { 9 | node { 10 | handle 11 | } 12 | } 13 | } 14 | } 15 | `).then(result => { 16 | result.data.allShopifyProduct.edges.forEach(({ node }) => { 17 | createPage({ 18 | path: `/product/${node.handle}/`, 19 | component: path.resolve(`./src/templates/product-page.tsx`), 20 | context: { 21 | handle: node.handle, 22 | }, 23 | }); 24 | }); 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/organisms/dev-404.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link as GatsbyLink } from 'gatsby'; 3 | import useAllSitePages from '../../hooks/use-all-site-pages'; 4 | 5 | const Dev404 = () => { 6 | const allPages = useAllSitePages(); 7 | 8 | return ( 9 | 10 |

Custom 404 page

11 |

This is a development only page

12 |
    13 | {allPages.map(item => ( 14 |
  • 15 | 16 | {' '} 17 | {item.path}{' '} 18 | 19 |
  • 20 | ))} 21 |
22 |
23 | ); 24 | }; 25 | 26 | export default Dev404; 27 | -------------------------------------------------------------------------------- /src/components/organisms/latest-products/latest-products-no-ssr.builder.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Builder, builder } from '@builder.io/react'; 3 | import loadable from '@loadable/component'; 4 | import UnderTheFold from '../../atoms/under-the-fold'; 5 | builder.init(process.env.GATSBY_BUILDER_API_KEY!); 6 | 7 | const LazyLatesProducts = loadable(() => import('./latest-products'), { ssr: false }); 8 | 9 | const UnderTheFoldProducts: React.FC = () => ( 10 | {() => } 11 | ); 12 | 13 | Builder.registerComponent(UnderTheFoldProducts, { 14 | name: 'LatestProductsGridNoSSR', 15 | description: 16 | 'Contains latest products in store, will not included on the page in SSR (for better performance)', 17 | }); 18 | -------------------------------------------------------------------------------- /src/components/providers/all-products-provider.builder.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Builder } from '@builder.io/react'; 3 | import loadable from '@loadable/component'; 4 | import StateProvider, { StateProviderProps } from './state-provider'; 5 | const AllProducts: typeof import('./all-products-provider').default = loadable( 6 | () => import('./all-products-provider') 7 | ); 8 | 9 | const AllProductsLazyProvider = ({ state, ...rest }: StateProviderProps) => { 10 | return ( 11 | 12 | {allProducts => } 13 | 14 | ); 15 | }; 16 | 17 | Builder.registerComponent(AllProductsLazyProvider, { 18 | name: 'All Product State Provider', 19 | canHaveChildren: true, 20 | noWrap: true, 21 | }); 22 | -------------------------------------------------------------------------------- /src/components/atoms/thumbnail.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { jsx } from 'theme-ui'; 3 | import Img from 'gatsby-image'; 4 | 5 | export interface ThumbnailProps { 6 | src: any; // for now; 7 | onClick?: React.MouseEventHandler; 8 | name: string; 9 | } 10 | 11 | const Thumbnail: React.FC = ({ src, onClick, name }) => { 12 | return ( 13 | 28 | ); 29 | }; 30 | 31 | export default Thumbnail; 32 | -------------------------------------------------------------------------------- /src/components/atoms/option-picker.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { jsx } from 'theme-ui'; 3 | import { Select, Label } from '@theme-ui/components'; 4 | export interface OptionPickerProps { 5 | name: string; 6 | options?: Readonly>; 7 | onChange: React.ChangeEventHandler; 8 | selected: string; 9 | } 10 | 11 | const OptionPicker: React.FC = ({ name, options, onChange, selected }) => { 12 | return ( 13 |
14 | 15 | 22 |
23 | ); 24 | }; 25 | 26 | export default OptionPicker; 27 | -------------------------------------------------------------------------------- /src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Container } from 'theme-ui'; 3 | import SEO from '../components/atoms/seo'; 4 | import { Builder } from '@builder.io/react'; 5 | import loadable from '@loadable/component'; 6 | import NoSSR from '../components/atoms/no-ssr'; 7 | import PageTemplate from '../templates/page'; 8 | 9 | const AsyncDev404 = loadable(() => import('../components/organisms/dev-404')); 10 | 11 | const NotFound: React.FC = () => ( 12 |
13 | 14 | 15 |

Page not found :(

16 |
17 |
18 | ); 19 | 20 | const FourOhFour: React.FC = () => { 21 | if (Builder.isEditing || Builder.isPreviewing) { 22 | return ; 23 | } 24 | return {process.env.NODE_ENV === 'development' ? : }; 25 | }; 26 | 27 | export default FourOhFour; 28 | -------------------------------------------------------------------------------- /src/hooks/use-all-products.ts: -------------------------------------------------------------------------------- 1 | import { useStaticQuery, graphql } from 'gatsby'; 2 | const productStaticQuery = graphql` 3 | query productQuery { 4 | allShopifyProduct(limit: 50) { 5 | nodes { 6 | title 7 | handle 8 | images { 9 | originalSrc 10 | localFile { 11 | childImageSharp { 12 | fluid(maxWidth: 290) { 13 | ...GatsbyImageSharpFluid_withWebp_noBase64 14 | } 15 | } 16 | } 17 | } 18 | priceRange { 19 | maxVariantPrice { 20 | amount 21 | currencyCode 22 | } 23 | } 24 | } 25 | } 26 | } 27 | `; 28 | 29 | const useAllStaticProducts = () => { 30 | const data = useStaticQuery(productStaticQuery); 31 | 32 | return data.allShopifyProduct.nodes; 33 | }; 34 | 35 | export default useAllStaticProducts; 36 | -------------------------------------------------------------------------------- /src/hooks/use-recent-static-products.ts: -------------------------------------------------------------------------------- 1 | import { useStaticQuery, graphql } from 'gatsby'; 2 | const latestStaticQuery = graphql` 3 | query latest { 4 | allShopifyProduct(sort: { fields: [createdAt], order: DESC }, limit: 9) { 5 | nodes { 6 | title 7 | handle 8 | images { 9 | localFile { 10 | childImageSharp { 11 | fluid(maxWidth: 290) { 12 | ...GatsbyImageSharpFluid_withWebp_noBase64 13 | } 14 | } 15 | } 16 | } 17 | priceRange { 18 | maxVariantPrice { 19 | amount 20 | currencyCode 21 | } 22 | } 23 | } 24 | } 25 | } 26 | `; 27 | 28 | const useRecentStaticProducts = () => { 29 | const data = useStaticQuery(latestStaticQuery); 30 | 31 | return data.allShopifyProduct.nodes; 32 | }; 33 | 34 | export default useRecentStaticProducts; 35 | -------------------------------------------------------------------------------- /src/components/organisms/product-page-details/product-page-details.builder.ts: -------------------------------------------------------------------------------- 1 | import { Builder, builder } from '@builder.io/react'; 2 | import loadable from '@loadable/component'; 3 | builder.init(process.env.GATSBY_BUILDER_API_KEY!); 4 | 5 | const LazyProductPageDetails = loadable(() => import('./product-page-details')); 6 | 7 | Builder.registerComponent(LazyProductPageDetails, { 8 | name: 'ProductPageDetails', 9 | description: 'Dynamic product details, included in SSR, should only be used in product pages', 10 | defaults: { 11 | bindings: { 12 | 'component.options.product': 'state.product', 13 | 'component.options.title': 'state.product.title', 14 | 'component.options.description': 'state.product.descriptionHtml', 15 | }, 16 | }, 17 | inputs: [ 18 | { 19 | name: 'description', 20 | type: 'richText', 21 | }, 22 | { 23 | name: 'title', 24 | type: 'text', 25 | }, 26 | ], 27 | }); 28 | -------------------------------------------------------------------------------- /src/components/atoms/product-grid.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { jsx } from 'theme-ui'; 3 | import { Grid } from '@theme-ui/components'; 4 | import ProductCard from './product-card'; 5 | 6 | export interface ProductGridProps { 7 | products: Readonly; 8 | imageLoading?: 'lazy' | 'eager' | 'auto'; 9 | } 10 | const ProductGrid: React.FC = ({ products, imageLoading }) => ( 11 | 19 | {products.map(product => ( 20 | 29 | ))} 30 | 31 | ); 32 | 33 | export default ProductGrid; 34 | -------------------------------------------------------------------------------- /src/components/molecules/aware-builder-component.tsx: -------------------------------------------------------------------------------- 1 | import { Builder, builder, BuilderComponent } from '@builder.io/react'; 2 | import { BuilderPageProps } from '@builder.io/react/src/components/builder-page.component'; 3 | import '@builder.io/widgets'; 4 | import React from 'react'; 5 | import Link from '../atoms/link'; 6 | import { useCartCount, useAddItemToCart } from 'gatsby-theme-shopify-manager/src'; 7 | 8 | const apiKey = process.env.GATSBY_BUILDER_API_KEY; 9 | builder.init(apiKey!); 10 | Builder.isStatic = true; 11 | 12 | const AwareBuilderComponent: React.FC> = props => { 13 | const cartCount = useCartCount(); 14 | const addItem = useAddItemToCart(); 15 | return ( 16 | { 18 | const internal = props.target !== '_blank' && /^\/(?!\/)/.test(props.href!); 19 | if (internal) { 20 | return ; 21 | } 22 | return ; 23 | }} 24 | context={{ cartCount, addItem }} 25 | {...props} 26 | /> 27 | ); 28 | }; 29 | 30 | export default AwareBuilderComponent; 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Builder.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "public", 4 | "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], 5 | "headers": [ 6 | { 7 | "source": "**/*.@(eot|otf|ttf|ttc|woff|font.css)", 8 | "headers": [ 9 | { 10 | "key": "Access-Control-Allow-Origin", 11 | "value": "*" 12 | } 13 | ] 14 | }, 15 | { 16 | "source": "**/*.@(jpg|jpeg|gif|png|webp|js)", 17 | "headers": [ 18 | { 19 | "key": "Cache-Control", 20 | "value": "max-age=31536000" 21 | } 22 | ] 23 | }, 24 | { 25 | "source": "404.html", 26 | "headers": [ 27 | { 28 | "key": "Cache-Control", 29 | "value": "max-age=300" 30 | } 31 | ] 32 | }, 33 | { 34 | "source": "**/*", 35 | "headers": [ 36 | { 37 | "key": "Cache-Control", 38 | "value": "public, max-age=600, s-maxage=2628000, stale-while-revalidate=2628000, stale-if-error=2628000" 39 | } 40 | ] 41 | } 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/components/providers/state-provider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BuilderBlockComponent, BuilderStoreContext, BuilderElement } from '@builder.io/react'; 3 | 4 | export interface StateProviderProps { 5 | state: any; 6 | builderBlock: BuilderElement; 7 | [key: string]: any; 8 | } 9 | 10 | const StateProvider: React.FC = props => ( 11 | 12 | {state => ( 13 | 26 | {props.builderBlock && 27 | props.builderBlock.children && 28 | props.builderBlock.children.map((block, index) => ( 29 | 30 | ))} 31 | {props.children} 32 | 33 | )} 34 | 35 | ); 36 | 37 | export default StateProvider; 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional eslint cache 37 | .eslintcache 38 | 39 | # Optional REPL history 40 | .node_repl_history 41 | 42 | # Output of 'npm pack' 43 | *.tgz 44 | 45 | # dotenv environment variable files 46 | 47 | # gatsby files 48 | .cache 49 | public 50 | 51 | # Mac files 52 | .DS_Store 53 | 54 | # Yarn 55 | yarn-error.log 56 | .pnp 57 | .pnp.js 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | .now 62 | 63 | # The TS auto-complete stuff 64 | apollo.config.js 65 | 66 | # GraphQl typegen 67 | __generated__ 68 | 69 | # firebase cache 70 | .firebase/ -------------------------------------------------------------------------------- /src/components/atoms/link.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { jsx } from 'theme-ui'; 3 | import { Link as GatsbyLink } from 'gatsby'; 4 | 5 | export interface LinkProps { 6 | isButton?: boolean; 7 | url: string; 8 | } 9 | 10 | const Link: React.FC = ({ isButton, url, children, ...props }) => { 11 | return isButton ? ( 12 | 31 | {children} 32 | 33 | ) : ( 34 | 45 | {children} 46 | 47 | ); 48 | }; 49 | 50 | export default Link; 51 | -------------------------------------------------------------------------------- /src/components/molecules/layout.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import React from 'react'; 3 | import { Button, ThemeProvider, jsx } from 'theme-ui'; 4 | import { Helmet } from 'react-helmet'; 5 | import theme from '../../gatsby-plugin-theme-ui'; 6 | import './footer/footer.builder'; 7 | import useBuilderHeader from '../../hooks/use-builder-header'; 8 | import AwareBuilderComponent from './aware-builder-component'; 9 | import './header/header.builder'; 10 | import 'normalize.css'; 11 | import useBuilderFooter from '../../hooks/use-builder-footer'; 12 | const Layout: React.FunctionComponent = ({ children }) => { 13 | const header = useBuilderHeader(); 14 | const footer = useBuilderFooter(); 15 | 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 |
30 |
{children}
31 | 32 |
33 |
34 | ); 35 | }; 36 | 37 | export default Layout; 38 | -------------------------------------------------------------------------------- /src/components/atoms/product-card.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import React from 'react'; 3 | import { Styled, jsx } from 'theme-ui'; 4 | import Img from 'gatsby-image'; 5 | import { Card, Text } from '@theme-ui/components'; 6 | import Link from './link'; 7 | import { getPrice } from '../../utils/product'; 8 | 9 | export interface ProductCardProps { 10 | title?: string; 11 | slug?: string; 12 | price: number | string; 13 | image: any; 14 | currency?: string; 15 | imageLoading?: `auto` | `lazy` | `eager`; 16 | } 17 | 18 | const ProductCard: React.FC = ({ 19 | title, 20 | slug, 21 | price, 22 | image, 23 | currency, 24 | imageLoading = 'eager', 25 | }) => { 26 | return ( 27 | 35 |
36 | {image && } 37 |
38 | {title} 39 | {getPrice(String(price), currency || 'USD')} 40 | 41 | View 42 | 43 |
44 | ); 45 | }; 46 | 47 | export default ProductCard; 48 | -------------------------------------------------------------------------------- /src/components/molecules/header/header.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { jsx, Styled } from 'theme-ui'; 3 | import Link from '../../atoms/link'; 4 | import { useCartCount } from 'gatsby-theme-shopify-manager/src'; 5 | 6 | const Header: React.FC<{ 7 | siteTitle: string; 8 | }> = ({ siteTitle }) => { 9 | const count = useCartCount(); 10 | return ( 11 | 12 |
23 | 24 | 36 | {siteTitle} 37 | 38 | 39 | 40 | Cart{count ? '*' : ''} 41 | 42 |
43 |
44 | ); 45 | }; 46 | 47 | export default Header; 48 | -------------------------------------------------------------------------------- /src/components/atoms/under-the-fold.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { useRef } from 'react'; 3 | import { Spinner, jsx } from 'theme-ui'; 4 | import VisibilitySensor from 'react-visibility-sensor'; 5 | interface Shape { 6 | top?: number; 7 | left?: number; 8 | bottom?: number; 9 | right?: number; 10 | } 11 | 12 | export interface UnderTheFoldProps { 13 | onChange?: (isVisible: boolean) => void; 14 | active?: boolean; 15 | partialVisibility?: boolean; 16 | offset?: Shape; 17 | minTopValue?: number; 18 | intervalCheck?: boolean; 19 | intervalDelay?: number; 20 | scrollCheck?: boolean; 21 | scrollDelay?: number; 22 | scrollThrottle?: number; 23 | resizeCheck?: boolean; 24 | resizeDelay?: number; 25 | resizeThrottle?: number; 26 | containment?: any; 27 | delayedCall?: boolean; 28 | children: (args: { isVisible: boolean; visibilityRect?: Shape }) => React.ReactNode; 29 | } 30 | 31 | const UnderTheFold: React.FC = props => { 32 | const seenRef = useRef(); 33 | return ( 34 | 35 | {({ isVisible, visibilityRect }) => { 36 | if (isVisible || seenRef.current) { 37 | seenRef.current = true; 38 | return props.children({ isVisible, visibilityRect }); 39 | } 40 | return ( 41 |
49 | 50 |
51 | ); 52 | }} 53 |
54 | ); 55 | }; 56 | export default UnderTheFold; 57 | -------------------------------------------------------------------------------- /src/templates/page.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { graphql } from 'gatsby'; 3 | import SEO from '../components/atoms/seo'; 4 | import AwareBuilderComponent from '../components/molecules/aware-builder-component'; 5 | import '../components/organisms/all-products/all-products.builder'; 6 | import '../components/organisms/latest-products/latest-products.builder'; 7 | import '../components/organisms/latest-products/latest-products-no-ssr.builder'; 8 | import '../components/providers/all-products-provider.builder'; 9 | 10 | export interface PageProps { 11 | data?: GatsbyTypes.Query; 12 | } 13 | 14 | const defaultDescription = 'Edit this in your entry for a better SEO'; 15 | const defaultTitle = 'Builder: Drag and Drop Page Building for Any Site'; 16 | 17 | const PageTemplate: React.FC = ({ data, ...rest }) => { 18 | const builderPage = data?.allBuilderModels?.onePage?.content; 19 | const seo = { 20 | title: (builderPage && builderPage.data.title) || defaultTitle, 21 | description: (builderPage && builderPage.data.description) || defaultDescription, 22 | keywords: (builderPage && builderPage.data.keywords) || [], 23 | image: builderPage && builderPage.data.image, 24 | }; 25 | 26 | return ( 27 | <> 28 | 29 | 30 | 31 | ); 32 | }; 33 | 34 | export default PageTemplate; 35 | 36 | export const query = graphql` 37 | query onePage($path: String!) { 38 | allBuilderModels { 39 | onePage(target: { urlPath: $path }, options: { cachebust: true, noTraverse: false }) { 40 | content 41 | } 42 | } 43 | } 44 | `; 45 | -------------------------------------------------------------------------------- /src/components/atoms/seo.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * SEO component that queries for data with 3 | * See: https://www.gatsbyjs.org/docs/use-static-query/ 4 | */ 5 | 6 | import React from 'react'; 7 | import { Helmet, HelmetProps, MetaProps } from 'react-helmet'; 8 | import useSiteMetadata from '../../hooks/use-site-metadata'; 9 | 10 | export interface SEOProps extends HelmetProps { 11 | description?: string; 12 | lang?: string; 13 | image?: string; 14 | } 15 | 16 | const SEO: React.FC = ({ lang, description, image, meta = [], ...rest }) => { 17 | const siteMetadata = useSiteMetadata(); 18 | const metaDescription = description || siteMetadata?.description; 19 | const helmetProps: HelmetProps = { 20 | titleTemplate: `%s | ${siteMetadata?.title}`, 21 | htmlAttributes: { 22 | lang: lang || 'en', 23 | }, 24 | ...rest, 25 | }; 26 | const helmetMeta: MetaProps[] = [ 27 | { 28 | name: `description`, 29 | content: metaDescription, 30 | }, 31 | { 32 | property: `og:title`, 33 | content: rest.title, 34 | }, 35 | { 36 | property: `og:description`, 37 | content: metaDescription, 38 | }, 39 | { 40 | property: `og:type`, 41 | content: `website`, 42 | }, 43 | ...(image 44 | ? [ 45 | { 46 | itemProp: 'image', 47 | content: image, 48 | }, 49 | { 50 | property: 'og:image', 51 | content: image, 52 | }, 53 | { 54 | name: 'twitter:image', 55 | content: image, 56 | }, 57 | ] 58 | : []), 59 | ...meta, 60 | ]; 61 | return ; 62 | }; 63 | 64 | export default SEO; 65 | -------------------------------------------------------------------------------- /src/html.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import cheerio from 'cheerio'; 3 | 4 | /** 5 | * 6 | * Gatsby does a two-pass render for HTML. It loops through your pages first 7 | * rendering only the body and then takes the result body HTML string and 8 | * passes it as the `body` prop to this component to complete the render. 9 | */ 10 | 11 | const postProcess = (body: string) => { 12 | let globalStyles = ''; 13 | 14 | if (body.includes(' { 19 | const str = $(element).html(); 20 | const styles = cheerio.load(String(str))('style'); 21 | globalStyles += styles 22 | .toArray() 23 | .map(el => $(el).html()) 24 | .join(' '); 25 | }); 26 | } 27 | return { body, globalStyles }; 28 | }; 29 | 30 | interface HTMLProps { 31 | htmlAttributes: {}; 32 | headComponents: ReactNode[]; 33 | bodyAttributes: {}; 34 | preBodyComponents: ReactNode[]; 35 | body: string; 36 | postBodyComponents: ReactNode[]; 37 | } 38 | const HTML: React.FC = props => { 39 | const { body, globalStyles } = postProcess(props.body); 40 | return ( 41 | 42 | 43 | 44 | 45 | 46 | {props.headComponents} 47 | 48 | 49 | {props.preBodyComponents} 50 | 51 |
52 | {props.postBodyComponents} 53 | 54 | 55 | ); 56 | }; 57 | 58 | export default HTML; 59 | -------------------------------------------------------------------------------- /src/templates/product-page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { graphql } from 'gatsby'; 3 | import AwareBuilderComponent from '../components/molecules/aware-builder-component'; 4 | import '../components/organisms/product-page-details/product-page-details.builder'; 5 | import '../components/organisms/latest-products/latest-products-no-ssr.builder'; 6 | import '../components/organisms/latest-products/latest-products.builder'; 7 | 8 | export interface ProductPageProps { 9 | data: GatsbyTypes.Query; 10 | } 11 | 12 | const ProductPage: React.FC = ({ 13 | data: { shopifyProduct, allBuilderModels }, 14 | }) => { 15 | const product = shopifyProduct!; 16 | const content = allBuilderModels?.oneProductPageTemplate?.content; 17 | return ; 18 | }; 19 | 20 | export default ProductPage; 21 | 22 | export const ProductPageQuery = graphql` 23 | query productPage($handle: String!) { 24 | allBuilderModels { 25 | oneProductPage( 26 | target: { productHandle: $handle } 27 | options: { cachebust: true, noTraverse: false } 28 | ) { 29 | content 30 | } 31 | } 32 | shopifyProduct(handle: { eq: $handle }) { 33 | id 34 | title 35 | description 36 | priceRange { 37 | maxVariantPrice { 38 | amount 39 | currencyCode 40 | } 41 | } 42 | descriptionHtml 43 | options { 44 | name 45 | values 46 | } 47 | variants { 48 | shopifyId 49 | selectedOptions { 50 | name 51 | value 52 | } 53 | image { 54 | localFile { 55 | childImageSharp { 56 | fluid(maxWidth: 446) { 57 | ...GatsbyImageSharpFluid_withWebp_noBase64 58 | } 59 | } 60 | } 61 | } 62 | } 63 | } 64 | } 65 | `; 66 | -------------------------------------------------------------------------------- /src/gatsby-plugin-theme-ui/index.ts: -------------------------------------------------------------------------------- 1 | import base from '@theme-ui/preset-base'; 2 | 3 | export default { 4 | initialColorModeName: 'light', 5 | ...base, 6 | colors: { 7 | text: '#557571', 8 | background: '#eeeeee', 9 | primary: '#596e79', 10 | secondary: '#3b6978', 11 | }, 12 | styles: { 13 | ...base.styles, 14 | a: { 15 | color: 'text', 16 | textDecoration: 'none', 17 | '&:hover': { 18 | color: 'secondary', 19 | textDecoration: 'underline', 20 | }, 21 | }, 22 | hr: { 23 | display: 'block', 24 | height: '1px', 25 | border: 0, 26 | borderTop: '1px solid', 27 | borderColor: 'secondary', 28 | opacity: '0.3', 29 | }, 30 | }, 31 | fontWeights: { 32 | medium: 600, 33 | bold: 800, 34 | }, 35 | text: { 36 | bold: { 37 | fontWeight: 600, 38 | }, 39 | }, 40 | alerts: { 41 | primary: { 42 | border: '1px solid', 43 | borderColor: 'text', 44 | color: 'background', 45 | bg: 'text', 46 | fontWeight: 'normal', 47 | }, 48 | }, 49 | cards: { 50 | primary: { 51 | padding: 2, 52 | borderRadius: 4, 53 | }, 54 | compact: { 55 | padding: 1, 56 | borderRadius: 2, 57 | border: '1px solid', 58 | borderColor: 'text', 59 | }, 60 | }, 61 | buttons: { 62 | primary: { 63 | color: 'background', 64 | bg: 'primary', 65 | fontWeight: 600, 66 | '&:hover': { 67 | bg: 'secondary', 68 | cursor: 'pointer', 69 | }, 70 | }, 71 | secondary: { 72 | color: 'background', 73 | bg: 'primary', 74 | }, 75 | link: { 76 | color: 'text', 77 | textDecoration: 'none', 78 | padding: 0, 79 | background: 'transparent', 80 | '&:hover': { 81 | textDecoration: 'underline', 82 | color: 'secondary', 83 | cursor: 'pointer', 84 | }, 85 | }, 86 | }, 87 | }; 88 | -------------------------------------------------------------------------------- /gatsby-config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config({ 2 | path: `.env.all`, 3 | }); 4 | 5 | const path = require('path'); 6 | 7 | module.exports = { 8 | siteMetadata: { 9 | title: `Builder.io`, 10 | description: ``, 11 | author: `Aziz Abbas`, 12 | }, 13 | plugins: [ 14 | `gatsby-plugin-typescript`, 15 | `gatsby-plugin-react-helmet`, 16 | { 17 | resolve: `gatsby-plugin-layout`, 18 | options: { 19 | component: require.resolve(`./src/components/molecules/layout.tsx`), 20 | }, 21 | }, 22 | { 23 | resolve: `gatsby-theme-shopify-manager`, 24 | options: { 25 | shopName: process.env.GATSBY_SHOP_NAME, 26 | accessToken: process.env.GATSBY_SHOPIFY_ACCESS_TOKEN, 27 | }, 28 | }, 29 | `gatsby-plugin-theme-ui`, 30 | { 31 | resolve: 'gatsby-theme-style-guide', 32 | options: { 33 | // sets path for generated page 34 | basePath: '/design-system', 35 | }, 36 | }, 37 | 38 | 'gatsby-transformer-sharp', 39 | 'gatsby-plugin-sharp', 40 | { 41 | resolve: '@builder.io/gatsby', 42 | options: { 43 | publicAPIKey: process.env.BUILDER_API_KEY, 44 | /* to allow live preview editing on localhost*/ 45 | custom404Dev: path.resolve('./src/pages/404.tsx'), 46 | templates: { 47 | /* Render every `page` model as a new page using the /page.tsx template 48 | /* based on the URL provided in Builder.io 49 | */ 50 | page: path.resolve('./src/templates/page.tsx'), 51 | }, 52 | }, 53 | }, 54 | { 55 | resolve: `gatsby-plugin-typegen`, 56 | options: { 57 | emitSchema: { 58 | './src/__generated__/gatsby-schema.graphql': true, 59 | './src/__generated__/gatsby-introspection.json': true, 60 | }, 61 | emitPluginDocuments: { 62 | './src/__generated__/gatsby-plugin-documents.graphql': true, 63 | }, 64 | }, 65 | }, 66 | { 67 | resolve: `gatsby-plugin-loadable-components-ssr`, 68 | options: { 69 | // Whether replaceHydrateFunction should call ReactDOM.hydrate or ReactDOM.render 70 | // Defaults to ReactDOM.render on develop and ReactDOM.hydrate on build 71 | useHydrate: true, 72 | }, 73 | }, 74 | ], 75 | }; 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gatsby-builder-shopify", 3 | "scripts": { 4 | "prettier": "prettier", 5 | "prettier-staged": "git diff --staged --name-only | xargs prettier --write", 6 | "build": "gatsby build", 7 | "develop": "gatsby develop", 8 | "serve": "gatsby serve", 9 | "clean": "gatsby clean", 10 | "deploy": "firebase deploy" 11 | }, 12 | "dependencies": { 13 | "@builder.io/gatsby": "^1.0.10", 14 | "@builder.io/react": "^1.1.34-10", 15 | "@builder.io/widgets": "^1.2.17", 16 | "@emotion/cache": "^10.0.29", 17 | "@emotion/core": "^10.0.35", 18 | "@loadable/component": "^5.13.2", 19 | "@theme-ui/components": "^0.3.1", 20 | "@theme-ui/preset-base": "^0.3.0", 21 | "@types/cheerio": "^0.22.22", 22 | "@types/react-visibility-sensor": "^5.1.0", 23 | "cheerio": "^1.0.0-rc.3", 24 | "gatsby": "^2.20.12", 25 | "gatsby-image": "^2.3.1", 26 | "gatsby-link": "^2.4.15", 27 | "gatsby-plugin-google-fonts": "^1.0.1", 28 | "gatsby-plugin-layout": "^1.2.1", 29 | "gatsby-plugin-loadable-components-ssr": "^2.1.0", 30 | "gatsby-plugin-offline": "^3.2.13", 31 | "gatsby-plugin-react-helmet": "^3.3.6", 32 | "gatsby-plugin-sharp": "^2.6.36", 33 | "gatsby-plugin-theme-ui": "^0.3.0", 34 | "gatsby-plugin-typegen": "^2.0.0", 35 | "gatsby-plugin-typescript": "^2.3.1", 36 | "gatsby-source-graphql": "^2.1.32", 37 | "gatsby-source-shopify": "^3.2.32", 38 | "gatsby-theme-shopify-manager": "^0.1.8", 39 | "gatsby-theme-style-guide": "^0.3.1", 40 | "gatsby-transformer-sharp": "^2.5.15", 41 | "graphql": "14.6.0", 42 | "normalize.css": "^8.0.1", 43 | "preact": "^10.5.5", 44 | "react": "^16.13.1", 45 | "react-dom": "^16.13.1", 46 | "react-helmet": "^6.1.0", 47 | "react-visibility-sensor": "^5.1.1", 48 | "theme-ui": "^0.3.1" 49 | }, 50 | "resolutions": { 51 | "graphql": "14.6.0" 52 | }, 53 | "devDependencies": { 54 | "@types/node": "^13.11.0", 55 | "@types/react": "^16.9.32", 56 | "@types/react-dom": "^16.9.6", 57 | "@types/react-helmet": "^6.0.0", 58 | "@types/theme-ui": "^0.3.1", 59 | "gatsby-plugin-ts-config": "^1.1.0", 60 | "gatsby-plugin-tslint": "^0.0.2", 61 | "prettier": "^2.0.2", 62 | "typescript": "^3.8.3", 63 | "yarn": "^1.22.5" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/utils/product.ts: -------------------------------------------------------------------------------- 1 | /* 2 | prepareVariantsWithOptions() 3 | 4 | This function changes the structure of the variants to 5 | more easily get at their options. The original data 6 | structure looks like this: 7 | 8 | { 9 | "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8zMTc4NDQ4MTAzMDE4OA==", 10 | "selectedOptions": [ 11 | { 12 | "name": "Color", 13 | "value": "Red" 14 | }, 15 | { 16 | "name": "Size", 17 | "value": "Small" 18 | } 19 | ] 20 | }, 21 | 22 | This function accepts that and outputs a data structure that looks like this: 23 | 24 | { 25 | "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8zMTc4NDQ4MTAzMDE4OA==", 26 | "color": "Red", 27 | "size": "Small" 28 | }, 29 | */ 30 | 31 | export function prepareVariantsWithOptions( 32 | variants: Readonly 33 | ) { 34 | return variants.map(variant => { 35 | // convert the options to a dictionary instead of an array 36 | const optionsDictionary = variant.selectedOptions?.reduce>( 37 | (options, option) => { 38 | options[`${option?.name?.toLowerCase()}`] = option?.value; 39 | return options; 40 | }, 41 | {} 42 | ); 43 | 44 | // return an object with all of the variant properties + the options at the top level 45 | return { 46 | ...optionsDictionary, 47 | ...variant, 48 | }; 49 | }) as any[]; 50 | } 51 | 52 | export const getPrice = (price: string, currency: string) => 53 | Intl.NumberFormat(undefined, { 54 | currency, 55 | minimumFractionDigits: 2, 56 | style: 'currency', 57 | }).format(parseFloat(price ? price : '0')); 58 | 59 | /* 60 | prepareVariantsImages() 61 | 62 | This function distills the variants images into a non-redundant 63 | group that includes an option 'key' (most likely color). The 64 | datastructure coming into this function looks like this: 65 | 66 | { 67 | "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8zMTc4NDQ4MTAzMDE4OA==", 68 | "image": image1, 69 | "color": "Red", 70 | "size": "Small" 71 | }, 72 | { 73 | "shopifyId": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaW1l2C8zMTc4NDQ4MTAzMDE4OA==", 74 | "image": image1, 75 | "color": "Red", 76 | "size": "Medium" 77 | }, 78 | 79 | And condenses them so that there is only one unique 80 | image per key value: 81 | 82 | { 83 | "image": image1, 84 | "color": "Red", 85 | }, 86 | */ 87 | 88 | export function prepareVariantsImages( 89 | variants: any[], 90 | // variants: Readonly, 91 | optionKey: any 92 | ): any[] { 93 | // Go through the variants and reduce them into non-redundant 94 | // images by optionKey. Output looks like this: 95 | // { 96 | // [optionKey]: image 97 | // } 98 | const imageDictionary = variants.reduce>((images, variant) => { 99 | images[variant[optionKey]] = variant.image; 100 | return images; 101 | }, {}); 102 | 103 | // prepare an array of image objects that include both the image 104 | // and the optionkey value. 105 | const images = Object.keys(imageDictionary).map(key => { 106 | return { 107 | [optionKey]: key, 108 | src: imageDictionary[key], 109 | }; 110 | }); 111 | 112 | return images; 113 | } 114 | -------------------------------------------------------------------------------- /src/pages/cart.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { Styled, jsx } from 'theme-ui'; 3 | import React from 'react'; 4 | import { Grid, Divider, Button, Card, Text, Image } from '@theme-ui/components'; 5 | import Link from '../components/atoms/link'; 6 | import SEO from '../components/atoms/seo'; 7 | import { 8 | useCartItems, 9 | useCheckoutUrl, 10 | useCart, 11 | useUpdateItemQuantity, 12 | } from 'gatsby-theme-shopify-manager/src'; 13 | import NoSSR from '../components/atoms/no-ssr'; 14 | import builder from '@builder.io/react'; 15 | 16 | const CartPage = () => { 17 | const lineItems = useCartItems(); 18 | const updateItemQuantity = useUpdateItemQuantity(); 19 | // handing over session ID will allow you to do conversion tracking cross domains in Builder 20 | // otherwise not necessary 21 | const checkoutUrl = useCheckoutUrl() + `&builder.overrideSessionId=${builder.sessionId}`; 22 | const cart = useCart(); 23 | const { tax, total } = getCartTotals(cart); 24 | 25 | function getCartTotals(cart: ShopifyBuy.Cart | null) { 26 | if (cart == null) { 27 | return { tax: '-', total: '-' }; 28 | } 29 | 30 | const total = cart.subtotalPrice ? `$${Number(cart.subtotalPrice).toFixed(2)}` : '-'; 31 | 32 | return { 33 | tax: '-', 34 | total, 35 | }; 36 | } 37 | 38 | async function removeFromCart(variantId: string) { 39 | try { 40 | await updateItemQuantity(variantId, 0); 41 | } catch (e) { 42 | console.log(e); 43 | } 44 | } 45 | 46 | const getPrice = (price: any, currency: string) => 47 | Intl.NumberFormat(undefined, { 48 | currency, 49 | minimumFractionDigits: 2, 50 | style: 'currency', 51 | }).format(parseFloat(price ? price : 0)); 52 | 53 | const LineItem: React.FC<{ item: /*ShopifyBuy.LineItem todo: check if updated types*/ any }> = ({ 54 | item, 55 | }) => ( 56 | 57 |
58 |
59 | {item.variant.image.altText} 60 |
61 |
62 |
63 | 67 | {item.title} 68 | 69 | 70 |
  • 71 | Quantity: 72 | {item.quantity} 73 |
  • 74 | {item.variant.selectedOptions.map((option: any) => ( 75 |
  • 76 | {option.name}:{option.value} 77 |
  • 78 | ))} 79 |
    80 |
    81 | 88 | 95 | {getPrice(item.variant.priceV2.amount, item.variant.priceV2.currencyCode || 'USD')} 96 | 97 |
    98 | ); 99 | 100 | const emptyCart = ( 101 | 102 | 103 | Cart 104 | Your shopping cart is empty. 105 | 106 | ); 107 | 108 | const skeleton = todo; 109 | 110 | return ( 111 | 112 | {lineItems.length < 1 ? ( 113 | emptyCart 114 | ) : ( 115 | 116 | 117 | Cart 118 | {lineItems.map(item => ( 119 | 120 | 121 | 122 | 123 | ))} 124 |
    158 | 159 | )} 160 | 161 | ); 162 | }; 163 | 164 | export default CartPage; 165 | -------------------------------------------------------------------------------- /src/components/organisms/product-page-details/product-page-details.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { useState, useEffect, useMemo, Fragment } from 'react'; 3 | import { Styled, jsx } from 'theme-ui'; 4 | import Img from 'gatsby-image'; 5 | import { Grid, Button, Alert, Close } from '@theme-ui/components'; 6 | import SEO from '../../atoms/seo'; 7 | import Thumbnail from '../../atoms/thumbnail'; 8 | import OptionPicker from '../../atoms/option-picker'; 9 | import { 10 | prepareVariantsWithOptions, 11 | prepareVariantsImages, 12 | getPrice, 13 | } from '../../../utils/product'; 14 | import { useAddItemToCart } from 'gatsby-theme-shopify-manager/src'; 15 | 16 | export interface ProductPageDetailsProps { 17 | product: GatsbyTypes.ShopifyProduct; 18 | title: string; 19 | description: string; 20 | } 21 | 22 | const ProductPageDetails: React.FC = ({ product, title, description }) => { 23 | const colors = product?.options?.find(option => option?.name?.toLowerCase() === 'color')?.values!; 24 | const sizes = product?.options?.find(option => option?.name?.toLowerCase() === 'size')?.values; 25 | 26 | const variants = useMemo(() => prepareVariantsWithOptions(product!.variants! as any), [ 27 | product.variants, 28 | ]); 29 | const images = useMemo(() => prepareVariantsImages(variants, 'color'), [variants]); 30 | 31 | if (images.length < 1) { 32 | throw new Error('Must have at least one product image!'); 33 | } 34 | 35 | const addItemToCart = useAddItemToCart(); 36 | const [variant, setVariant] = useState(variants[0]); 37 | const [color, setColor] = useState(variant.color); 38 | const [size, setSize] = useState(variant.size); 39 | const [addedToCartMessage, setAddedToCartMessage] = useState(''); 40 | 41 | useEffect(() => { 42 | const newVariant = variants.find(variant => { 43 | return variant.size === size && variant.color === color; 44 | }); 45 | 46 | if (variant.shopifyId !== newVariant.shopifyId) { 47 | setVariant(newVariant); 48 | } 49 | }, [size, color, variants, variant.shopifyId]); 50 | 51 | const gallery = 52 | images.length > 1 ? ( 53 | 54 | {images.map(({ src, color }) => ( 55 | setColor(color)} /> 56 | ))} 57 | 58 | ) : null; 59 | 60 | async function handleAddToCart() { 61 | try { 62 | await addItemToCart(variant.shopifyId, 1); 63 | setAddedToCartMessage('🛒 Added to your cart!'); 64 | } catch (e) { 65 | setAddedToCartMessage('There was a problem adding this to your cart'); 66 | } 67 | } 68 | 69 | return ( 70 | 71 | 75 | {addedToCartMessage ? ( 76 | 77 | {addedToCartMessage} 78 | setAddedToCartMessage('')} 87 | /> 88 | 89 | ) : null} 90 | 91 |
    92 |
    99 | 104 |
    105 | {gallery} 106 |
    107 |
    108 | 109 | {title || product.title} 110 | 111 | { 112 | /** 113 | * TODO: load this from api client side for selected variant 114 | */ 115 | getPrice( 116 | product.priceRange?.maxVariantPrice?.amount!, 117 | product.priceRange?.maxVariantPrice?.currencyCode! 118 | ) 119 | } 120 | 121 | 122 |
    123 |
    124 | 125 | {colors?.length && ( 126 | setColor(event.target.value)} 132 | /> 133 | )} 134 | {sizes?.length && ( 135 | setSize(event.target.value)} 141 | /> 142 | )} 143 | 144 |
    145 | 148 |
    149 | 150 | 151 | ); 152 | }; 153 | 154 | export default ProductPageDetails; 155 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Builder.io + Shopify + Gatsby Starter 2 | 3 | :heavy_check_mark: All the power of going headless, speed of Gatsby and, the customizability of [Builder.io](https://builder.io) 4 | 5 | :heavy_check_mark: Typescript + Theme-UI 6 | 7 | :heavy_check_mark: [gatsby-theme-shopify-manager](https://github.com/thetrevorharmon/gatsby-theme-shopify-manager) The easiest way to build a shopify store on gatsby. 8 | 9 | :heavy_check_mark: Analytics, A/B testing, product augmentation, and heatmaps out of the box. 10 |
    11 |
    12 | Editor example 13 | 14 | ## Get Started 15 | 16 | ### Install the Builder.io cli 17 | 18 | ``` 19 | npm install @builder.io/cli -g 20 | ``` 21 | 22 | ### Clone this repo 23 | 24 | using git 25 | 26 | ``` 27 | git clone https://github.com/BuilderIO/gatsby-builder-shopify 28 | ``` 29 | 30 | ### Generate your Builder.io space 31 | 32 | 33 | 34 | [Signup for Builder.io](builder.io/signup), then go to your [organization settings page](https://builder.io/account/organization?root=true), create a private key and copy it, then create your space and give it a name 35 | 36 | ``` 37 | cd gatsby-builder-shopify 38 | builder create -k [private-key] -n [space-name] -d 39 | ``` 40 | 41 | This command when done it'll print your new space's public api key, copy it and add as the value for `GATSBY_BUILDER_PUBLIC_KEY` into the .env files (`.env.production` and `.env.development`) 42 | 43 | ``` 44 | GATBY_BUILDER_PUBLIC_KEY=... 45 | ``` 46 | 47 | ### Connect Shopify 48 | 49 | Now you have a space clone matching the spec defined in this repo, you'll need to connect it to your shopify store. 50 | 51 | Create a [private app](https://help.shopify.com/en/manual/apps/private-apps) in your Shpoify store and generate both admin api keys and storefront API token. 52 | 53 | Access your newly created space, by selecting it from the [list of spaces](https://builder.io/spaces) in your organization, then from space settings, configure the `@builder.io/plugin-shopify` with the required details: admin api key / password, store domain, please feel free to ignore the `import your products/collections` step since it's not needed for this starter. 54 | 55 | Add your storefront api token to the .env files (`.env.all`) 56 | 57 | ``` 58 | GATSBY_SHOPIFY_ACCESS_TOKEN=... 59 | GATSBY_SHOP_NAME=... 60 | ``` 61 | 62 | ### Install dependencies 63 | 64 | ``` 65 | yarn 66 | ``` 67 | 68 | ### Run the dev server 69 | 70 | ``` 71 | yarn develop 72 | ``` 73 | 74 | It'll start a dev server at `http://localhost:8000` 75 | 76 | ### Deploy 77 | 78 | [![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/BuilderIO/gatsby-builder-shopify) 79 | For continuous deployment from netlify <> Builder.io : 80 | - Create a [build hook](https://docs.netlify.com/configure-builds/build-hooks/) in netlify 81 | - Add the build hook from last step to Builder.io global webhooks in your new [space settings](https://builder.io/account/space). 82 | 83 | 84 | ## 🧐 What's inside? 85 | 86 | This starter demonstrates: 87 | 88 | - creating product pages in builder.io for easier a/b testing and cusotm targeting. 89 | - shows how to pass context with the editor for binding components to dynamic state object for easier templating, for things like product pages, collection pages. 90 | - shows how can you customize and augment your data, for example a specific product in shopify you want to override it's description for an a/b test, that's as simple as setting a default binding, and allowing users to break the binding for a specific product handle. 91 | 92 | See: 93 | 94 | - [src/components/molecules/aware-builder-component.tsx](src/components/molecules/aware-builder-component.tsx) 95 | - [src/templates/product-page.tsx](src/templates/product-page.tsx) for using GraphQL to query and render Builder.io components 96 | - [@builder.io/gatsby](https://github.com/builderio/builder/tree/master/packages/gatsby) the plugin used in this starter to generate pages dynamically. 97 | 98 | ### Using your custom components in the editor 99 | 100 | > 👉**Tip: want to limit page building to only your components? Try [components only mode](https://builder.io/c/docs/guides/components-only-mode)** 101 | 102 | Register a component 103 | 104 | ```tsx 105 | import { Builder } from '@builder.io/react'; 106 | 107 | class SimpleText extends React.Component { 108 | render() { 109 | return

    {this.props.text}

    ; 110 | } 111 | } 112 | 113 | Builder.registerComponent(SimpleText, { 114 | name: 'Simple Text', 115 | inputs: [{ name: 'text', type: 'string' }], 116 | }); 117 | ``` 118 | 119 | Then import it in the template entry point 120 | 121 | ```tsx 122 | import './components/simple-text'; 123 | // ... 124 | ``` 125 | 126 | See: 127 | 128 | - [design systems example](https://github.com/BuilderIO/builder/tree/master/examples/react-design-system) for lots of examples using your deisgn system + custom components 129 | 130 | ### Mixed Content errors when hosting on insecure http 131 | 132 | Our editor uses the preview URL you supply for live editing. Because the editor is on `https`, the preview might not work correctly if your development setup uses http. To fix this, change your development set up to serve using https. Or, as a workaround, on Chrome you can allow insecure content on localhost, by toggling the `insecure content` option here [chrome://settings/content/siteDetails?site=http%3A%2F%2Flocalhost%3A9009](chrome://settings/content/siteDetails?site=http%3A%2F%2Flocalhost%3A8000) 133 | 134 | ## Prerequisites 135 | 136 | - Node 137 | - [Gatsby CLI](https://www.gatsbyjs.org/docs/) 138 | 139 | ## Available scripts 140 | 141 | ### `build` 142 | 143 | Build the static files into the `public` folder 144 | 145 | #### Usage 146 | 147 | ```sh 148 | $ yarn build 149 | ``` 150 | 151 | ### `develop` or `start` 152 | 153 | Runs the `clean` script and starts the gatsby develop server using the command `gatsby develop`. 154 | 155 | #### Usage 156 | 157 | ```sh 158 | yarn develop 159 | ``` 160 | 161 | ### `format` 162 | 163 | Formats code and docs according to our style guidelines using `prettier` 164 | 165 | ## CONTRIBUTING 166 | 167 | Contributions are always welcome, no matter how large or small. 168 | 169 | ## Learn more 170 | 171 | - [Official docs](https://www.builder.io/c/docs/getting-started) 172 | --------------------------------------------------------------------------------