├── .editorconfig ├── .env.development ├── .env.production ├── .env.template ├── .gitignore ├── .prettierignore ├── .vscode └── settings.json ├── README.md ├── assets ├── base.css ├── components.css └── main.css ├── blocks ├── CollectionView │ ├── CollectionView.builder.ts │ └── CollectionView.tsx ├── ProductGrid │ ├── ProductGrid.builder.ts │ └── ProductGrid.tsx ├── ProductView │ ├── ProductLoader.tsx │ ├── ProductView.builder.ts │ ├── ProductView.tsx │ └── ProductViewDemo.tsx └── utils.ts ├── builder ├── announcement-bar │ ├── announcement-bar.json │ └── schema.model.json ├── cart-upsell-sidebar │ └── schema.model.json ├── collection-page │ ├── generic-collection.json │ └── schema.model.json ├── page │ ├── about-us.json │ ├── faq.json │ └── schema.model.json ├── product-page │ ├── generic-template.json │ └── schema.model.json ├── settings.json └── theme │ ├── schema.model.json │ └── site-theme.json ├── components ├── cart │ ├── CartItem │ │ ├── CartItem.tsx │ │ └── index.ts │ ├── CartSidebarView │ │ ├── CartSidebarView.tsx │ │ └── index.ts │ └── index.ts ├── common │ ├── FeatureBar.tsx │ ├── Head.tsx │ ├── Layout.tsx │ ├── Navbar.tsx │ ├── NoSSR.tsx │ ├── OptionPicker.tsx │ ├── ProductCard.tsx │ ├── ProductCardDemo.tsx │ ├── Searchbar.tsx │ ├── Thumbnail.tsx │ ├── UntilInteraction.tsx │ ├── UserNav.tsx │ └── index.ts ├── icons │ ├── ArrowLeft.tsx │ ├── Bag.tsx │ ├── Check.tsx │ ├── ChevronUp.tsx │ ├── Cross.tsx │ ├── DoubleChevron.tsx │ ├── Github.tsx │ ├── Heart.tsx │ ├── Info.tsx │ ├── Minus.tsx │ ├── Moon.tsx │ ├── Plus.tsx │ ├── RightArrow.tsx │ ├── Sun.tsx │ ├── Trash.tsx │ ├── Vercel.tsx │ └── index.ts └── ui │ ├── ImageCarousel.tsx │ ├── LazyImageCarousel.tsx │ ├── Link │ ├── Link.tsx │ └── index.ts │ ├── LoadingDots │ ├── LoadingDots.module.css │ ├── LoadingDots.tsx │ └── index.ts │ ├── README.md │ ├── Sidebar │ ├── Sidebar.tsx │ └── index.ts │ ├── context.tsx │ └── index.ts ├── config ├── builder.ts ├── env.ts ├── seo.json ├── swell.ts └── theme.ts ├── docs ├── ROADMAP.md └── images │ ├── builder-io-organizations.png │ ├── private-key-flow.png │ ├── shopify-api-key-mapping.png │ ├── shopify-permissions.png │ └── shopify-private-app-flow.png ├── global.d.ts ├── lib ├── click-outside │ ├── click-outside.tsx │ ├── has-parent.js │ ├── index.ts │ └── is-in-dom.js ├── colors.ts ├── defaults.ts ├── get-layout-props.ts ├── hooks │ └── useAcceptCookies.ts ├── logger.ts ├── range-map.ts ├── resolve-swell-content.ts ├── swell │ └── storefront-data-hooks │ │ ├── index.ts │ │ └── src │ │ ├── CommerceProvider.tsx │ │ ├── Context.tsx │ │ ├── api │ │ ├── operations-swell.ts │ │ └── typings.d.ts │ │ ├── hooks │ │ ├── index.ts │ │ ├── useAddItemToCart.ts │ │ ├── useAddItemsToCart.ts │ │ ├── useCart.ts │ │ ├── useCartCount.ts │ │ ├── useCartItems.ts │ │ ├── useCheckoutUrl.ts │ │ ├── useGetLineItem.ts │ │ ├── useRemoveItemFromCart.ts │ │ └── useUpdateItemQuantity.ts │ │ ├── types.ts │ │ └── utils │ │ └── product.ts └── to-pixels.ts ├── license.md ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── [[...path]].tsx ├── _app.tsx ├── _document.tsx ├── cart.tsx ├── collection │ └── [handle].tsx └── product │ └── [handle].tsx ├── postcss.config.js ├── public ├── bg-products.svg ├── cursor-left.png ├── cursor-right.png ├── flag-en-us.svg ├── flag-es-ar.svg ├── flag-es-co.svg ├── flag-es.svg ├── icon-144x144.png ├── icon-192x192.png ├── icon-512x512.png ├── icon.png ├── jacket.png ├── site.webmanifest ├── slider-arrows.png └── vercel.svg ├── tailwind.config.js ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | tab_width = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [*.js] 16 | quote_type = single 17 | 18 | [{*.c,*.cc,*.h,*.hh,*.cpp,*.hpp,*.m,*.mm,*.mpp,*.js,*.java,*.go,*.rs,*.php,*.ng,*.jsx,*.ts,*.d,*.cs,*.swift}] 19 | curly_bracket_next_line = false 20 | spaces_around_operators = true 21 | spaces_around_brackets = outside 22 | # close enough to 1TB 23 | indent_brace_style = K&R 24 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | BUILDER_PUBLIC_KEY= 2 | SWELL_STORE_ID= 3 | SWELL_PUBLIC_KEY= -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | BUILDER_PUBLIC_KEY= 2 | SWELL_STORE_ID= 3 | SWELL_PUBLIC_KEY= -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | BUILDER_PUBLIC_KEY= 2 | SWELL_STORE_ID= 3 | SWELL_PUBLIC_KEY= 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # dev 37 | framework 38 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | public -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "git.ignoreLimitWarning": true 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fswellstores%2Fnextjs-builder&env=BUILDER_PUBLIC_KEY,SWELL_STORE_ID,SWELL_PUBLIC_KEY&envDescription=API%20keys%20needed%20to%20connect%20to%20your%20Swell%20store%20and%20Builder%20dashboard&envLink=https%3A%2F%2Fgithub.com%2Fswellstores%2Fnextjs-builder%2Fblob%2Fmaster%2FREADME.md) 3 | 4 | # Next.js + Swell + Builder.io starter kit 5 | 6 | The ultimate starter for headless Swell stores. 7 | 8 | Demo live at [Nextjs-builder](https://builder-demo-theta.vercel.app/about-us) 9 | 10 | ## Goals and Features 11 | 12 | - Ultra high performance 13 | - SEO optimized 14 | - Themable 15 | - Personalizable (internationalization, a/b testing, etc) 16 | - Builder.io Visual CMS integrated 17 | - Connect to Swell data through Builder's high speed data layer 18 | 19 | ## Table of contents 20 | 21 | - [Getting Started](#getting-started) 22 | - [1: Create an account for Builder.io](#1-create-an-account-for-builderio) 23 | - [2: Your Builder.io private key](#2-your-builderio-private-key) 24 | - [3: Clone this repository and initialize a Builder.io space](#3-clone-this-repository-and-initialize-a-builderio-space) 25 | - [4. Connecting Builder to Swell](#4-connecting-builder-to-swell) 26 | - [5. Configure the project to talk to Swell](#5-configure-the-project-to-talk-to-swell) 27 | - [6. Up and Running!](#6-up-and-running) 28 | - [7. Start building!](#7-start-building) 29 | - [Deploy](#deployment-options) 30 | 31 | 32 | ## Getting Started 33 | 34 | **Pre-requisites** 35 | 36 | This guide will assume that you have the following software installed: 37 | 38 | - nodejs (>=12.0.0) 39 | - npm or yarn 40 | - git 41 | 42 | You should already have a [Swell](https://swell.store/signup) account and store created before starting as well. 43 | 44 | **Introduction** 45 | 46 | This starter kit is everything you need to get your own self hosted 47 | Next.js project powered by Builder.io for content and Swell as an 48 | e-commerce back office. 49 | 50 | After following this guide you will have 51 | 52 | - A Next.js app, ready to deploy to a hosting provider of your choice 53 | - Pulling live collection and product information from Swell 54 | - Powered by the Builder.io visual CMS 55 | 56 | ### 1: Create an account for Builder.io 57 | 58 | Before we start, head over to Builder.io and [create an account](https://builder.io/signup). 59 | 60 | ### 2: Your Builder.io private key 61 | 62 | Head over to your [organization settings page](https://builder.io/account/organization?root=true) and create a 63 | private key, copy the key for the next step. 64 | 65 | - Visit the [organization settings page](https://builder.io/account/organization?root=true), or select 66 | an organization from the list 67 | 68 | ![organizations drop down list](./docs/images/builder-io-organizations.png) 69 | 70 | - Click "Account" from the left hand sidebar 71 | - Click the edit icon for the "Private keys" row 72 | - Copy the value of the auto-generated key, or create a new one with a name that's meaningful to you 73 | 74 | 75 | ![Example of how to get your private key](./docs/images/private-key-flow.png) 76 | 77 | ### 3: Clone this repository and initialize a Builder.io space 78 | 79 | Next, we'll create a copy of the starter project, and create a new 80 | [space](https://www.builder.io/c/docs/spaces) for it's content to live 81 | in. 82 | 83 | In the example below, replace `` with the key you copied 84 | in the previous step, and change `` to something that's 85 | meaningful to you -- don't worry, you can change it later! 86 | 87 | ``` 88 | git clone https://github.com/swellstores/nextjs-builder.git 89 | cd nextjs-builder 90 | 91 | npm install --global "@builder.io/cli" 92 | 93 | builder create --key "" --name "" --debug 94 | ``` 95 | 96 | If this was a success you should be greeted with a message that 97 | includes a public API key for your newly minted Builder.io space. 98 | 99 | *Note: This command will also publish some starter builder.io cms 100 | content from the ./builder directory to your new space when it's 101 | created.* 102 | 103 | ``` bash 104 | ____ _ _ _ _ _ _ 105 | | __ ) _ _ (_) | | __| | ___ _ __ (_) ___ ___ | | (_) 106 | | _ \ | | | | | | | | / _` | / _ \ | '__| | | / _ \ / __| | | | | 107 | | |_) | | |_| | | | | | | (_| | | __/ | | _ | | | (_) | | (__ | | | | 108 | |____/ \__,_| |_| |_| \__,_| \___| |_| (_) |_| \___/ \___| |_| |_| 109 | 110 | |████████████████████████████████████████| swell-product | 0/0 111 | |████████████████████████████████████████| product-page: writing generic-template.json | 1/1 112 | |████████████████████████████████████████| swell-collection | 0/0 113 | |████████████████████████████████████████| collection-page: writing generic-collection.json | 1/1 114 | |████████████████████████████████████████| page: writing homepage.json | 2/2 115 | 116 | 117 | Your new space "next.js Swell starter" public API Key: 012345abcdef0123456789abcdef0123 118 | ``` 119 | 120 | Copy the public API key ("012345abcdef0123456789abcdef0123" in the example above) for the next step. 121 | 122 | This starter project uses dotenv files to configure environment variables. 123 | Open the files [.env.development](./.env.development) and 124 | [.env.production](./.env.production) in your favorite text editor, and 125 | set the value of `BUILDER_PUBLIC_KEY` to the public key you just copied. 126 | You can ignore the other variables for now, we'll set them later. 127 | 128 | ```diff 129 | + BUILDER_PUBLIC_KEY=012345abcdef0123456789abcdef0123 130 | - BUILDER_PUBLIC_KEY= 131 | SWELL_STORE_ID= 132 | SWELL_PUBLIC_KEY= 133 | ``` 134 | 135 | ### 4. Connecting Builder to Swell 136 | 137 | Access your newly created space by selecting it from the [list of spaces](https://builder.io/spaces?root=true) 138 | in your organization. 139 | 140 | Ensure the Swell plugin is connected, by editing 'Plugins' listed in your space settings. Update the store id and public key if necessary. 141 | 142 | ### 5. Configure the project to talk to Swell 143 | 144 | Open up [.env.development](./.env.development) and [.env.production](./.env.production) again, 145 | but this time set the other two Swell keys. 146 | 147 | ```diff 148 | BUILDER_PUBLIC_KEY=012345abcdef0123456789abcdef0123 149 | + SWELL_STORE_ID=my-store 150 | - SWELL_STORE_ID= 151 | + SWELL_PUBLIC_KEY=c11b4053408085753bd76a45806f80dd 152 | - SWELL_PUBLIC_KEY= 153 | ``` 154 | 155 | ### 6. Up and Running! 156 | 157 | The hard part is over, all you have to do is start up the project now. 158 | 159 | ```bash 160 | npm install 161 | npm run dev 162 | ``` 163 | or 164 | ```bash 165 | yarn && yarn dev 166 | ``` 167 | 168 | This will start a server at `http://localhost:3000`. 169 | 170 | Go to your [new space settings](https://builder.io/account/space) and change the site url to your localhost `http://localhost:3000` for site editing. 171 | 172 | 173 | ### 7. Start building 174 | 175 | Now that we have everything setup, start building and publishing pages on builder.io! 176 | 177 | ## Deployment Options 178 | 179 | You can deploy this code anywhere you like - you can find many deployment options for Next.js [here](https://nextjs.org/docs/deployment). 180 | 181 | Don't forget to update the Site URL to point to the production URL when ready. 182 | -------------------------------------------------------------------------------- /assets/base.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary: #ffffff; 3 | --primary-2: #f1f3f5; 4 | --secondary: #000000; 5 | --secondary-2: #111; 6 | 7 | --selection: var(--cyan); 8 | 9 | --text-base: #000000; 10 | --text-primary: #000000; 11 | --text-secondary: white; 12 | 13 | --hover: rgba(0, 0, 0, 0.075); 14 | --hover-1: rgba(0, 0, 0, 0.15); 15 | --hover-2: rgba(0, 0, 0, 0.25); 16 | 17 | --cyan: #22b8cf; 18 | --green: #37b679; 19 | --red: #da3c3c; 20 | --pink: #e64980; 21 | --purple: #f81ce5; 22 | 23 | --blue: #0070f3; 24 | 25 | --violet-light: #7048e8; 26 | --violet: #5f3dc4; 27 | 28 | --accents-0: #f8f9fa; 29 | --accents-1: #f1f3f5; 30 | --accents-2: #e9ecef; 31 | --accents-3: #dee2e6; 32 | --accents-4: #ced4da; 33 | --accents-5: #adb5bd; 34 | --accents-6: #868e96; 35 | --accents-7: #495057; 36 | --accents-8: #343a40; 37 | --accents-9: #212529; 38 | --font-sans: -apple-system, system-ui, BlinkMacSystemFont, 'Helvetica Neue', 39 | 'Helvetica', sans-serif; 40 | } 41 | 42 | [data-theme='dark'] { 43 | --primary: #000000; 44 | --primary-2: #111; 45 | --secondary: #ffffff; 46 | --secondary-2: #f1f3f5; 47 | --hover: rgba(255, 255, 255, 0.075); 48 | --hover-1: rgba(255, 255, 255, 0.15); 49 | --hover-2: rgba(255, 255, 255, 0.25); 50 | --selection: var(--purple); 51 | 52 | --text-base: white; 53 | --text-primary: white; 54 | --text-secondary: black; 55 | 56 | --accents-0: #212529; 57 | --accents-1: #343a40; 58 | --accents-2: #495057; 59 | --accents-3: #868e96; 60 | --accents-4: #adb5bd; 61 | --accents-5: #ced4da; 62 | --accents-6: #dee2e6; 63 | --accents-7: #e9ecef; 64 | --accents-8: #f1f3f5; 65 | --accents-9: #f8f9fa; 66 | } 67 | 68 | *, 69 | *:before, 70 | *:after { 71 | box-sizing: inherit; 72 | } 73 | 74 | html { 75 | height: 100%; 76 | box-sizing: border-box; 77 | touch-action: manipulation; 78 | font-feature-settings: 'case' 1, 'rlig' 1, 'calt' 0; 79 | text-rendering: optimizeLegibility; 80 | -webkit-font-smoothing: antialiased; 81 | -moz-osx-font-smoothing: grayscale; 82 | } 83 | 84 | html, 85 | body { 86 | font-family: var(--font-sans); 87 | text-rendering: optimizeLegibility; 88 | -webkit-font-smoothing: antialiased; 89 | -moz-osx-font-smoothing: grayscale; 90 | background-color: var(--primary); 91 | color: var(--text-primary); 92 | } 93 | 94 | body { 95 | position: relative; 96 | min-height: 100%; 97 | margin: 0; 98 | } 99 | 100 | a { 101 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 102 | } 103 | 104 | .animated { 105 | -webkit-animation-duration: 1s; 106 | animation-duration: 1s; 107 | -webkit-animation-duration: 1s; 108 | animation-duration: 1s; 109 | -webkit-animation-fill-mode: both; 110 | animation-fill-mode: both; 111 | } 112 | 113 | .fadeIn { 114 | -webkit-animation-name: fadeIn; 115 | animation-name: fadeIn; 116 | } 117 | 118 | @-webkit-keyframes fadeIn { 119 | from { 120 | opacity: 0; 121 | } 122 | 123 | to { 124 | opacity: 1; 125 | } 126 | } 127 | 128 | @keyframes fadeIn { 129 | from { 130 | opacity: 0; 131 | } 132 | 133 | to { 134 | opacity: 1; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /assets/components.css: -------------------------------------------------------------------------------- 1 | .fit { 2 | min-height: calc(100vh - 88px); 3 | } 4 | 5 | input::-webkit-outer-spin-button, 6 | input::-webkit-inner-spin-button { 7 | -webkit-appearance: none; 8 | margin: 0; 9 | } 10 | 11 | input[type=number] { 12 | -moz-appearance: textfield; 13 | } -------------------------------------------------------------------------------- /assets/main.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @import './base.css'; 3 | 4 | @tailwind components; 5 | @import './components.css'; 6 | 7 | @tailwind utilities; 8 | -------------------------------------------------------------------------------- /blocks/CollectionView/CollectionView.builder.ts: -------------------------------------------------------------------------------- 1 | import { Input } from '@builder.io/sdk' 2 | import dynamic from 'next/dynamic' 3 | import { productGridSchema } from '../ProductGrid/ProductGrid.builder' 4 | import { restrictedRegister } from 'blocks/utils' 5 | const LazyCollectionView = dynamic(() => import(`./CollectionView`)) 6 | 7 | const collectionBoxSchema: Input[] = [ 8 | { 9 | name: 'productGridOptions', 10 | type: 'object', 11 | subFields: productGridSchema, 12 | defaultValue: { 13 | cardProps: { 14 | imgPriority: true, 15 | imgLayout: 'responsive', 16 | imgLoading: 'eager', 17 | imgWidth: 540, 18 | imgHeight: 540, 19 | layout: 'fixed', 20 | }, 21 | }, 22 | }, 23 | { 24 | type: 'boolean', 25 | name: 'renderSeo', 26 | advanced: true, 27 | helperText: 28 | 'toggle to render seo info on page, only use for collection pages', 29 | }, 30 | ] 31 | 32 | restrictedRegister( 33 | LazyCollectionView, 34 | { 35 | name: 'CollectionBox', 36 | description: 'Pick a collection to display its details', 37 | image: 'https://unpkg.com/css.gg@2.0.0/icons/svg/collage.svg', 38 | inputs: collectionBoxSchema 39 | .concat([ 40 | { 41 | name: 'collection', 42 | type: 'SwellCategoryHandle', 43 | }, 44 | ]) 45 | .reverse(), 46 | }, 47 | ['page', 'product-page', 'theme'] 48 | ) 49 | 50 | restrictedRegister( 51 | LazyCollectionView, 52 | { 53 | name: 'CollectionView', 54 | description: 55 | 'Dynamic collection detaills, autobinds to the collection in context, use only on collection pages', 56 | inputs: collectionBoxSchema, 57 | defaults: { 58 | bindings: { 59 | 'component.options.collection': 'state.collection', 60 | 'component.options.renderSeo': 'true', 61 | }, 62 | }, 63 | }, 64 | ['collection-page', 'theme'] 65 | ) 66 | 67 | -------------------------------------------------------------------------------- /blocks/CollectionView/CollectionView.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxRuntime classic */ 2 | /** @jsx jsx */ 3 | import React, { FC, useState, useEffect } from 'react' 4 | import { NextSeo } from 'next-seo' 5 | import { Themed, jsx } from 'theme-ui' 6 | import { LoadingDots } from '@components/ui' 7 | import builderConfig from '@config/builder' 8 | import { ProductGrid, ProductGridProps } from '../ProductGrid/ProductGrid' 9 | import { getCollection } from '@lib/swell/storefront-data-hooks/src/api/operations-swell' 10 | 11 | interface Props { 12 | className?: string 13 | children?: any 14 | collection: string | any 15 | productGridOptions: ProductGridProps 16 | renderSeo?: boolean 17 | } 18 | 19 | const CollectionPreview: FC = ({ 20 | collection: initialCollection, 21 | productGridOptions, 22 | renderSeo, 23 | }) => { 24 | const [collection, setCollection] = useState(initialCollection) 25 | const [loading, setLoading] = useState(false) 26 | 27 | useEffect(() => setCollection(initialCollection), [initialCollection]) 28 | 29 | useEffect(() => { 30 | const fetchCollection = async () => { 31 | setLoading(true) 32 | const result = await getCollection(builderConfig, { 33 | handle: collection, 34 | }) 35 | setCollection(result) 36 | setLoading(false) 37 | } 38 | if (typeof collection === 'string') { 39 | fetchCollection() 40 | } 41 | }, [collection]) 42 | 43 | if (!collection || typeof collection === 'string' || loading) { 44 | return 45 | } 46 | 47 | const { title, description, products } = collection 48 | return ( 49 | 53 | {renderSeo && ( 54 | 63 | )} 64 |
65 | 66 | {collection.title} 67 | 68 |
69 |
70 | 71 | 72 | 73 | 74 | ) 75 | } 76 | 77 | export default CollectionPreview 78 | -------------------------------------------------------------------------------- /blocks/ProductGrid/ProductGrid.builder.ts: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | import { Builder } from '@builder.io/react' 3 | import { Input } from '@builder.io/sdk' 4 | const LazyProductGrid = dynamic(async () => { 5 | return (await import('./ProductGrid')).ProductGrid 6 | }) 7 | const isDemo = Boolean(process.env.IS_DEMO) 8 | 9 | const productCardFields: Input[] = [ 10 | { 11 | name: 'imgWidth', 12 | type: 'number', 13 | defaultValue: 540, 14 | }, 15 | { 16 | name: 'imgHeight', 17 | type: 'number', 18 | defaultValue: 540, 19 | }, 20 | { 21 | name: 'imgPriority', 22 | type: 'boolean', 23 | advanced: true, 24 | defaultValue: true, 25 | }, 26 | { 27 | name: 'imgLoading', 28 | type: 'enum', 29 | advanced: true, 30 | defaultValue: 'lazy', 31 | enum: ['eager', 'lazy'], 32 | }, 33 | { 34 | name: 'imgLayout', 35 | type: 'enum', 36 | enum: ['fixed', 'intrinsic', 'responsive', 'fill'], 37 | advanced: true, 38 | defaultValue: 'fill', 39 | }, 40 | ] 41 | 42 | export const productGridSchema: Input[] = [ 43 | { 44 | name: 'cardProps', 45 | defaultValue: { 46 | imgPriority: true, 47 | imgLayout: 'responsive', 48 | imgLoading: 'eager', 49 | imgWidth: 540, 50 | imgHeight: 540, 51 | layout: 'fixed', 52 | }, 53 | type: 'object', 54 | subFields: productCardFields, 55 | }, 56 | { 57 | name: 'offset', 58 | type: 'number', 59 | defaultValue: 0, 60 | }, 61 | { 62 | name: 'limit', 63 | type: 'number', 64 | defaultValue: 9, 65 | }, 66 | ] 67 | 68 | Builder.registerComponent(LazyProductGrid, { 69 | name: 'ProductGrid', 70 | image: 'https://unpkg.com/css.gg@2.0.0/icons/svg/play-list-add.svg', 71 | description: 'Pick products free form', 72 | inputs: [ 73 | { 74 | name: 'productsList', 75 | type: 'list', 76 | subFields: [ 77 | { 78 | name: 'product', 79 | type: `SwellProductHandle`, 80 | }, 81 | ], 82 | }, 83 | ].concat(productGridSchema as any), 84 | }) 85 | 86 | Builder.registerComponent(LazyProductGrid, { 87 | name: 'ProductCollectionGrid', 88 | image: 'https://unpkg.com/css.gg@2.0.0/icons/svg/display-grid.svg', 89 | description: 'Choose a collection to show its products in a grid', 90 | inputs: [ 91 | { 92 | name: 'collection', 93 | type: `SwellCategoryHandle`, 94 | }, 95 | ].concat(productGridSchema), 96 | }) 97 | 98 | -------------------------------------------------------------------------------- /blocks/ProductGrid/ProductGrid.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxRuntime classic */ 2 | /** @jsx jsx */ 3 | import { jsx } from 'theme-ui' 4 | import { FC, useEffect, useState } from 'react' 5 | import { LoadingDots } from '@components/ui' 6 | import { Grid } from '@theme-ui/components' 7 | import { ProductCardProps } from '@components/common/ProductCard' 8 | import { ProductCardDemo, ProductCard } from '@components/common' 9 | import { Product } from '@lib/swell/storefront-data-hooks/src/types' 10 | 11 | import { 12 | getCollection, 13 | getProduct, 14 | } from '@lib/swell/storefront-data-hooks/src/api/operations-swell' 15 | import builderConfig from '@config/builder' 16 | interface HighlightedCardProps extends Omit { 17 | index: number 18 | } 19 | 20 | export interface ProductGridProps { 21 | products?: any[] 22 | productsList?: Array<{ product: string }> 23 | collection?: string | any 24 | offset: number 25 | limit: number 26 | cardProps: Omit 27 | highlightCard?: HighlightedCardProps 28 | } 29 | 30 | export const ProductGrid: FC = ({ 31 | products: initialProducts, 32 | collection, 33 | productsList, 34 | offset = 0, 35 | limit = 10, 36 | cardProps, 37 | highlightCard, 38 | }) => { 39 | const [products, setProducts] = useState(initialProducts || []) 40 | const [loading, setLoading] = useState(false) 41 | 42 | useEffect(() => { 43 | const getProducts = async () => { 44 | setLoading(true) 45 | const promises = productsList! 46 | .map((entry) => entry.product) 47 | .filter((handle: string | undefined) => typeof handle === 'string') 48 | .map( 49 | async (handle: string) => { 50 | return await getProduct({ slug: handle }) 51 | } 52 | ) 53 | const result = await Promise.all(promises) 54 | setProducts(result) 55 | setLoading(false) 56 | } 57 | if (productsList && !initialProducts) { 58 | getProducts() 59 | } 60 | }, [productsList, initialProducts]) 61 | 62 | useEffect(() => { 63 | const fetchCollection = async () => { 64 | setLoading(true) 65 | const result = await getCollection(builderConfig, { 66 | handle: collection, 67 | }) 68 | setProducts(result.products) 69 | setLoading(false) 70 | } 71 | if (typeof collection === 'string' && !initialProducts) { 72 | fetchCollection() 73 | } 74 | }, [collection]) 75 | 76 | if (loading) { 77 | return 78 | } 79 | const ProductComponent: any = process.env.IS_DEMO 80 | ? ProductCardDemo 81 | : ProductCard 82 | 83 | return ( 84 | 85 | {products.slice(offset, limit).map((product, i) => ( 86 | 91 | ))} 92 | 93 | ) 94 | } 95 | -------------------------------------------------------------------------------- /blocks/ProductView/ProductLoader.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxRuntime classic */ 2 | /** @jsx jsx */ 3 | import React, { useState, useEffect } from 'react' 4 | import { jsx } from 'theme-ui' 5 | import { getProduct } from '@lib/swell/storefront-data-hooks/src/api/operations-swell' 6 | import builderConfig from '@config/builder' 7 | import { LoadingDots } from '@components/ui' 8 | 9 | interface Props { 10 | className?: string 11 | children: (product: any) => React.ReactElement 12 | product: string | any 13 | } 14 | 15 | const ProductLoader: React.FC = ({ 16 | product: initialProduct, 17 | children, 18 | }) => { 19 | const [product, setProduct] = useState(initialProduct) 20 | const [loading, setLoading] = useState(false) 21 | 22 | useEffect(() => setProduct(initialProduct), [initialProduct]) 23 | 24 | useEffect(() => { 25 | const fetchProduct = async () => { 26 | setLoading(true) 27 | const result = await getProduct({ 28 | slug: String(product), 29 | }) 30 | setProduct(result) 31 | setLoading(false) 32 | } 33 | if (typeof product === 'string') { 34 | fetchProduct() 35 | } 36 | }, [product]) 37 | 38 | if (!product || typeof product === 'string' || loading) { 39 | return 40 | } 41 | return children(product) 42 | } 43 | 44 | export default ProductLoader 45 | -------------------------------------------------------------------------------- /blocks/ProductView/ProductView.builder.ts: -------------------------------------------------------------------------------- 1 | import { restrictedRegister } from 'blocks/utils' 2 | import dynamic from 'next/dynamic' 3 | 4 | const isDemo = Boolean(process.env.IS_DEMO) 5 | const LazyProductView = dynamic( 6 | () => 7 | isDemo 8 | ? import(`blocks/ProductView/ProductViewDemo`) 9 | : import(`blocks/ProductView/ProductView`), 10 | { ssr: true } 11 | ) 12 | 13 | restrictedRegister( 14 | LazyProductView, 15 | { 16 | name: 'ProductView', 17 | image: 'https://unpkg.com/css.gg@2.0.0/icons/svg/inpicture.svg', 18 | description: 19 | 'Product details, should only be used in product page template, dynamically bind to product in context.', 20 | defaults: { 21 | bindings: { 22 | 'component.options.product': 'state.product', 23 | 'component.options.title': 'state.product.title', 24 | 'component.options.description': 'state.product.descriptionHtml', 25 | }, 26 | }, 27 | }, 28 | ['product-page', 'theme'] 29 | ) 30 | 31 | restrictedRegister( 32 | LazyProductView, 33 | { 34 | name: 'ProductBox', 35 | inputs: [ 36 | { 37 | name: 'product', 38 | type: `SwellProductHandle`, 39 | }, 40 | { 41 | name: 'description', 42 | richText: true, 43 | type: 'html', 44 | helperText: 'Override product description from swell', 45 | }, 46 | { 47 | name: 'title', 48 | type: 'text', 49 | helperText: 'Override product title from swell', 50 | }, 51 | ], 52 | image: 'https://unpkg.com/css.gg@2.0.0/icons/svg/ereader.svg', 53 | description: 'Choose a product to show its details on page', 54 | }, 55 | ['page', 'collection-page', 'theme'] 56 | ) 57 | -------------------------------------------------------------------------------- /blocks/ProductView/ProductView.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxRuntime classic */ 2 | /** @jsx jsx */ 3 | import React, { useMemo, useState, useEffect } from 'react' 4 | import { Themed, jsx } from 'theme-ui' 5 | import { Grid, Button } from '@theme-ui/components' 6 | import OptionPicker from '@components/common/OptionPicker' 7 | import { NextSeo } from 'next-seo' 8 | import { useUI } from '@components/ui/context' 9 | import { useAddItemToCart } from '@lib/swell/storefront-data-hooks' 10 | import { 11 | prepareVariantsWithOptions, 12 | prepareVariantsImages, 13 | getPrice, 14 | } from '@lib/swell/storefront-data-hooks/src/utils/product' 15 | import { ImageCarousel, LoadingDots } from '@components/ui' 16 | import ProductLoader from './ProductLoader' 17 | import { OptionInput, OptionValue, ProductOption, Product } from '@lib/swell/storefront-data-hooks/src/types' 18 | 19 | export interface ProductProps { 20 | className?: string 21 | children?: any 22 | product: Product 23 | renderSeo?: boolean 24 | description?: string 25 | title?: string 26 | } 27 | 28 | 29 | const ProductBox: React.FC = ({ 30 | product, 31 | renderSeo = true, 32 | description = product.description, 33 | title = product.name, 34 | }) => { 35 | 36 | const [loading, setLoading] = useState(false) 37 | const addItem = useAddItemToCart() 38 | 39 | const formatOptionValues = (values: OptionValue[]) => { 40 | return values.map(value => value.name); 41 | } 42 | const variants = useMemo( 43 | () => prepareVariantsWithOptions(product), 44 | [product] 45 | ) 46 | 47 | interface Selection extends OptionInput { 48 | id: string 49 | name: string 50 | value: string 51 | } 52 | 53 | const options = product?.options; 54 | 55 | const defaultSelections: Selection[] = options?.filter(options => options.values?.length).map((option) => { 56 | return { 57 | id: option.values[0].id, name: option.name, value: option.values[0].name 58 | } 59 | }) 60 | 61 | 62 | const images = useMemo(() => prepareVariantsImages(variants, 'color'), [ 63 | variants, 64 | ]) 65 | 66 | function setSelectedVariant() { 67 | const selectedVariant = variants.find((variant) => { 68 | return variant.option_value_ids?.every((id: string) => { 69 | 70 | return selections.find(selection => { 71 | return selection.id==id 72 | }) 73 | }) 74 | }) 75 | if (selectedVariant) { 76 | setVariant(selectedVariant) 77 | } 78 | } 79 | 80 | const { openSidebar } = useUI() 81 | 82 | const [variant, setVariant] = useState(variants[0] || null) 83 | const [selections, setSelections] = useState(defaultSelections) 84 | const [ productOptions, setProductOptions ] = useState(options); 85 | 86 | function inputChangeHandler(option: ProductOption, value: string) { 87 | const { name, values } = option; 88 | const id = values.find((optionValue => optionValue.name == value))?.id ?? ''; 89 | const selectionToUpdate = selections.find(selection => { 90 | return selection.name == name; 91 | }) 92 | 93 | if (selectionToUpdate) { 94 | selectionToUpdate.value = value 95 | selectionToUpdate.id = id; 96 | 97 | setSelections(selections) 98 | setSelectedVariant() 99 | } 100 | } 101 | 102 | const addToCart = async () => { 103 | setLoading(true) 104 | try { 105 | await addItem(product.id, 1, selections) 106 | openSidebar() 107 | setLoading(false) 108 | } catch (err) { 109 | setLoading(false) 110 | } 111 | } 112 | const allImages = images 113 | .map((image) => ({ src: image.src})) 114 | .concat( 115 | product.images && 116 | product.images.filter( 117 | ({ file }) => !images.find((image) => image.file?.url === file?.url) 118 | ).map(productImage => ({ ...productImage, src: productImage.file?.url ?? 'https://via.placeholder.com/1050x1050' })) 119 | ) 120 | 121 | useEffect(() => { 122 | setSelections(defaultSelections) 123 | setSelectedVariant(); 124 | }, []) 125 | 126 | return ( 127 | 128 | {renderSeo && ( 129 | 146 | )} 147 | 148 |
149 |
156 | { 163 | // if (images[index]?.color) { 164 | // setColor(images[index].color) 165 | // } 166 | // }} 167 | images={allImages?.length > 0 ? allImages : [{ 168 | src: `https://via.placeholder.com/1050x1050`, 169 | }]} 170 | > 171 |
172 |
173 |
174 | 175 | {title} 176 | 177 | {getPrice(variant ? variant?.price : product.price, product.currency ?? 'USD')} 178 | 179 | 180 |
181 |
182 | {productOptions?.length > 0 && productOptions?.map((option) => { 183 | return ( 184 | 185 | {Boolean(option.values?.length) && ( 186 | {inputChangeHandler(option, event.target.value)}} 194 | /> 195 | )} 196 | 197 | ) 198 | })} 199 |
200 | 208 |
209 | 210 | 211 | ) 212 | } 213 | 214 | const ProductView: React.FC<{ 215 | product: string | any 216 | renderSeo?: boolean 217 | description?: string 218 | title?: string 219 | }> = ({ product, ...props }) => { 220 | return ( 221 | 222 | {(productObject) => } 223 | 224 | ) 225 | } 226 | export default ProductView 227 | -------------------------------------------------------------------------------- /blocks/ProductView/ProductViewDemo.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxRuntime classic */ 2 | /** @jsx jsx */ 3 | import React, { useState } from 'react' 4 | import { Themed, jsx } from 'theme-ui' 5 | import { Grid, Button } from '@theme-ui/components' 6 | import OptionPicker from '@components/common/OptionPicker' 7 | import { NextSeo } from 'next-seo' 8 | import { getPrice } from '@lib/swell/storefront-data-hooks/src/utils/product' 9 | import ProductLoader from './ProductLoader' 10 | import { ImageCarousel } from '@components/ui' 11 | 12 | interface Props { 13 | className?: string 14 | children?: any 15 | product: any & Record 16 | renderSeo?: boolean 17 | description?: string 18 | title?: string 19 | } 20 | 21 | const ProductBox: React.FC = ({ 22 | product, 23 | renderSeo = true, 24 | description = product.body_html, 25 | title = product.title, 26 | }) => { 27 | const variants = product.variants as any[] 28 | const images = product.images 29 | const variant = variants.find((v) => v.available) || variants[0] 30 | const price = getPrice(variant.compare_at_price || variant.price, 'USD') 31 | const [image, setImage] = useState( 32 | variant.featured_image || product.images[0] 33 | ) 34 | 35 | return ( 36 | 37 | {renderSeo && ( 38 | 55 | )} 56 | 57 |
64 | 72 |
73 | 74 |
75 | 76 | {title} 77 | 78 | {price} 79 | 80 | 81 |
82 |
83 | 84 | {product.options.map((opt: any) => { 85 | return ( 86 | 91 | ) 92 | })} 93 | 94 |
95 | 102 |
103 | 104 | 105 | ) 106 | } 107 | const ProductView: React.FC<{ 108 | product: string | any 109 | renderSeo?: boolean 110 | description?: string 111 | title?: string 112 | }> = ({ product, ...props }) => { 113 | return ( 114 | 115 | {(productObject) => } 116 | 117 | ) 118 | } 119 | export default ProductView 120 | -------------------------------------------------------------------------------- /blocks/utils.ts: -------------------------------------------------------------------------------- 1 | import { Builder, Component, builder } from '@builder.io/sdk' 2 | 3 | export function restrictedRegister( 4 | component: any, 5 | options: Component, 6 | models: string[] 7 | ) { 8 | if (!Builder.isEditing || models.includes(builder.editingModel!)) { 9 | return Builder.registerComponent(component, options) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /builder/announcement-bar/announcement-bar.json: -------------------------------------------------------------------------------- 1 | { 2 | "createdBy": "4FFFg0MNRJT0z0nW4uUizDHfHJV2", 3 | "createdDate": 1631307633579, 4 | "data": { 5 | "inputs": [], 6 | "blocks": [ 7 | { 8 | "@type": "@builder.io/sdk:Element", 9 | "@version": 2, 10 | "bindings": { 11 | "style.backgroundColor": "var _virtual_index=state.theme.rawColors.secondary;return _virtual_index", 12 | "style.color": "var _virtual_index=state.theme.rawColors.background;return _virtual_index" 13 | }, 14 | "code": { 15 | "bindings": { 16 | "style.backgroundColor": "state.theme.rawColors.secondary", 17 | "style.color": "state.theme.rawColors.background" 18 | } 19 | }, 20 | "id": "builder-797d6a380a3b4d4c8a2f2f46f63719ba", 21 | "children": [ 22 | { 23 | "@type": "@builder.io/sdk:Element", 24 | "@version": 2, 25 | "id": "builder-7de38feeda7a4773a663b7b8efbcc90e", 26 | "component": { 27 | "name": "Text", 28 | "options": { 29 | "text": "

Free shipping on all orders above $50 until the end of the month

" 30 | } 31 | }, 32 | "responsiveStyles": { 33 | "large": { 34 | "display": "flex", 35 | "flexDirection": "column", 36 | "position": "relative", 37 | "flexShrink": "0", 38 | "boxSizing": "border-box", 39 | "marginTop": "auto", 40 | "lineHeight": "normal", 41 | "height": "auto", 42 | "textAlign": "center", 43 | "marginLeft": "auto", 44 | "marginRight": "auto", 45 | "marginBottom": "auto" 46 | } 47 | } 48 | } 49 | ], 50 | "responsiveStyles": { 51 | "large": { 52 | "display": "flex", 53 | "flexDirection": "column", 54 | "position": "relative", 55 | "flexShrink": "0", 56 | "boxSizing": "border-box", 57 | "height": "40px" 58 | } 59 | } 60 | } 61 | ], 62 | "state": { 63 | "deviceSize": "large", 64 | "location": { 65 | "path": "", 66 | "query": {} 67 | } 68 | } 69 | }, 70 | "id": "83fa2ebddbb242649499db90d7786fb5", 71 | "lastUpdatedBy": "4FFFg0MNRJT0z0nW4uUizDHfHJV2", 72 | "meta": { 73 | "hasLinks": false, 74 | "kind": "component", 75 | "needsHydration": false 76 | }, 77 | "modelId": "807497368ce84a74a5382b255b50fc0f", 78 | "name": "announcement bar", 79 | "published": "published", 80 | "query": [], 81 | "testRatio": 1, 82 | "variations": {}, 83 | "lastUpdated": 1631307988830, 84 | "screenshot": "https://cdn.builder.io/api/v1/image/assets%2F4d832b53c3cf422f9a6c92a23abf35ac%2Fcdc575fac3f4459e847edf4d130b7240", 85 | "rev": "9stmz1sjji6", 86 | "@originOrg": "4d832b53c3cf422f9a6c92a23abf35ac", 87 | "@originContentId": "22b07daaafe2483b880096a6b2fac937", 88 | "@originModelId": "0997d1c844de5489f349df50288f8e98e551089ef0eba4a70f43562ee9f78153" 89 | } 90 | -------------------------------------------------------------------------------- /builder/announcement-bar/schema.model.json: -------------------------------------------------------------------------------- 1 | { 2 | "hideOptions": false, 3 | "helperText": "", 4 | "isPage": false, 5 | "publicWritable": false, 6 | "showTargeting": true, 7 | "bigData": false, 8 | "autoTracked": true, 9 | "hideFromUI": false, 10 | "name": "announcement-bar", 11 | "allowBuiltInComponents": true, 12 | "pathPrefix": "/", 13 | "subType": "", 14 | "allowMetrics": true, 15 | "allowTests": true, 16 | "hooks": {}, 17 | "@originId": "0997d1c844de5489f349df50288f8e98e551089ef0eba4a70f43562ee9f78153", 18 | "fields": [ 19 | { 20 | "helperText": "", 21 | "noPhotoPicker": false, 22 | "required": true, 23 | "name": "blocks", 24 | "copyOnAdd": true, 25 | "simpleTextOnly": false, 26 | "hideFromFieldsEditor": true, 27 | "autoFocus": false, 28 | "mandatory": false, 29 | "model": "", 30 | "advanced": false, 31 | "onChange": "", 32 | "showTemplatePicker": true, 33 | "hideFromUI": false, 34 | "disallowRemove": false, 35 | "hidden": false, 36 | "@type": "@builder.io/core:Field", 37 | "type": "uiBlocks", 38 | "showIf": "", 39 | "subFields": [], 40 | "permissionsRequiredToEdit": "" 41 | } 42 | ], 43 | "showScheduling": true, 44 | "webhooks": [], 45 | "injectWcAt": "", 46 | "injectWcPosition": "", 47 | "publicReadable": true, 48 | "useQueryParamTargetingClientSide": false, 49 | "lastUpdateBy": null, 50 | "schema": {}, 51 | "archived": false, 52 | "createdDate": 1631307264405, 53 | "strictPrivateWrite": false, 54 | "examplePageUrl": "", 55 | "strictPrivateRead": false, 56 | "allowHeatmap": true, 57 | "showAbTests": true, 58 | "repeatable": false, 59 | "individualEmbed": false, 60 | "designerVersion": 1, 61 | "showMetrics": true, 62 | "defaultQuery": [], 63 | "singleton": false, 64 | "apiGenerated": true, 65 | "sendToMongoDb": true, 66 | "kind": "component", 67 | "getSchemaFromPage": false, 68 | "id": "807497368ce84a74a5382b255b50fc0f", 69 | "componentsOnlyMode": false, 70 | "sendToElasticSearch": false, 71 | "hidden": false, 72 | "requiredTargets": [] 73 | } 74 | -------------------------------------------------------------------------------- /builder/cart-upsell-sidebar/schema.model.json: -------------------------------------------------------------------------------- 1 | { 2 | "designerVersion": 1, 3 | "id": "081cd8ec88d84fa2bfc39bac56c9144c", 4 | "allowMetrics": true, 5 | "helperText": "", 6 | "name": "cart-upsell-sidebar", 7 | "strictPrivateRead": false, 8 | "allowBuiltInComponents": true, 9 | "allowHeatmap": true, 10 | "archived": false, 11 | "webhooks": [], 12 | "componentsOnlyMode": false, 13 | "showAbTests": true, 14 | "autoTracked": true, 15 | "createdDate": 1631307261211, 16 | "sendToMongoDb": true, 17 | "showTargeting": true, 18 | "defaultQuery": [], 19 | "injectWcAt": "", 20 | "strictPrivateWrite": false, 21 | "useQueryParamTargetingClientSide": false, 22 | "injectWcPosition": "", 23 | "publicReadable": true, 24 | "apiGenerated": true, 25 | "requiredTargets": [], 26 | "bigData": false, 27 | "showMetrics": true, 28 | "lastUpdateBy": null, 29 | "pathPrefix": "/", 30 | "examplePageUrl": "", 31 | "allowTests": true, 32 | "getSchemaFromPage": false, 33 | "hideOptions": false, 34 | "hooks": {}, 35 | "individualEmbed": false, 36 | "hideFromUI": false, 37 | "publicWritable": false, 38 | "kind": "component", 39 | "repeatable": false, 40 | "showScheduling": true, 41 | "@originId": "953b09d275bbb145858565ee9a39294d35903d1b556451d6776655956d6df47c", 42 | "hidden": false, 43 | "isPage": false, 44 | "sendToElasticSearch": false, 45 | "schema": {}, 46 | "subType": "", 47 | "fields": [ 48 | { 49 | "noPhotoPicker": false, 50 | "showIf": "", 51 | "required": true, 52 | "@type": "@builder.io/core:Field", 53 | "mandatory": false, 54 | "onChange": "", 55 | "permissionsRequiredToEdit": "", 56 | "simpleTextOnly": false, 57 | "disallowRemove": false, 58 | "subFields": [], 59 | "showTemplatePicker": true, 60 | "hideFromUI": false, 61 | "copyOnAdd": true, 62 | "model": "", 63 | "advanced": false, 64 | "hidden": false, 65 | "type": "uiBlocks", 66 | "helperText": "", 67 | "hideFromFieldsEditor": true, 68 | "autoFocus": false, 69 | "name": "blocks" 70 | } 71 | ], 72 | "singleton": false 73 | } 74 | -------------------------------------------------------------------------------- /builder/collection-page/generic-collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "@originContentId": "172a83bd533848d52db8ff1d087a2a2f223d246df3d4f37a82d7defb034e066a", 3 | "@originModelId": "2d8db355355b2b2fa611387eaeb02e39ed9c64483e31354853c6376f69aa49b0", 4 | "@originOrg": "4d832b53c3cf422f9a6c92a23abf35ac", 5 | "createdBy": "agZ9n5CUKRfbL9t6CaJOyVSK4Es2", 6 | "createdDate": 1611769584081, 7 | "data": { 8 | "state": { 9 | "deviceSize": "large", 10 | "location": { 11 | "path": "", 12 | "query": {} 13 | } 14 | }, 15 | "blocks": [ 16 | { 17 | "@type": "@builder.io/sdk:Element", 18 | "@version": 2, 19 | "bindings": { 20 | "component.options.collection": "state.collection", 21 | "component.options.renderSeo": "true" 22 | }, 23 | "id": "builder-02434c3799094ba2a50eb11212e7e54c", 24 | "component": { 25 | "name": "CollectionView", 26 | "options": { 27 | "productGridOptions": { 28 | "cardProps": { 29 | "variant": "simple", 30 | "imgPriority": true, 31 | "imgLayout": "responsive", 32 | "imgLoading": "eager", 33 | "imgWidth": 440, 34 | "imgHeight": 440, 35 | "layout": "fixed" 36 | }, 37 | "gridProps": { 38 | "variant": "filled", 39 | "layout": "normal" 40 | }, 41 | "limit": 9, 42 | "highlightCard": { 43 | "index": null, 44 | "variant": "simple", 45 | "imgPriority": true 46 | }, 47 | "offset": 0 48 | } 49 | } 50 | }, 51 | "responsiveStyles": { 52 | "large": { 53 | "display": "flex", 54 | "flexDirection": "column", 55 | "position": "relative", 56 | "flexShrink": "0", 57 | "boxSizing": "border-box", 58 | "marginTop": "20px" 59 | } 60 | } 61 | } 62 | ] 63 | }, 64 | "id": "15594ec857a24558b292e79f8bbec059", 65 | "lastUpdated": 1612948484060, 66 | "lastUpdatedBy": "agZ9n5CUKRfbL9t6CaJOyVSK4Es2", 67 | "meta": { 68 | "hasLinks": false, 69 | "kind": "component", 70 | "needsHydration": false 71 | }, 72 | "modelId": "706f88a2775b42de9665ea421fc74956", 73 | "name": "generic collection", 74 | "published": "published", 75 | "query": [], 76 | "rev": "r6uptckae2g", 77 | "screenshot": "https://cdn.builder.io/api/v1/image/assets%2F9aa8825c0d33424ca3e9076c2cc4a328%2F9c84aa35d8c54162ac7d2aa0a1708f53", 78 | "testRatio": 1, 79 | "variations": {} 80 | } 81 | -------------------------------------------------------------------------------- /builder/collection-page/schema.model.json: -------------------------------------------------------------------------------- 1 | { 2 | "getSchemaFromPage": false, 3 | "individualEmbed": false, 4 | "pathPrefix": "/", 5 | "publicReadable": true, 6 | "allowBuiltInComponents": true, 7 | "showScheduling": true, 8 | "showAbTests": true, 9 | "schema": {}, 10 | "hidden": false, 11 | "name": "collection-page", 12 | "repeatable": false, 13 | "archived": false, 14 | "examplePageUrl": "${space.siteUrl}/collection/${previewResource.handle}", 15 | "fields": [ 16 | { 17 | "simpleTextOnly": false, 18 | "mandatory": false, 19 | "noPhotoPicker": false, 20 | "permissionsRequiredToEdit": "", 21 | "hideFromFieldsEditor": true, 22 | "showTemplatePicker": true, 23 | "subFields": [], 24 | "bubble": false, 25 | "required": true, 26 | "hideFromUI": false, 27 | "advanced": false, 28 | "type": "uiBlocks", 29 | "model": "", 30 | "copyOnAdd": true, 31 | "onChange": "", 32 | "autoFocus": false, 33 | "showIf": "", 34 | "@type": "@builder.io/core:Field", 35 | "helperText": "", 36 | "hidden": false, 37 | "disallowRemove": false, 38 | "name": "blocks", 39 | "broadcast": false 40 | }, 41 | { 42 | "hideFromFieldsEditor": false, 43 | "model": "", 44 | "@type": "@builder.io/core:Field", 45 | "type": "SwellCategoryPreview", 46 | "showTemplatePicker": true, 47 | "mandatory": false, 48 | "hidden": false, 49 | "copyOnAdd": true, 50 | "onChange": "", 51 | "name": "categoryPreview", 52 | "bubble": false, 53 | "hideFromUI": false, 54 | "helperText": "Switch to view template on different categories", 55 | "subFields": [], 56 | "advanced": false, 57 | "disallowRemove": false, 58 | "permissionsRequiredToEdit": "", 59 | "autoFocus": false, 60 | "simpleTextOnly": false, 61 | "showIf": "", 62 | "broadcast": false, 63 | "required": false, 64 | "noPhotoPicker": false 65 | } 66 | ], 67 | "helperText": "", 68 | "sendToElasticSearch": false, 69 | "isPage": false, 70 | "allowHeatmap": true, 71 | "id": "706f88a2775b42de9665ea421fc74956", 72 | "singleton": false, 73 | "componentsOnlyMode": false, 74 | "allowTests": true, 75 | "lastUpdateBy": null, 76 | "showTargeting": true, 77 | "requiredTargets": [], 78 | "injectWcPosition": "", 79 | "strictPrivateRead": false, 80 | "allowMetrics": true, 81 | "hooks": {}, 82 | "showMetrics": true, 83 | "subType": "", 84 | "bigData": false, 85 | "injectWcAt": "", 86 | "defaultQuery": [], 87 | "hideFromUI": false, 88 | "publicWritable": false, 89 | "useQueryParamTargetingClientSide": false, 90 | "autoTracked": true, 91 | "kind": "component", 92 | "hideOptions": false, 93 | "strictPrivateWrite": false, 94 | "@originId": "2d8db355355b2b2fa611387eaeb02e39ed9c64483e31354853c6376f69aa49b0" 95 | } 96 | -------------------------------------------------------------------------------- /builder/page/schema.model.json: -------------------------------------------------------------------------------- 1 | { 2 | "showAbTests": true, 3 | "individualEmbed": false, 4 | "useQueryParamTargetingClientSide": false, 5 | "hideOptions": false, 6 | "hideFromUI": false, 7 | "strictPrivateWrite": false, 8 | "kind": "page", 9 | "@originId": "50b47b9bbb4a7e3f4e511d380916bd11ccfb542bdd4662cb5eb87f1cf92c8d3a", 10 | "autoTracked": true, 11 | "publicReadable": true, 12 | "id": "98351febac3a4ec2a6900cd0eaaad0b9", 13 | "defaultQuery": [], 14 | "webhooks": [], 15 | "helperText": "", 16 | "subType": "", 17 | "showMetrics": true, 18 | "isPage": false, 19 | "strictPrivateRead": false, 20 | "allowTests": true, 21 | "hidden": false, 22 | "injectWcAt": "", 23 | "publicWritable": false, 24 | "componentsOnlyMode": false, 25 | "showScheduling": true, 26 | "apiGenerated": true, 27 | "injectWcPosition": "", 28 | "showTargeting": true, 29 | "bigData": false, 30 | "name": "page", 31 | "requiredTargets": [], 32 | "lastUpdateBy": null, 33 | "designerVersion": 1, 34 | "sendToElasticSearch": false, 35 | "schema": {}, 36 | "sendToMongoDb": true, 37 | "singleton": false, 38 | "fields": [ 39 | { 40 | "hidden": true, 41 | "required": false, 42 | "@type": "@builder.io/core:Field", 43 | "permissionsRequiredToEdit": "", 44 | "copyOnAdd": true, 45 | "disallowRemove": false, 46 | "noPhotoPicker": false, 47 | "mandatory": false, 48 | "model": "", 49 | "hideFromUI": false, 50 | "showTemplatePicker": true, 51 | "type": "uiBlocks", 52 | "showIf": "", 53 | "advanced": false, 54 | "subFields": [], 55 | "onChange": "", 56 | "autoFocus": false, 57 | "hideFromFieldsEditor": false, 58 | "simpleTextOnly": false, 59 | "name": "blocks", 60 | "helperText": "" 61 | }, 62 | { 63 | "noPhotoPicker": false, 64 | "simpleTextOnly": false, 65 | "onChange": "", 66 | "showIf": "", 67 | "helperText": "SEO page title", 68 | "copyOnAdd": true, 69 | "name": "title", 70 | "@type": "@builder.io/core:Field", 71 | "required": false, 72 | "type": "text", 73 | "hidden": false, 74 | "model": "", 75 | "hideFromUI": false, 76 | "permissionsRequiredToEdit": "", 77 | "autoFocus": false, 78 | "mandatory": false, 79 | "showTemplatePicker": true, 80 | "hideFromFieldsEditor": false, 81 | "advanced": false, 82 | "subFields": [], 83 | "disallowRemove": false 84 | }, 85 | { 86 | "mandatory": false, 87 | "autoFocus": false, 88 | "hideFromUI": false, 89 | "helperText": "SEO page description", 90 | "disallowRemove": false, 91 | "@type": "@builder.io/core:Field", 92 | "required": false, 93 | "advanced": false, 94 | "copyOnAdd": true, 95 | "showTemplatePicker": true, 96 | "permissionsRequiredToEdit": "", 97 | "hideFromFieldsEditor": false, 98 | "model": "", 99 | "hidden": false, 100 | "name": "description", 101 | "subFields": [], 102 | "type": "longText", 103 | "showIf": "", 104 | "noPhotoPicker": false, 105 | "simpleTextOnly": false, 106 | "onChange": "" 107 | } 108 | ], 109 | "getSchemaFromPage": false, 110 | "allowBuiltInComponents": true, 111 | "hooks": {}, 112 | "repeatable": false, 113 | "allowHeatmap": true, 114 | "createdDate": 1631307260921, 115 | "archived": false, 116 | "allowMetrics": true 117 | } 118 | -------------------------------------------------------------------------------- /builder/product-page/generic-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "@originContentId": "ac5fd99dfef77e38cee5b713cd77fd0676f39168ac84582dc6d4a789ce857e38", 3 | "@originModelId": "15670ab9d951f7385dab0f6313f22ea884553d0d8ebbe3a39c60afc225df2d3d", 4 | "@originOrg": "4d832b53c3cf422f9a6c92a23abf35ac", 5 | "createdBy": "agZ9n5CUKRfbL9t6CaJOyVSK4Es2", 6 | "createdDate": 1610516058604, 7 | "data": { 8 | "inputs": [], 9 | "state": { 10 | "deviceSize": "large", 11 | "location": { 12 | "path": "", 13 | "query": {} 14 | } 15 | }, 16 | "blocks": [ 17 | { 18 | "@type": "@builder.io/sdk:Element", 19 | "@version": 2, 20 | "bindings": { 21 | "component.options.product": "state.product", 22 | "component.options.title": "state.product.title", 23 | "component.options.description": "state.product.descriptionHtml" 24 | }, 25 | "id": "builder-0651a7f9080a46b2a5cf92ab47265e54", 26 | "component": { 27 | "name": "ProductView", 28 | "options": {} 29 | }, 30 | "responsiveStyles": { 31 | "large": { 32 | "display": "flex", 33 | "flexDirection": "column", 34 | "position": "relative", 35 | "flexShrink": "0", 36 | "boxSizing": "border-box", 37 | "marginTop": "20px" 38 | } 39 | } 40 | } 41 | ] 42 | }, 43 | "id": "05e6afc6324347b49c0f7c80dae94360", 44 | "lastUpdated": 1611873566299, 45 | "lastUpdatedBy": "agZ9n5CUKRfbL9t6CaJOyVSK4Es2", 46 | "meta": { 47 | "hasLinks": false, 48 | "kind": "component", 49 | "needsHydration": false 50 | }, 51 | "modelId": "5d7832a6821e439280138b242dfee5ad", 52 | "name": "Generic template", 53 | "published": "published", 54 | "query": [], 55 | "rev": "7y1qohp2k5i", 56 | "screenshot": "https://cdn.builder.io/api/v1/image/assets%2F6e5b10afb1a9457ba648112d4c5d30e8%2F52a6831152264f29a2b28166304a30d5", 57 | "testRatio": 1, 58 | "variations": {} 59 | } 60 | -------------------------------------------------------------------------------- /builder/product-page/schema.model.json: -------------------------------------------------------------------------------- 1 | { 2 | "webhooks": [], 3 | "allowTests": true, 4 | "showMetrics": true, 5 | "hideFromUI": false, 6 | "defaultQuery": [], 7 | "helperText": "", 8 | "allowMetrics": true, 9 | "publicReadable": true, 10 | "examplePageUrl": "${space.siteUrl}/product/${previewResource.handle}", 11 | "repeatable": false, 12 | "allowHeatmap": true, 13 | "hooks": {}, 14 | "bigData": false, 15 | "showAbTests": true, 16 | "allowBuiltInComponents": true, 17 | "lastUpdateBy": null, 18 | "useQueryParamTargetingClientSide": false, 19 | "schema": {}, 20 | "injectWcPosition": "", 21 | "injectWcAt": "", 22 | "id": "5d7832a6821e439280138b242dfee5ad", 23 | "showTargeting": true, 24 | "strictPrivateRead": false, 25 | "hideOptions": false, 26 | "designerVersion": 1, 27 | "componentsOnlyMode": false, 28 | "sendToMongoDb": true, 29 | "fields": [ 30 | { 31 | "permissionsRequiredToEdit": "", 32 | "advanced": false, 33 | "hidden": false, 34 | "required": true, 35 | "broadcast": false, 36 | "helperText": "", 37 | "hideFromUI": false, 38 | "noPhotoPicker": false, 39 | "simpleTextOnly": false, 40 | "showIf": "", 41 | "hideFromFieldsEditor": true, 42 | "mandatory": false, 43 | "subFields": [], 44 | "name": "blocks", 45 | "copyOnAdd": true, 46 | "bubble": false, 47 | "onChange": "", 48 | "type": "uiBlocks", 49 | "showTemplatePicker": true, 50 | "@type": "@builder.io/core:Field", 51 | "autoFocus": false, 52 | "disallowRemove": false, 53 | "model": "" 54 | }, 55 | { 56 | "showTemplatePicker": true, 57 | "broadcast": false, 58 | "subFields": [], 59 | "mandatory": false, 60 | "@type": "@builder.io/core:Field", 61 | "onChange": "", 62 | "model": "", 63 | "hideFromFieldsEditor": false, 64 | "helperText": "Which product to preview your template on.", 65 | "showIf": "", 66 | "permissionsRequiredToEdit": "", 67 | "advanced": false, 68 | "hidden": false, 69 | "disallowRemove": false, 70 | "copyOnAdd": true, 71 | "hideFromUI": false, 72 | "noPhotoPicker": false, 73 | "name": "preview", 74 | "required": false, 75 | "type": "SwellProductPreview", 76 | "autoFocus": false, 77 | "simpleTextOnly": false, 78 | "bubble": false 79 | } 80 | ], 81 | "singleton": false, 82 | "hidden": false, 83 | "strictPrivateWrite": false, 84 | "kind": "component", 85 | "isPage": false, 86 | "subType": "", 87 | "getSchemaFromPage": false, 88 | "archived": false, 89 | "individualEmbed": false, 90 | "showScheduling": true, 91 | "autoTracked": true, 92 | "sendToElasticSearch": false, 93 | "name": "product-page", 94 | "pathPrefix": "/", 95 | "publicWritable": false, 96 | "requiredTargets": [], 97 | "@originId": "15670ab9d951f7385dab0f6313f22ea884553d0d8ebbe3a39c60afc225df2d3d" 98 | } 99 | -------------------------------------------------------------------------------- /builder/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "space", 3 | "hasIntegrated": "local", 4 | "settings": { 5 | "optimizeContentVisibility": true, 6 | "reloadPreviewForMobile": false 7 | }, 8 | "siteUrl": "http://localhost:3000", 9 | "customTargetingAttributes": { 10 | "collectionHandle": { 11 | "type": "SwellCategoryHandle" 12 | }, 13 | "itemInCart": { 14 | "type": "SwellProductHandle" 15 | }, 16 | "productHandle": { 17 | "type": "SwellProductHandle" 18 | } 19 | }, 20 | "loadPlugins": ["@builder.io/plugin-swell"], 21 | "@version": 5, 22 | "cloneInfo": { 23 | "contentIdMap": { 24 | "ac5fd99dfef77e38cee5b713cd77fd0676f39168ac84582dc6d4a789ce857e38": "05e6afc6324347b49c0f7c80dae94360", 25 | "a312ae57f3de886b360cafc1986714f81e8ed93bc6d0a09f717b9f91dae23f79": "c77df46d5ce0441c86ca4bafd757d88d", 26 | "036efa873df3ced70c26f218e2234f53b7611826a1c89915e5671cb3c7f3c1a5": "d221fc78c13a4b0ca96869f9ef096d1b", 27 | "6e26c3a66619aa945c31fbde0c5ad35e52b0ecface55a3f5cbdc79952326be96": "e644ce7d6bf949289f1e3d12b21b0ea7", 28 | "172a83bd533848d52db8ff1d087a2a2f223d246df3d4f37a82d7defb034e066a": "15594ec857a24558b292e79f8bbec059", 29 | "22b07daaafe2483b880096a6b2fac937": "83fa2ebddbb242649499db90d7786fb5" 30 | }, 31 | "modelIdMap": { 32 | "15670ab9d951f7385dab0f6313f22ea884553d0d8ebbe3a39c60afc225df2d3d": "5d7832a6821e439280138b242dfee5ad", 33 | "93a28cddbe257c8c956bbdb79408188bfab7220ad04e7c505e572d47a6b0406d": "c97db5356e4b440d94423eb6c6d8173d", 34 | "50b47b9bbb4a7e3f4e511d380916bd11ccfb542bdd4662cb5eb87f1cf92c8d3a": "98351febac3a4ec2a6900cd0eaaad0b9", 35 | "2d8db355355b2b2fa611387eaeb02e39ed9c64483e31354853c6376f69aa49b0": "706f88a2775b42de9665ea421fc74956", 36 | "953b09d275bbb145858565ee9a39294d35903d1b556451d6776655956d6df47c": "081cd8ec88d84fa2bfc39bac56c9144c", 37 | "0997d1c844de5489f349df50288f8e98e551089ef0eba4a70f43562ee9f78153": "807497368ce84a74a5382b255b50fc0f" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /builder/theme/schema.model.json: -------------------------------------------------------------------------------- 1 | { 2 | "publicReadable": true, 3 | "allowBuiltInComponents": true, 4 | "autoTracked": true, 5 | "singleton": false, 6 | "fields": [ 7 | { 8 | "hideFromFieldsEditor": false, 9 | "bubble": false, 10 | "hidden": false, 11 | "simpleTextOnly": false, 12 | "noPhotoPicker": false, 13 | "subFields": [], 14 | "permissionsRequiredToEdit": "", 15 | "model": "", 16 | "onChange": "", 17 | "type": "text", 18 | "hideFromUI": false, 19 | "required": false, 20 | "showIf": "", 21 | "enum": [ 22 | "base", 23 | "tailwind", 24 | "swiss", 25 | "future", 26 | "deep", 27 | "system", 28 | "funk", 29 | "sketchy", 30 | "roboto", 31 | "tosh", 32 | "bulma", 33 | "polaris" 34 | ], 35 | "helperText": "", 36 | "@type": "@builder.io/core:Field", 37 | "advanced": false, 38 | "showTemplatePicker": true, 39 | "mandatory": false, 40 | "disallowRemove": false, 41 | "broadcast": false, 42 | "name": "theme", 43 | "copyOnAdd": true, 44 | "autoFocus": false 45 | }, 46 | { 47 | "type": "object", 48 | "showIf": "", 49 | "bubble": false, 50 | "hideFromFieldsEditor": false, 51 | "hidden": false, 52 | "copyOnAdd": true, 53 | "broadcast": false, 54 | "defaultValue": {}, 55 | "@type": "@builder.io/core:Field", 56 | "helperText": "", 57 | "onChange": "", 58 | "model": "", 59 | "subFields": [ 60 | { 61 | "showTemplatePicker": true, 62 | "hideFromUI": false, 63 | "advanced": false, 64 | "broadcast": false, 65 | "@type": "@builder.io/core:Field", 66 | "disallowRemove": false, 67 | "hideFromFieldsEditor": false, 68 | "bubble": false, 69 | "helperText": "", 70 | "hidden": false, 71 | "required": false, 72 | "showIf": "", 73 | "permissionsRequiredToEdit": "", 74 | "model": "", 75 | "noPhotoPicker": false, 76 | "name": "navigationLinks", 77 | "type": "list", 78 | "onChange": "", 79 | "mandatory": false, 80 | "copyOnAdd": true, 81 | "simpleTextOnly": false, 82 | "subFields": [ 83 | { 84 | "hideFromFieldsEditor": false, 85 | "advanced": false, 86 | "noPhotoPicker": false, 87 | "onChange": "", 88 | "permissionsRequiredToEdit": "", 89 | "subFields": [], 90 | "showTemplatePicker": true, 91 | "required": true, 92 | "autoFocus": false, 93 | "@type": "@builder.io/core:Field", 94 | "simpleTextOnly": false, 95 | "copyOnAdd": true, 96 | "disallowRemove": false, 97 | "helperText": "", 98 | "model": "", 99 | "bubble": false, 100 | "hideFromUI": false, 101 | "type": "url", 102 | "broadcast": false, 103 | "mandatory": false, 104 | "hidden": false, 105 | "name": "link", 106 | "showIf": "" 107 | }, 108 | { 109 | "type": "text", 110 | "autoFocus": false, 111 | "hideFromUI": false, 112 | "simpleTextOnly": false, 113 | "hidden": false, 114 | "model": "", 115 | "copyOnAdd": true, 116 | "showIf": "", 117 | "mandatory": false, 118 | "broadcast": false, 119 | "advanced": false, 120 | "noPhotoPicker": false, 121 | "bubble": false, 122 | "permissionsRequiredToEdit": "", 123 | "@type": "@builder.io/core:Field", 124 | "disallowRemove": false, 125 | "hideFromFieldsEditor": false, 126 | "name": "title", 127 | "showTemplatePicker": true, 128 | "subFields": [], 129 | "required": true, 130 | "onChange": "", 131 | "helperText": "" 132 | } 133 | ], 134 | "autoFocus": false 135 | }, 136 | { 137 | "hideFromUI": false, 138 | "required": false, 139 | "simpleTextOnly": false, 140 | "autoFocus": false, 141 | "hidden": false, 142 | "permissionsRequiredToEdit": "", 143 | "broadcast": false, 144 | "name": "logo", 145 | "disallowRemove": false, 146 | "helperText": "", 147 | "noPhotoPicker": false, 148 | "copyOnAdd": true, 149 | "showIf": "", 150 | "model": "", 151 | "hideFromFieldsEditor": false, 152 | "@type": "@builder.io/core:Field", 153 | "showTemplatePicker": true, 154 | "defaultValue": {}, 155 | "onChange": "", 156 | "advanced": false, 157 | "subFields": [ 158 | { 159 | "helperText": "optional, providing a logo image will override this.", 160 | "required": false, 161 | "hideFromUI": false, 162 | "autoFocus": false, 163 | "mandatory": false, 164 | "copyOnAdd": true, 165 | "noPhotoPicker": false, 166 | "broadcast": false, 167 | "hideFromFieldsEditor": false, 168 | "disallowRemove": false, 169 | "onChange": "", 170 | "@type": "@builder.io/core:Field", 171 | "type": "text", 172 | "bubble": false, 173 | "model": "", 174 | "simpleTextOnly": false, 175 | "subFields": [], 176 | "permissionsRequiredToEdit": "", 177 | "name": "text", 178 | "hidden": false, 179 | "showIf": "", 180 | "showTemplatePicker": true, 181 | "advanced": false 182 | }, 183 | { 184 | "noPhotoPicker": false, 185 | "showIf": "", 186 | "permissionsRequiredToEdit": "", 187 | "broadcast": false, 188 | "showTemplatePicker": true, 189 | "autoFocus": false, 190 | "onChange": "", 191 | "helperText": "", 192 | "@type": "@builder.io/core:Field", 193 | "hideFromUI": false, 194 | "required": false, 195 | "name": "image", 196 | "copyOnAdd": true, 197 | "bubble": false, 198 | "hideFromFieldsEditor": false, 199 | "mandatory": false, 200 | "subFields": [], 201 | "type": "file", 202 | "simpleTextOnly": false, 203 | "allowedFileTypes": ["jpeg", "png"], 204 | "advanced": false, 205 | "hidden": false, 206 | "disallowRemove": false, 207 | "model": "" 208 | }, 209 | { 210 | "hideFromUI": false, 211 | "bubble": false, 212 | "advanced": false, 213 | "type": "number", 214 | "hidden": false, 215 | "hideFromFieldsEditor": false, 216 | "defaultValue": 190, 217 | "onChange": "", 218 | "disallowRemove": false, 219 | "copyOnAdd": true, 220 | "model": "", 221 | "@type": "@builder.io/core:Field", 222 | "noPhotoPicker": false, 223 | "broadcast": false, 224 | "name": "width", 225 | "showTemplatePicker": true, 226 | "helperText": "Required when passing an image", 227 | "mandatory": false, 228 | "showIf": "", 229 | "permissionsRequiredToEdit": "", 230 | "autoFocus": false, 231 | "simpleTextOnly": false, 232 | "subFields": [], 233 | "required": false 234 | }, 235 | { 236 | "showIf": "", 237 | "name": "height", 238 | "defaultValue": 60, 239 | "showTemplatePicker": true, 240 | "hideFromUI": false, 241 | "hideFromFieldsEditor": false, 242 | "hidden": false, 243 | "model": "", 244 | "autoFocus": false, 245 | "bubble": false, 246 | "required": false, 247 | "broadcast": false, 248 | "type": "number", 249 | "noPhotoPicker": false, 250 | "copyOnAdd": true, 251 | "disallowRemove": false, 252 | "helperText": "required when passing an image", 253 | "mandatory": false, 254 | "subFields": [], 255 | "onChange": "", 256 | "simpleTextOnly": false, 257 | "@type": "@builder.io/core:Field", 258 | "advanced": false, 259 | "permissionsRequiredToEdit": "" 260 | } 261 | ], 262 | "mandatory": false, 263 | "bubble": false, 264 | "type": "object" 265 | } 266 | ], 267 | "noPhotoPicker": false, 268 | "name": "siteSettings", 269 | "simpleTextOnly": false, 270 | "advanced": false, 271 | "required": false, 272 | "disallowRemove": false, 273 | "autoFocus": false, 274 | "mandatory": false, 275 | "hideFromUI": false, 276 | "permissionsRequiredToEdit": "", 277 | "showTemplatePicker": true 278 | }, 279 | { 280 | "onChange": "", 281 | "copyOnAdd": true, 282 | "disallowRemove": false, 283 | "type": "object", 284 | "permissionsRequiredToEdit": "", 285 | "showIf": "", 286 | "noPhotoPicker": false, 287 | "model": "", 288 | "autoFocus": false, 289 | "mandatory": false, 290 | "broadcast": false, 291 | "bubble": false, 292 | "hidden": false, 293 | "simpleTextOnly": false, 294 | "name": "colorOverrides", 295 | "defaultValue": {}, 296 | "@type": "@builder.io/core:Field", 297 | "hideFromUI": false, 298 | "advanced": false, 299 | "hideFromFieldsEditor": false, 300 | "subFields": [ 301 | { 302 | "helperText": "", 303 | "advanced": false, 304 | "showIf": "", 305 | "hideFromUI": false, 306 | "broadcast": false, 307 | "model": "", 308 | "hideFromFieldsEditor": false, 309 | "name": "text", 310 | "bubble": false, 311 | "type": "color", 312 | "noPhotoPicker": false, 313 | "@type": "@builder.io/core:Field", 314 | "hidden": false, 315 | "copyOnAdd": true, 316 | "permissionsRequiredToEdit": "", 317 | "simpleTextOnly": false, 318 | "autoFocus": false, 319 | "showTemplatePicker": true, 320 | "onChange": "", 321 | "required": false, 322 | "disallowRemove": false, 323 | "subFields": [], 324 | "mandatory": false 325 | }, 326 | { 327 | "copyOnAdd": true, 328 | "showIf": "", 329 | "advanced": false, 330 | "subFields": [], 331 | "mandatory": false, 332 | "onChange": "", 333 | "hideFromFieldsEditor": false, 334 | "noPhotoPicker": false, 335 | "broadcast": false, 336 | "permissionsRequiredToEdit": "", 337 | "type": "color", 338 | "name": "background", 339 | "hidden": false, 340 | "@type": "@builder.io/core:Field", 341 | "autoFocus": false, 342 | "disallowRemove": false, 343 | "required": false, 344 | "showTemplatePicker": true, 345 | "hideFromUI": false, 346 | "simpleTextOnly": false, 347 | "bubble": false, 348 | "model": "", 349 | "helperText": "" 350 | }, 351 | { 352 | "@type": "@builder.io/core:Field", 353 | "hidden": false, 354 | "bubble": false, 355 | "model": "", 356 | "copyOnAdd": true, 357 | "permissionsRequiredToEdit": "", 358 | "required": false, 359 | "hideFromUI": false, 360 | "subFields": [], 361 | "disallowRemove": false, 362 | "advanced": false, 363 | "helperText": "", 364 | "broadcast": false, 365 | "simpleTextOnly": false, 366 | "type": "color", 367 | "noPhotoPicker": false, 368 | "name": "primary", 369 | "hideFromFieldsEditor": false, 370 | "showTemplatePicker": true, 371 | "onChange": "", 372 | "autoFocus": false, 373 | "mandatory": false, 374 | "showIf": "" 375 | }, 376 | { 377 | "disallowRemove": false, 378 | "helperText": "", 379 | "bubble": false, 380 | "copyOnAdd": true, 381 | "model": "", 382 | "hidden": false, 383 | "hideFromUI": false, 384 | "name": "secondary", 385 | "@type": "@builder.io/core:Field", 386 | "subFields": [], 387 | "required": false, 388 | "onChange": "", 389 | "broadcast": false, 390 | "permissionsRequiredToEdit": "", 391 | "hideFromFieldsEditor": false, 392 | "noPhotoPicker": false, 393 | "mandatory": false, 394 | "type": "color", 395 | "showTemplatePicker": true, 396 | "autoFocus": false, 397 | "simpleTextOnly": false, 398 | "showIf": "", 399 | "advanced": false 400 | }, 401 | { 402 | "autoFocus": false, 403 | "name": "muted", 404 | "copyOnAdd": true, 405 | "helperText": "", 406 | "hidden": false, 407 | "hideFromUI": false, 408 | "showTemplatePicker": true, 409 | "advanced": false, 410 | "hideFromFieldsEditor": false, 411 | "required": false, 412 | "broadcast": false, 413 | "noPhotoPicker": false, 414 | "permissionsRequiredToEdit": "", 415 | "model": "", 416 | "disallowRemove": false, 417 | "onChange": "", 418 | "@type": "@builder.io/core:Field", 419 | "showIf": "", 420 | "subFields": [], 421 | "bubble": false, 422 | "mandatory": false, 423 | "type": "color", 424 | "simpleTextOnly": false 425 | } 426 | ], 427 | "required": false, 428 | "helperText": "", 429 | "showTemplatePicker": true 430 | }, 431 | { 432 | "permissionsRequiredToEdit": "", 433 | "subFields": [ 434 | { 435 | "subFields": [], 436 | "helperText": "Default SEO title", 437 | "simpleTextOnly": false, 438 | "advanced": false, 439 | "hidden": false, 440 | "defaultValue": "Builder.io + Next.js + Swell Demo", 441 | "required": false, 442 | "autoFocus": false, 443 | "mandatory": false, 444 | "name": "title", 445 | "showTemplatePicker": true, 446 | "permissionsRequiredToEdit": "", 447 | "broadcast": false, 448 | "copyOnAdd": true, 449 | "model": "", 450 | "onChange": "", 451 | "disallowRemove": false, 452 | "noPhotoPicker": false, 453 | "@type": "@builder.io/core:Field", 454 | "hideFromUI": false, 455 | "bubble": false, 456 | "type": "text", 457 | "hideFromFieldsEditor": false, 458 | "showIf": "" 459 | }, 460 | { 461 | "hideFromFieldsEditor": false, 462 | "showIf": "", 463 | "required": false, 464 | "helperText": "%s is where the page title will be", 465 | "bubble": false, 466 | "hidden": false, 467 | "name": "titleTemplate", 468 | "model": "", 469 | "subFields": [], 470 | "simpleTextOnly": false, 471 | "type": "text", 472 | "onChange": "", 473 | "hideFromUI": false, 474 | "showTemplatePicker": true, 475 | "copyOnAdd": true, 476 | "@type": "@builder.io/core:Field", 477 | "disallowRemove": false, 478 | "defaultValue": "%s - Headless Demo", 479 | "autoFocus": false, 480 | "noPhotoPicker": false, 481 | "permissionsRequiredToEdit": "", 482 | "mandatory": false, 483 | "broadcast": false, 484 | "advanced": false 485 | }, 486 | { 487 | "type": "text", 488 | "bubble": false, 489 | "disallowRemove": false, 490 | "onChange": "", 491 | "hideFromFieldsEditor": false, 492 | "broadcast": false, 493 | "model": "", 494 | "@type": "@builder.io/core:Field", 495 | "advanced": false, 496 | "showIf": "", 497 | "name": "description", 498 | "mandatory": false, 499 | "hidden": false, 500 | "copyOnAdd": true, 501 | "hideFromUI": false, 502 | "noPhotoPicker": false, 503 | "autoFocus": false, 504 | "permissionsRequiredToEdit": "", 505 | "simpleTextOnly": false, 506 | "defaultValue": "A starter kit demo for Swell/Builder.io/Next.js -> https://github.com/swellstores/nextjs-builder", 507 | "showTemplatePicker": true, 508 | "required": false, 509 | "helperText": "Default description text for site pages ( SEO)", 510 | "subFields": [] 511 | }, 512 | { 513 | "subFields": [], 514 | "autoFocus": false, 515 | "type": "map", 516 | "onChange": "", 517 | "bubble": false, 518 | "defaultValue": { 519 | "locale": "en_IE", 520 | "type": "website", 521 | "site_name": "Builder.io + Next.js + Swell Demo", 522 | "url": "https://github.com/swellstores/nextjs-builder" 523 | }, 524 | "showIf": "", 525 | "model": "", 526 | "permissionsRequiredToEdit": "", 527 | "@type": "@builder.io/core:Field", 528 | "name": "openGraph", 529 | "advanced": false, 530 | "noPhotoPicker": false, 531 | "hideFromUI": false, 532 | "helperText": "Open graph map", 533 | "hidden": false, 534 | "mandatory": false, 535 | "required": false, 536 | "copyOnAdd": true, 537 | "hideFromFieldsEditor": false, 538 | "disallowRemove": false, 539 | "showTemplatePicker": true, 540 | "broadcast": false, 541 | "simpleTextOnly": false 542 | } 543 | ], 544 | "@type": "@builder.io/core:Field", 545 | "showTemplatePicker": true, 546 | "model": "", 547 | "bubble": false, 548 | "hideFromUI": false, 549 | "onChange": "", 550 | "hidden": false, 551 | "name": "siteInformation", 552 | "broadcast": false, 553 | "hideFromFieldsEditor": false, 554 | "disallowRemove": false, 555 | "mandatory": false, 556 | "copyOnAdd": true, 557 | "showIf": "", 558 | "type": "object", 559 | "required": false, 560 | "advanced": false, 561 | "noPhotoPicker": false, 562 | "defaultValue": { 563 | "titleTemplate": "%s - Headless Demo", 564 | "openGraph": { 565 | "site_name": "Builder.io + Next.js + Swell Demo", 566 | "locale": "en_IE", 567 | "type": "website", 568 | "url": "https://github.com/swellstores/nextjs-builder" 569 | }, 570 | "title": "Builder.io + Next.js + Swell Demo", 571 | "description": "A starter kit demo store for using Swell and Builder.io -> https://github.com/swellstores/nextjs-builder" 572 | }, 573 | "helperText": "", 574 | "simpleTextOnly": false, 575 | "autoFocus": false 576 | } 577 | ], 578 | "getSchemaFromPage": false, 579 | "helperText": "", 580 | "injectWcPosition": "", 581 | "id": "c97db5356e4b440d94423eb6c6d8173d", 582 | "defaultQuery": [], 583 | "publicWritable": false, 584 | "kind": "data", 585 | "createdDate": 1631307264305, 586 | "schema": {}, 587 | "bigData": false, 588 | "examplePageUrl": "http://localhost:3000", 589 | "hooks": {}, 590 | "individualEmbed": false, 591 | "apiGenerated": true, 592 | "allowTests": true, 593 | "showAbTests": true, 594 | "hideFromUI": false, 595 | "strictPrivateWrite": false, 596 | "hideOptions": false, 597 | "webhooks": [], 598 | "pathPrefix": "/", 599 | "sendToElasticSearch": false, 600 | "subType": "", 601 | "requiredTargets": [], 602 | "useQueryParamTargetingClientSide": false, 603 | "isPage": false, 604 | "componentsOnlyMode": false, 605 | "strictPrivateRead": false, 606 | "designerVersion": 1, 607 | "lastUpdateBy": null, 608 | "repeatable": false, 609 | "injectWcAt": "", 610 | "hidden": false, 611 | "showMetrics": true, 612 | "name": "theme", 613 | "showScheduling": true, 614 | "@originId": "93a28cddbe257c8c956bbdb79408188bfab7220ad04e7c505e572d47a6b0406d", 615 | "allowMetrics": true, 616 | "archived": false, 617 | "showTargeting": true, 618 | "sendToMongoDb": true, 619 | "allowHeatmap": true 620 | } 621 | -------------------------------------------------------------------------------- /builder/theme/site-theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "@originContentId": "a312ae57f3de886b360cafc1986714f81e8ed93bc6d0a09f717b9f91dae23f79", 3 | "@originModelId": "93a28cddbe257c8c956bbdb79408188bfab7220ad04e7c505e572d47a6b0406d", 4 | "@originOrg": "4d832b53c3cf422f9a6c92a23abf35ac", 5 | "createdBy": "4FFFg0MNRJT0z0nW4uUizDHfHJV2", 6 | "createdDate": 1618890934748, 7 | "data": { 8 | "colorOverrides": {}, 9 | "siteInformation": { 10 | "description": "A starter kit demo store for using Swell and Builder.io -> https://github.com/swellstores/nextjs-builder", 11 | "openGraph": { 12 | "locale": "en_IE", 13 | "site_name": "Builder.io + Next.js + Swell Demo", 14 | "type": "website", 15 | "url": "https://github.com/swellstores/nextjs-builder" 16 | }, 17 | "title": "Builder.io + Next.js + Swell Demo", 18 | "titleTemplate": "%s - Headless Demo" 19 | }, 20 | "siteSettings": { 21 | "logo": { 22 | "text": "Storefront" 23 | }, 24 | "navigationLinks": [ 25 | { 26 | "link": "/faq", 27 | "title": "FAQ" 28 | }, 29 | { 30 | "link": "/about-us", 31 | "title": "About us" 32 | } 33 | ] 34 | }, 35 | "theme": "swiss" 36 | }, 37 | "id": "c77df46d5ce0441c86ca4bafd757d88d", 38 | "lastUpdated": 1631307937069, 39 | "lastUpdatedBy": "4FFFg0MNRJT0z0nW4uUizDHfHJV2", 40 | "meta": { 41 | "kind": "data" 42 | }, 43 | "modelId": "c97db5356e4b440d94423eb6c6d8173d", 44 | "name": "Site theme", 45 | "published": "published", 46 | "query": [], 47 | "rev": "n7tziq0rw7c", 48 | "testRatio": 1, 49 | "variations": {} 50 | } 51 | -------------------------------------------------------------------------------- /components/cart/CartItem/CartItem.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxRuntime classic */ 2 | /** @jsx jsx */ 3 | import { Themed, jsx, Grid, Button, Input, Text, IconButton } from 'theme-ui' 4 | import React, { ChangeEvent, useEffect, useState } from 'react' 5 | import Image from 'next/image' 6 | import Link from 'next/link' 7 | import { Plus, Minus } from '@components/icons' 8 | import { getPrice } from '@lib/swell/storefront-data-hooks/src/utils/product' 9 | import { 10 | useUpdateItemQuantity, 11 | useRemoveItemFromCart, 12 | } from '@lib/swell/storefront-data-hooks' 13 | 14 | const CartItem = ({ 15 | item, 16 | currencyCode, 17 | }: { 18 | item: any 19 | currencyCode: string 20 | }) => { 21 | const updateItem = useUpdateItemQuantity() 22 | const removeItem = useRemoveItemFromCart() 23 | const [quantity, setQuantity] = useState(item.quantity) 24 | const [removing, setRemoving] = useState(false) 25 | const updateQuantity = async (quantity: number) => { 26 | await updateItem(item.id, quantity) 27 | } 28 | const handleQuantity = (e: ChangeEvent) => { 29 | const val = Number(e.target.value) 30 | 31 | if (Number.isInteger(val) && val >= 0) { 32 | setQuantity(val) 33 | } 34 | } 35 | const handleBlur = () => { 36 | const val = Number(quantity) 37 | 38 | if (val !== item.quantity) { 39 | updateQuantity(val) 40 | } 41 | } 42 | const increaseQuantity = (n = 1) => { 43 | const val = Number(quantity) + n 44 | 45 | if (Number.isInteger(val) && val >= 0) { 46 | setQuantity(val) 47 | updateQuantity(val) 48 | } 49 | } 50 | const handleRemove = async () => { 51 | setRemoving(true) 52 | 53 | try { 54 | // If this action succeeds then there's no need to do `setRemoving(true)` 55 | // because the component will be removed from the view 56 | await removeItem(item.product.id) 57 | } catch (error) { 58 | console.error(error) 59 | setRemoving(false) 60 | } 61 | } 62 | 63 | useEffect(() => { 64 | // Reset the quantity state if the item quantity changes 65 | if (item.quantity !== Number(quantity)) { 66 | setQuantity(item.quantity) 67 | } 68 | }, [item.quantity]) 69 | 70 | return ( 71 | 72 |
81 | {item.product.meta_description} 88 |
89 |
90 | 95 | <> 96 | {item.product.name} 97 | 105 | {getPrice( 106 | item.price, 107 | currencyCode 108 | )} 109 | 110 | 111 | 112 | 113 |
  • 114 |
    115 | increaseQuantity(-1)}> 116 | 117 | 118 | 119 | 133 | increaseQuantity(1)}> 134 | 135 | 136 |
    137 |
  • 138 | {/* {item.variant.selectedOptions.map((option: any) => ( 139 |
  • 140 | {option.name}:{option.value} 141 |
  • 142 | ))} */} 143 |
    144 |
    145 |
    146 | ) 147 | } 148 | 149 | /** 150 | * 151 | 152 | */ 153 | 154 | export default CartItem 155 | -------------------------------------------------------------------------------- /components/cart/CartItem/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './CartItem' 2 | -------------------------------------------------------------------------------- /components/cart/CartSidebarView/CartSidebarView.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxRuntime classic */ 2 | /** @jsx jsx */ 3 | import React from 'react' 4 | import { Themed, jsx, Text, Card, Grid, Divider, NavLink } from 'theme-ui' 5 | import { FC, useEffect, useState } from 'react' 6 | import { Bag } from '@components/icons' 7 | import { useCart, useCheckoutUrl } from '@lib/swell/storefront-data-hooks' 8 | import CartItem from '../CartItem' 9 | import { BuilderComponent, builder } from '@builder.io/react' 10 | import env from '@config/env' 11 | import { getPrice } from '@lib/swell/storefront-data-hooks/src/utils/product' 12 | 13 | const CartSidebarView: FC = () => { 14 | const checkoutUrl = useCheckoutUrl() 15 | const cart = useCart() 16 | const subTotal = getPrice(cart?.sub_total, cart?.currency ?? 'USD') 17 | const total = getPrice(cart?.grand_total, cart?.currency ?? 'USD') 18 | const shippingTotal = getPrice(cart?.shipment_total, cart?.currency ?? 'USD') 19 | const taxTotal = getPrice(cart?.tax_total, cart?.currency ?? 'USD') 20 | 21 | const items = cart?.items ?? [] 22 | const isEmpty = items.length === 0 23 | const [cartUpsell, setCartUpsell] = useState() 24 | 25 | useEffect(() => { 26 | async function fetchContent() { 27 | const items = cart?.items || [] 28 | const cartUpsellContent = await builder 29 | .get('cart-upsell-sidebar', { 30 | cachebust: env.isDev, 31 | userAttributes: { 32 | itemInCart: items.map((item: any) => item.product?.slug), 33 | } as any, 34 | }) 35 | .toPromise() 36 | setCartUpsell(cartUpsellContent) 37 | } 38 | fetchContent() 39 | }, [cart?.items]) 40 | 41 | return ( 42 | 56 | {isEmpty ? ( 57 | <> 58 | 59 | Your cart is empty 60 | 61 | Biscuit oat cake wafer icing ice cream tiramisu pudding cupcake. 62 | 63 | 64 | ) : ( 65 | <> 66 | {items.map((item: any) => ( 67 | 72 | ))} 73 | 74 | 75 | Subtotal: 76 | {subTotal} 77 | Shipping: 78 | {shippingTotal} 79 | Tax: 80 | {taxTotal} 81 | 82 | 83 | 84 | 85 | Estimated Total: 86 | 87 | {total} 88 | 89 | 90 | 91 | 92 | {checkoutUrl && ( 93 | 98 | Proceed to Checkout 99 | 100 | )} 101 | 102 | )} 103 | 104 | ) 105 | } 106 | 107 | export default CartSidebarView 108 | -------------------------------------------------------------------------------- /components/cart/CartSidebarView/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './CartSidebarView' 2 | -------------------------------------------------------------------------------- /components/cart/index.ts: -------------------------------------------------------------------------------- 1 | export { default as CartSidebarView } from './CartSidebarView' 2 | export { default as CartItem } from './CartItem' 3 | -------------------------------------------------------------------------------- /components/common/FeatureBar.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxRuntime classic */ 2 | /** @jsx jsx */ 3 | import React, { useEffect, useState } from 'react' 4 | import { Themed, jsx } from 'theme-ui' 5 | import { CenterModal, ModalTitle, ModalCloseTarget } from 'react-spring-modal' 6 | 7 | interface FeatureBarProps { 8 | className?: string 9 | title: string 10 | description?: string 11 | hide?: boolean 12 | action?: React.ReactNode 13 | delay?: number 14 | } 15 | 16 | const FeatureBar: React.FC = ({ 17 | title, 18 | description, 19 | action, 20 | hide, 21 | delay, 22 | }) => { 23 | const [delayPassed, setDelayPassed] = useState(false) 24 | useEffect(() => { 25 | const timeout = setTimeout(() => setDelayPassed(true), delay || 6000) 26 | return () => clearTimeout(timeout) 27 | }) 28 | return ( 29 | 30 | {title} 31 | {description} 32 | 33 | {action && action} 34 | 35 | 36 | ) 37 | } 38 | 39 | export default FeatureBar 40 | -------------------------------------------------------------------------------- /components/common/Head.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import NextHead from 'next/head' 3 | import { DefaultSeo } from 'next-seo' 4 | 5 | const Head: FC<{ seoInfo: any }> = (props) => { 6 | return ( 7 | <> 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | ) 20 | } 21 | 22 | export default Head 23 | -------------------------------------------------------------------------------- /components/common/Layout.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxRuntime classic */ 2 | /** @jsx jsx */ 3 | import React from 'react' 4 | import { ThemeProvider, jsx } from 'theme-ui' 5 | import dynamic from 'next/dynamic' 6 | import { ManagedUIContext, useUI } from '@components/ui/context' 7 | import { Head, Navbar } from '@components/common' 8 | import { useAcceptCookies } from '@lib/hooks/useAcceptCookies' 9 | import { Button } from 'theme-ui' 10 | import { Sidebar } from '@components/ui' 11 | import { CartSidebarView } from '@components/cart' 12 | import { CommerceProvider } from '@lib/swell/storefront-data-hooks' 13 | import swellConfig from '@config/swell' 14 | import { builder, BuilderContent, Builder } from '@builder.io/react' 15 | import themesMap from '@config/theme' 16 | import '@builder.io/widgets' 17 | import 'react-spring-modal/styles.css' 18 | import seoConfig from '@config/seo.json' 19 | import NoSSR from './NoSSR' 20 | 21 | const FeatureBar = dynamic(() => import('@components/common/FeatureBar'), { 22 | ssr: false, 23 | }) 24 | 25 | const Layout: React.FC<{ pageProps: any }> = ({ children, pageProps }) => { 26 | const builderTheme = pageProps.theme 27 | const isLive = !Builder.isEditing && !Builder.isPreviewing 28 | return ( 29 | 30 | 35 | {(data, loading) => { 36 | if (loading && !builderTheme) { 37 | return 'loading ...' 38 | } 39 | const siteSettings = data?.siteSettings 40 | const colorOverrides = data?.colorOverrides 41 | const siteSeoInfo = data?.siteInformation 42 | return ( 43 | 44 | 45 | 49 | {children} 50 | 51 | 52 | ) 53 | }} 54 | 55 | 56 | ) 57 | } 58 | 59 | const InnerLayout: React.FC<{ 60 | themeName: string 61 | colorOverrides?: { 62 | text?: string 63 | background?: string 64 | primary?: string 65 | secondary?: string 66 | muted?: string 67 | } 68 | }> = ({ themeName, children, colorOverrides }) => { 69 | const theme = { 70 | ...themesMap[themeName], 71 | colors: { 72 | ...themesMap[themeName].colors, 73 | ...colorOverrides, 74 | }, 75 | } 76 | const { displaySidebar, closeSidebar } = useUI() 77 | const { acceptedCookies, onAcceptCookies } = useAcceptCookies() 78 | return ( 79 | 80 | 81 |
    90 |
    {children}
    91 |
    92 | 93 | 101 | 102 | 103 | 104 | onAcceptCookies()}>Accept cookies 109 | } 110 | /> 111 | 112 |
    113 | ) 114 | } 115 | 116 | export default Layout 117 | -------------------------------------------------------------------------------- /components/common/Navbar.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxRuntime classic */ 2 | /** @jsx jsx */ 3 | import React, { FC, useState, useEffect } from 'react' 4 | import Link from 'next/link' 5 | import { UserNav } from '@components/common' 6 | import env from '@config/env' 7 | import { BuilderComponent, builder } from '@builder.io/react' 8 | import { useCart } from '@lib/swell/storefront-data-hooks' 9 | import { jsx, Themed, useThemeUI } from 'theme-ui' 10 | import { useUI } from '@components/ui/context' 11 | import Image from 'next/image' 12 | import Searchbar from './Searchbar' 13 | 14 | const Navbar: FC = () => { 15 | const [announcement, setAnnouncement] = useState() 16 | const { theme } = useThemeUI() 17 | const { navigationLinks, logo } = useUI() 18 | const cart = useCart() 19 | 20 | useEffect(() => { 21 | async function fetchContent() { 22 | const items = cart?.items || [] 23 | const anouncementContent = await builder 24 | .get('announcement-bar', { 25 | cachebust: env.isDev, 26 | userAttributes: { 27 | itemInCart: items.map((item: any) => item.product.slug), 28 | } as any, 29 | }) 30 | .toPromise() 31 | setAnnouncement(anouncementContent) 32 | } 33 | fetchContent() 34 | }, [cart?.items]) 35 | 36 | return ( 37 | 38 | 43 | 56 | 64 | {navigationLinks?.map((link, index) => ( 65 | 71 | {link.title} 72 | 73 | ))} 74 | 75 | 82 | 88 | {logo && logo.image && ( 89 | 98 | 104 | 105 | )} 106 | {logo && logo.text && !logo.image && ( 107 | 116 | {logo.text} 117 | 118 | )} 119 | 120 | 121 | 129 | 130 | 131 | 132 | 133 | 134 | ) 135 | } 136 | 137 | export default Navbar 138 | -------------------------------------------------------------------------------- /components/common/NoSSR.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | 3 | const NoSSR: React.FC<{ skeleton?: React.ReactNode }> = ({ 4 | children, 5 | skeleton, 6 | }) => { 7 | const [render, setRender] = useState(false) 8 | useEffect(() => setRender(true), []) 9 | if (render) { 10 | return <>{children} 11 | } 12 | if (skeleton) { 13 | return <>{skeleton} 14 | } 15 | return null 16 | } 17 | export default NoSSR 18 | -------------------------------------------------------------------------------- /components/common/OptionPicker.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxRuntime classic */ 2 | /** @jsx jsx */ 3 | import { jsx } from 'theme-ui' 4 | import { Select, Label } from '@theme-ui/components' 5 | export interface OptionPickerProps { 6 | name: string 7 | options?: Readonly> 8 | onChange?: React.ChangeEventHandler 9 | selected?: string 10 | } 11 | 12 | const OptionPicker: React.FC = ({ 13 | name, 14 | options, 15 | onChange, 16 | selected, 17 | }) => { 18 | return ( 19 |
    20 | 21 | 28 |
    29 | ) 30 | } 31 | 32 | export default OptionPicker 33 | -------------------------------------------------------------------------------- /components/common/ProductCard.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxRuntime classic */ 2 | /** @jsx jsx */ 3 | import { Themed, jsx } from 'theme-ui' 4 | import { Card, Text } from '@theme-ui/components' 5 | import { Link, ImageCarousel } from '@components/ui' 6 | import { getPrice } from '@lib/swell/storefront-data-hooks/src/utils/product' 7 | import { Product } from '@lib/swell/storefront-data-hooks/src/types' 8 | 9 | 10 | export interface ProductCardProps { 11 | className?: string 12 | product: Product 13 | imgWidth: number | string 14 | imgHeight: number | string 15 | imgLayout?: 'fixed' | 'intrinsic' | 'responsive' | undefined 16 | imgPriority?: boolean 17 | imgLoading?: 'eager' | 'lazy' 18 | imgSizes?: string 19 | } 20 | 21 | const ProductCard: React.FC = ({ 22 | product, 23 | imgWidth, 24 | imgHeight, 25 | imgPriority, 26 | imgLoading, 27 | imgSizes, 28 | imgLayout = 'responsive', 29 | }) => { 30 | const handle = (product).slug 31 | const price = getPrice( 32 | product.price, 33 | product.currency ?? 'USD' 34 | ) 35 | 36 | return ( 37 | 45 | 46 |
    47 | 62 |
    63 |
    64 | 65 | {product.name} 66 | 67 | {price} 68 |
    69 | 70 |
    71 | ) 72 | } 73 | 74 | export default ProductCard 75 | -------------------------------------------------------------------------------- /components/common/ProductCardDemo.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxRuntime classic */ 2 | /** @jsx jsx */ 3 | import { Themed, jsx } from 'theme-ui' 4 | import Image from 'next/image' 5 | import { Card, Text } from '@theme-ui/components' 6 | import { Link } from '@components/ui' 7 | import { getPrice } from '@lib/swell/storefront-data-hooks/src/utils/product' 8 | import { useState } from 'react' 9 | import NoSSR from './NoSSR' 10 | 11 | export interface ProductCardProps { 12 | className?: string 13 | product: any 14 | imgWidth: number | string 15 | imgHeight: number | string 16 | imgLayout?: 'fixed' | 'intrinsic' | 'responsive' | undefined 17 | imgPriority?: boolean 18 | imgLoading?: 'eager' | 'lazy' 19 | imgSizes?: string 20 | } 21 | 22 | const ProductCardDemo: React.FC = ({ 23 | product, 24 | imgWidth, 25 | imgHeight, 26 | imgPriority, 27 | imgLoading, 28 | imgSizes, 29 | imgLayout = 'responsive', 30 | }) => { 31 | const [showAlternate, setShowAlternate] = useState(false) 32 | const [canToggle, setCanToggle] = useState(false) 33 | const src = product.images[0].src 34 | const handle = (product as any).handle 35 | const productVariant: any = product.variants[0] 36 | const price = getPrice( 37 | productVariant.price, 38 | product.currency ?? 'USD' 39 | ) 40 | const alternateImage = product.images[1]?.file.url 41 | 42 | return ( 43 | setShowAlternate(false)} 51 | onMouseOver={() => setShowAlternate(true)} 52 | > 53 | 54 |
    55 | {alternateImage && ( 56 |
    59 | 60 | {product.title} setCanToggle(true)} 69 | loading="eager" 70 | /> 71 | 72 |
    73 | )} 74 |
    80 | {product.title} 91 |
    92 |
    93 |
    94 | 95 | {product.title} 96 | 97 | {price} 98 |
    99 | 100 |
    101 | ) 102 | } 103 | 104 | export default ProductCardDemo 105 | -------------------------------------------------------------------------------- /components/common/Searchbar.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxRuntime classic */ 2 | /** @jsx jsx */ 3 | import React, { FC, useState, useEffect, useCallback, useRef } from 'react' 4 | import { useRouter } from 'next/router' 5 | import { LoadingDots } from '@components/ui' 6 | import { ProductGrid } from 'blocks/ProductGrid/ProductGrid' 7 | import { Button, Themed, jsx, Input, Label } from 'theme-ui' 8 | import { searchProducts } from '@lib/swell/storefront-data-hooks/src/api/operations-swell' 9 | import { ExpandModal } from 'react-spring-modal' 10 | import { throttle } from 'lodash' 11 | import 'react-spring-modal/styles.css' 12 | import { Cross } from '@components/icons' 13 | 14 | interface Props { 15 | className?: string 16 | id?: string 17 | } 18 | 19 | const Searchbar: FC = () => { 20 | const router = useRouter() 21 | const { q } = router.query 22 | const [isOpen, setIsOpen] = useState(false) 23 | const buttonRef = useRef(null) 24 | 25 | useEffect(() => { 26 | setIsOpen(false) 27 | }, [router.asPath.split('?')[0]]) 28 | 29 | return ( 30 | 31 | 45 | { 48 | const op = q ? 'replace' : 'push' 49 | router[op]({ 50 | pathname: router.asPath.split('?')[0], 51 | query: { 52 | q: term, 53 | }, 54 | }) 55 | }} 56 | /> 57 | 58 | 59 | setIsOpen(!isOpen)} 64 | aria-label="Search" 65 | > 66 | {isOpen ? ( 67 | 68 | ) : ( 69 | 76 | 81 | 82 | )} 83 | 84 | 85 | ) 86 | } 87 | 88 | const SearchModalContent = (props: { 89 | initialSearch?: string 90 | onSearch: (term: string) => any 91 | }) => { 92 | const [search, setSearch] = useState( 93 | props.initialSearch && String(props.initialSearch) 94 | ) 95 | const [products, setProducts] = useState([] as any[]) 96 | const [loading, setLoading] = useState(false) 97 | const router = useRouter() 98 | const getProducts = async (searchTerm: string) => { 99 | setLoading(true) 100 | const results = await searchProducts( 101 | String(searchTerm), 102 | // TODO: pagination 103 | 20, 104 | 0 105 | ) 106 | setSearch(searchTerm) 107 | setProducts(results) 108 | setLoading(false) 109 | if (searchTerm) { 110 | props.onSearch(searchTerm) 111 | } 112 | } 113 | 114 | useEffect(() => { 115 | if (search) { 116 | getProducts(search) 117 | } 118 | }, []) 119 | 120 | const throttleSearch = useCallback(throttle(getProducts), []) 121 | 122 | return ( 123 | 132 | throttleSearch(event.target.value)} 138 | /> 139 | {loading ? ( 140 | 141 | ) : products.length ? ( 142 | <> 143 | 146 | 156 | 157 | ) : ( 158 | 159 | {search ? ( 160 | <> 161 | There are no products that match "{search}" 162 | 163 | ) : ( 164 | <> 165 | )} 166 | 167 | )} 168 | 169 | ) 170 | } 171 | 172 | export default Searchbar 173 | -------------------------------------------------------------------------------- /components/common/Thumbnail.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxRuntime classic */ 2 | /** @jsx jsx */ 3 | import { jsx, AspectRatio } from 'theme-ui' 4 | import Image from 'next/image' 5 | 6 | export interface ThumbnailProps { 7 | src: any // for now; 8 | onClick?: React.MouseEventHandler 9 | onHover?: React.MouseEventHandler 10 | name?: string 11 | width: number 12 | height: number 13 | } 14 | 15 | const Thumbnail: React.FC = ({ 16 | src, 17 | onClick, 18 | onHover, 19 | name, 20 | width, 21 | height, 22 | }) => { 23 | return ( 24 | 40 | ) 41 | } 42 | 43 | export default Thumbnail 44 | -------------------------------------------------------------------------------- /components/common/UntilInteraction.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | 3 | const UntilInteraction: React.FC<{ skeleton: React.ReactNode }> = ({ 4 | children, 5 | skeleton, 6 | }) => { 7 | const [render, setRender] = useState(false) 8 | if (render) { 9 | return <>{children} 10 | } 11 | return ( 12 |
    setRender(true)} 14 | onClick={() => setRender(true)} 15 | onTouchStart={() => setRender(true)} 16 | > 17 | {skeleton} 18 |
    19 | ) 20 | } 21 | export default UntilInteraction 22 | -------------------------------------------------------------------------------- /components/common/UserNav.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxRuntime classic */ 2 | /** @jsx jsx */ 3 | import { FC } from 'react' 4 | import { Bag } from '@components/icons' 5 | import { useUI } from '@components/ui/context' 6 | import { Button, jsx } from 'theme-ui' 7 | 8 | interface Props { 9 | className?: string 10 | } 11 | 12 | const UserNav: FC = ({ className, children, ...props }) => { 13 | const { toggleSidebar } = useUI() 14 | 15 | return ( 16 | 19 | ) 20 | } 21 | 22 | export default UserNav 23 | -------------------------------------------------------------------------------- /components/common/index.ts: -------------------------------------------------------------------------------- 1 | export { default as FeatureBar } from './FeatureBar' 2 | export { default as Layout } from './Layout' 3 | export { default as Navbar } from './Navbar' 4 | export { default as Searchbar } from './Searchbar' 5 | export { default as UserNav } from './UserNav' 6 | export { default as Head } from './Head' 7 | export { default as OptionPicker } from './OptionPicker' 8 | export { default as ProductCard } from './ProductCard' 9 | export { default as ProductCardDemo } from './ProductCardDemo' 10 | -------------------------------------------------------------------------------- /components/icons/ArrowLeft.tsx: -------------------------------------------------------------------------------- 1 | const ArrowLeft = ({ ...props }) => { 2 | return ( 3 | 11 | 17 | 23 | 24 | ) 25 | } 26 | 27 | export default ArrowLeft 28 | -------------------------------------------------------------------------------- /components/icons/Bag.tsx: -------------------------------------------------------------------------------- 1 | const Bag = ({ ...props }) => { 2 | return ( 3 | 11 | 17 | 23 | 29 | 30 | ) 31 | } 32 | 33 | export default Bag 34 | -------------------------------------------------------------------------------- /components/icons/Check.tsx: -------------------------------------------------------------------------------- 1 | const Check = ({ ...props }) => { 2 | return ( 3 | 11 | 17 | 18 | ) 19 | } 20 | 21 | export default Check 22 | -------------------------------------------------------------------------------- /components/icons/ChevronUp.tsx: -------------------------------------------------------------------------------- 1 | const ChevronUp = ({ ...props }) => { 2 | return ( 3 | 15 | 16 | 17 | ) 18 | } 19 | 20 | export default ChevronUp 21 | -------------------------------------------------------------------------------- /components/icons/Cross.tsx: -------------------------------------------------------------------------------- 1 | const Cross = ({ ...props }) => { 2 | return ( 3 | 15 | 16 | 17 | 18 | ) 19 | } 20 | 21 | export default Cross 22 | -------------------------------------------------------------------------------- /components/icons/DoubleChevron.tsx: -------------------------------------------------------------------------------- 1 | const DoubleChevron = ({ ...props }) => { 2 | return ( 3 | 11 | 18 | 19 | ) 20 | } 21 | 22 | export default DoubleChevron 23 | -------------------------------------------------------------------------------- /components/icons/Github.tsx: -------------------------------------------------------------------------------- 1 | const Github = ({ ...props }) => { 2 | return ( 3 | 10 | 16 | 17 | ) 18 | } 19 | 20 | export default Github 21 | -------------------------------------------------------------------------------- /components/icons/Heart.tsx: -------------------------------------------------------------------------------- 1 | const Heart = ({ ...props }) => { 2 | return ( 3 | 11 | 18 | 19 | ) 20 | } 21 | 22 | export default Heart 23 | -------------------------------------------------------------------------------- /components/icons/Info.tsx: -------------------------------------------------------------------------------- 1 | const Info = ({ ...props }) => { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | ) 20 | } 21 | 22 | export default Info 23 | -------------------------------------------------------------------------------- /components/icons/Minus.tsx: -------------------------------------------------------------------------------- 1 | const Minus = ({ ...props }) => { 2 | return ( 3 | 4 | 11 | 12 | ) 13 | } 14 | 15 | export default Minus 16 | -------------------------------------------------------------------------------- /components/icons/Moon.tsx: -------------------------------------------------------------------------------- 1 | const Moon = ({ ...props }) => { 2 | return ( 3 | 15 | 16 | 17 | ) 18 | } 19 | 20 | export default Moon 21 | -------------------------------------------------------------------------------- /components/icons/Plus.tsx: -------------------------------------------------------------------------------- 1 | const Plus = ({ ...props }) => { 2 | return ( 3 | 4 | 11 | 18 | 19 | ) 20 | } 21 | 22 | export default Plus 23 | -------------------------------------------------------------------------------- /components/icons/RightArrow.tsx: -------------------------------------------------------------------------------- 1 | const RightArrow = ({ ...props }) => { 2 | return ( 3 | 11 | 18 | 25 | 26 | ) 27 | } 28 | 29 | export default RightArrow 30 | -------------------------------------------------------------------------------- /components/icons/Sun.tsx: -------------------------------------------------------------------------------- 1 | const Sun = ({ ...props }) => { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ) 26 | } 27 | 28 | export default Sun 29 | -------------------------------------------------------------------------------- /components/icons/Trash.tsx: -------------------------------------------------------------------------------- 1 | const Trash = ({ ...props }) => { 2 | return ( 3 | 11 | 18 | 25 | 32 | 39 | 40 | ) 41 | } 42 | 43 | export default Trash 44 | -------------------------------------------------------------------------------- /components/icons/Vercel.tsx: -------------------------------------------------------------------------------- 1 | const Vercel = ({ ...props }) => { 2 | return ( 3 | 11 | 12 | 16 | 20 | 24 | 28 | 32 | 36 | 37 | ) 38 | } 39 | 40 | export default Vercel 41 | -------------------------------------------------------------------------------- /components/icons/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Bag } from './Bag' 2 | export { default as Heart } from './Heart' 3 | export { default as Trash } from './Trash' 4 | export { default as Cross } from './Cross' 5 | export { default as ArrowLeft } from './ArrowLeft' 6 | export { default as Plus } from './Plus' 7 | export { default as Minus } from './Minus' 8 | export { default as Check } from './Check' 9 | export { default as Sun } from './Sun' 10 | export { default as Moon } from './Moon' 11 | export { default as Github } from './Github' 12 | export { default as DoubleChevron } from './DoubleChevron' 13 | export { default as RightArrow } from './RightArrow' 14 | export { default as Info } from './Info' 15 | export { default as ChevronUp } from './ChevronUp' 16 | export { default as Vercel } from './Vercel' 17 | -------------------------------------------------------------------------------- /components/ui/ImageCarousel.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxRuntime classic */ 2 | /** @jsx jsx */ 3 | import { jsx, Themed, AspectRatio } from 'theme-ui' 4 | import React from 'react' 5 | import Image from 'next/image' 6 | import dynamic from 'next/dynamic' 7 | import UntilInteraction from '@components/common/UntilInteraction' 8 | 9 | type props = import('./LazyImageCarousel').ImageCarouselProps 10 | 11 | const LazyCarousel = dynamic(() => import('./LazyImageCarousel'), { 12 | loading: () => , 13 | ssr: false, 14 | }) 15 | const ImageCarousel: React.FC = ({ 16 | images, 17 | onThumbnailClick, 18 | showZoom, 19 | currentSlide, 20 | ...imageProps 21 | }) => { 22 | return ( 23 | 24 | } 26 | > 27 | 34 | 35 | 36 | ) 37 | } 38 | export default ImageCarousel 39 | -------------------------------------------------------------------------------- /components/ui/LazyImageCarousel.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxRuntime classic */ 2 | /** @jsx jsx */ 3 | import { jsx, IconButton } from 'theme-ui' 4 | import { FC } from 'react' 5 | import { 6 | CarouselProvider, 7 | ImageWithZoom, 8 | Slide, 9 | Slider, 10 | Dot, 11 | } from 'pure-react-carousel' 12 | import Image from 'next/image' 13 | 14 | import 'pure-react-carousel/dist/react-carousel.es.css' 15 | 16 | const CustomDotGroup: FC> = ({ 17 | images, 18 | onThumbnailClick, 19 | ...imageProps 20 | }) => { 21 | return ( 22 |
    34 | {images.map((image, slide) => ( 35 | onThumbnailClick?.(slide)} 40 | > 41 | 42 | 48 | 49 | 50 | ))} 51 |
    52 | ) 53 | } 54 | 55 | export type ImageCarouselProps = { 56 | showZoom?: boolean 57 | images: Array<{ src: string }> 58 | alt: string 59 | onThumbnailClick?: (index: number) => void 60 | width: number | string 61 | height: number | string 62 | layout?: 'fixed' | 'intrinsic' | 'responsive' | undefined 63 | priority?: boolean 64 | loading?: 'eager' | 'lazy' 65 | sizes?: string 66 | currentSlide?: number 67 | } 68 | 69 | const ImageCarousel: FC = ({ 70 | images, 71 | onThumbnailClick, 72 | showZoom, 73 | currentSlide, 74 | ...imageProps 75 | }) => ( 76 | 83 | 84 | {images.map((image, index) => ( 85 | 86 | {showZoom ? ( 87 | 88 | ) : ( 89 | 90 | )} 91 | 92 | ))} 93 | 94 | {showZoom && ( 95 | 100 | )} 101 | 102 | 103 | ) 104 | 105 | export default ImageCarousel 106 | -------------------------------------------------------------------------------- /components/ui/Link/Link.tsx: -------------------------------------------------------------------------------- 1 | import NextLink, { LinkProps as NextLinkProps } from 'next/link' 2 | 3 | const Link: React.FC = ({ href, children, ...props }) => { 4 | return ( 5 | 6 | {children} 7 | 8 | ) 9 | } 10 | 11 | export default Link 12 | -------------------------------------------------------------------------------- /components/ui/Link/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Link' 2 | -------------------------------------------------------------------------------- /components/ui/LoadingDots/LoadingDots.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | @apply inline-flex text-center items-center leading-7; 3 | 4 | & span { 5 | @apply bg-accents-6 rounded-full h-2 w-2; 6 | animation-name: blink; 7 | animation-duration: 1.4s; 8 | animation-iteration-count: infinite; 9 | animation-fill-mode: both; 10 | margin: 0 2px; 11 | 12 | &:nth-of-type(2) { 13 | animation-delay: 0.2s; 14 | } 15 | 16 | &:nth-of-type(3) { 17 | animation-delay: 0.4s; 18 | } 19 | } 20 | } 21 | 22 | @keyframes blink { 23 | 0% { 24 | opacity: 0.2; 25 | } 26 | 20% { 27 | opacity: 1; 28 | } 29 | 100% { 30 | opacity: 0.2; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /components/ui/LoadingDots/LoadingDots.tsx: -------------------------------------------------------------------------------- 1 | import s from './LoadingDots.module.css' 2 | 3 | const LoadingDots: React.FC = () => { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | ) 11 | } 12 | 13 | export default LoadingDots 14 | -------------------------------------------------------------------------------- /components/ui/LoadingDots/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './LoadingDots' 2 | -------------------------------------------------------------------------------- /components/ui/README.md: -------------------------------------------------------------------------------- 1 | # UI 2 | 3 | Building blocks to build a rich graphical interfaces. Components should be atomic and pure. Serve one purpose. 4 | -------------------------------------------------------------------------------- /components/ui/Sidebar/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxRuntime classic */ 2 | /** @jsx jsx */ 3 | import { jsx, Close, Themed } from 'theme-ui' 4 | import { useResponsiveValue } from '@theme-ui/match-media' 5 | import { FC } from 'react' 6 | import { BaseModal, ModalCloseTarget } from 'react-spring-modal' 7 | 8 | interface Props { 9 | open: boolean 10 | onClose: () => void 11 | } 12 | 13 | const Sidebar: FC = ({ children, open = false, onClose }) => { 14 | const width = useResponsiveValue(['100%', 500]) 15 | return ( 16 | 34 | 35 | 44 | 45 | 46 | 47 | {children} 48 | 49 | ) 50 | } 51 | 52 | export default Sidebar 53 | -------------------------------------------------------------------------------- /components/ui/Sidebar/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Sidebar' 2 | -------------------------------------------------------------------------------- /components/ui/context.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useMemo } from 'react' 2 | 3 | export interface State { 4 | displaySidebar: boolean 5 | navigationLinks?: Array<{ link: string; title: string }> 6 | logo?: { image?: string; text: string; width: number; height: number } 7 | toggleSidebar?: any 8 | closeSidebar?: any 9 | openSidebar?: any 10 | } 11 | 12 | const initialState = { 13 | displaySidebar: false, 14 | } 15 | 16 | type Action = 17 | | { 18 | type: 'OPEN_SIDEBAR' 19 | } 20 | | { 21 | type: 'CLOSE_SIDEBAR' 22 | } 23 | 24 | export const UIContext = React.createContext(initialState) 25 | 26 | UIContext.displayName = 'UIContext' 27 | 28 | export const UIProvider: FC<{ siteSettings: Partial }> = ({ 29 | siteSettings, 30 | children, 31 | }) => { 32 | const [state, setState] = React.useState({ 33 | ...initialState, 34 | ...siteSettings, 35 | }) 36 | 37 | const openSidebar = () => setState(() => ({ displaySidebar: true })) 38 | const closeSidebar = () => setState(() => ({ displaySidebar: false })) 39 | const toggleSidebar = () => 40 | setState((prev) => ({ displaySidebar: !prev.displaySidebar })) 41 | 42 | const value = { 43 | ...state, 44 | ...siteSettings, 45 | openSidebar, 46 | closeSidebar, 47 | toggleSidebar, 48 | } 49 | 50 | return 51 | } 52 | 53 | export const useUI = () => { 54 | const context = React.useContext(UIContext) 55 | if (context === undefined) { 56 | throw new Error(`useUI must be used within a UIProvider`) 57 | } 58 | return context 59 | } 60 | 61 | export const ManagedUIContext: FC<{ siteSettings: Partial }> = ({ 62 | children, 63 | siteSettings, 64 | }) => {children} 65 | -------------------------------------------------------------------------------- /components/ui/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Sidebar } from './Sidebar' 2 | export { default as LoadingDots } from './LoadingDots' 3 | export { default as Link } from './Link' 4 | export { default as ImageCarousel } from './ImageCarousel' 5 | -------------------------------------------------------------------------------- /config/builder.ts: -------------------------------------------------------------------------------- 1 | if (!process.env.BUILDER_PUBLIC_KEY) { 2 | throw new Error('Missing env varialbe BUILDER_PUBLIC_KEY') 3 | } 4 | 5 | export default { 6 | apiKey: process.env.BUILDER_PUBLIC_KEY, 7 | productsModel: 'swell-product', 8 | collectionsModel: 'swell-collection', 9 | isDemo: Boolean(process.env.IS_DEMO), 10 | } 11 | -------------------------------------------------------------------------------- /config/env.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | isDev: process.env.NODE_ENV === 'development', 3 | } 4 | -------------------------------------------------------------------------------- /config/seo.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Builder.io + Swell + Next.js", 3 | "titleTemplate": "%s - Headless Demo", 4 | "description": "A starter kit demo store for using headless Swell with Builder.io -> https://github.com/BuilderIO/nextjs-swell", 5 | "openGraph": { 6 | "type": "website", 7 | "locale": "en_IE", 8 | "url": "https://github.com/BuilderIO/nextjs-swell", 9 | "site_name": "Builder.io + Swell + Next.js" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /config/swell.ts: -------------------------------------------------------------------------------- 1 | if (!process.env.SWELL_STORE_ID) { 2 | throw new Error('Missing required environment variable SWELL_STORE_ID') 3 | } 4 | if (!process.env.SWELL_PUBLIC_KEY) { 5 | throw new Error( 6 | 'Missing required environment variable SWELL_PUBLIC_KEY' 7 | ) 8 | } 9 | 10 | export default { 11 | storeId: process.env.SWELL_STORE_ID, 12 | publicKey: process.env.SWELL_PUBLIC_KEY, 13 | } 14 | -------------------------------------------------------------------------------- /config/theme.ts: -------------------------------------------------------------------------------- 1 | import * as themes from '@theme-ui/presets' 2 | 3 | export default themes as any 4 | -------------------------------------------------------------------------------- /docs/ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Roadmap 2 | 3 | - Move to theme-ui or chakra-ui or any better more accessible framework than tailwind. 4 | -------------------------------------------------------------------------------- /docs/images/builder-io-organizations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swellstores/nextjs-builder/36db193c59ee789141185331d701bb61fbac9d8f/docs/images/builder-io-organizations.png -------------------------------------------------------------------------------- /docs/images/private-key-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swellstores/nextjs-builder/36db193c59ee789141185331d701bb61fbac9d8f/docs/images/private-key-flow.png -------------------------------------------------------------------------------- /docs/images/shopify-api-key-mapping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swellstores/nextjs-builder/36db193c59ee789141185331d701bb61fbac9d8f/docs/images/shopify-api-key-mapping.png -------------------------------------------------------------------------------- /docs/images/shopify-permissions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swellstores/nextjs-builder/36db193c59ee789141185331d701bb61fbac9d8f/docs/images/shopify-permissions.png -------------------------------------------------------------------------------- /docs/images/shopify-private-app-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swellstores/nextjs-builder/36db193c59ee789141185331d701bb61fbac9d8f/docs/images/shopify-private-app-flow.png -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | // Declarations for modules without types 2 | declare module 'next-themes' 3 | -------------------------------------------------------------------------------- /lib/click-outside/click-outside.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect, MouseEvent } from 'react' 2 | import hasParent from './has-parent' 3 | 4 | interface ClickOutsideProps { 5 | active: boolean 6 | onClick: (e?: MouseEvent) => void 7 | children: any 8 | } 9 | 10 | const ClickOutside = ({ 11 | active = true, 12 | onClick, 13 | children, 14 | }: ClickOutsideProps) => { 15 | const innerRef = useRef() 16 | 17 | const handleClick = (event: any) => { 18 | if (!hasParent(event.target, innerRef?.current)) { 19 | if (typeof onClick === 'function') { 20 | onClick(event) 21 | } 22 | } 23 | } 24 | 25 | useEffect(() => { 26 | if (active) { 27 | document.addEventListener('mousedown', handleClick) 28 | document.addEventListener('touchstart', handleClick) 29 | } 30 | 31 | return () => { 32 | if (active) { 33 | document.removeEventListener('mousedown', handleClick) 34 | document.removeEventListener('touchstart', handleClick) 35 | } 36 | } 37 | }) 38 | 39 | return React.cloneElement(children, { ref: innerRef }) 40 | } 41 | 42 | export default ClickOutside 43 | -------------------------------------------------------------------------------- /lib/click-outside/has-parent.js: -------------------------------------------------------------------------------- 1 | import isInDOM from './is-in-dom' 2 | 3 | export default function hasParent(element, root) { 4 | return root && root.contains(element) && isInDOM(element) 5 | } 6 | -------------------------------------------------------------------------------- /lib/click-outside/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './click-outside' 2 | -------------------------------------------------------------------------------- /lib/click-outside/is-in-dom.js: -------------------------------------------------------------------------------- 1 | export default function isInDom(obj) { 2 | return Boolean(obj.closest('body')) 3 | } 4 | -------------------------------------------------------------------------------- /lib/colors.ts: -------------------------------------------------------------------------------- 1 | import random from 'lodash.random' 2 | 3 | export function getRandomPairOfColors() { 4 | const colors = ['#37B679', '#DA3C3C', '#3291FF', '#7928CA', '#79FFE1'] 5 | const getRandomIdx = () => random(0, colors.length - 1) 6 | let idx = getRandomIdx() 7 | let idx2 = getRandomIdx() 8 | 9 | // Has to be a different color 10 | while (idx2 === idx) { 11 | idx2 = getRandomIdx() 12 | } 13 | 14 | // Returns a pair of colors 15 | return [colors[idx], colors[idx2]] 16 | } 17 | 18 | function hexToRgb(hex: string = '') { 19 | // @ts-ignore 20 | const match = hex.toString(16).match(/[a-f0-9]{6}|[a-f0-9]{3}/i) 21 | 22 | if (!match) { 23 | return [0, 0, 0] 24 | } 25 | 26 | let colorString = match[0] 27 | 28 | if (match[0].length === 3) { 29 | colorString = colorString 30 | .split('') 31 | .map((char: string) => { 32 | return char + char 33 | }) 34 | .join('') 35 | } 36 | 37 | const integer = parseInt(colorString, 16) 38 | const r = (integer >> 16) & 0xff 39 | const g = (integer >> 8) & 0xff 40 | const b = integer & 0xff 41 | 42 | return [r, g, b] 43 | } 44 | 45 | export function isDark(color = '') { 46 | // Equation from http://24ways.org/2010/calculating-color-contrast 47 | const rgb = hexToRgb(color.toLowerCase()) 48 | const res = (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000 49 | return res < 128 50 | } 51 | -------------------------------------------------------------------------------- /lib/defaults.ts: -------------------------------------------------------------------------------- 1 | // Fallback to CMS Data 2 | 3 | export const defatultPageProps = { 4 | header: { 5 | links: [ 6 | { 7 | link: { 8 | title: 'New Arrivals', 9 | url: '/', 10 | }, 11 | }, 12 | ], 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /lib/get-layout-props.ts: -------------------------------------------------------------------------------- 1 | import { resolveSwellContent } from './resolve-swell-content' 2 | 3 | export async function getLayoutProps(targetingAttributes?: any) { 4 | const theme = await resolveSwellContent('theme', targetingAttributes) 5 | 6 | return { 7 | theme: theme || null, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/hooks/useAcceptCookies.ts: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | import { useEffect, useState } from 'react' 3 | 4 | const COOKIE_NAME = 'accept_cookies' 5 | 6 | export const useAcceptCookies = () => { 7 | const [acceptedCookies, setAcceptedCookies] = useState(true) 8 | 9 | useEffect(() => { 10 | if (!Cookies.get(COOKIE_NAME)) { 11 | setAcceptedCookies(false) 12 | } 13 | }, []) 14 | 15 | const acceptCookies = () => { 16 | setAcceptedCookies(true) 17 | Cookies.set(COOKIE_NAME, 'accepted', { expires: 365 }) 18 | } 19 | 20 | return { 21 | acceptedCookies, 22 | onAcceptCookies: acceptCookies, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/logger.ts: -------------------------------------------------------------------------------- 1 | import bunyan from 'bunyan' 2 | import PrettyStream from 'bunyan-prettystream' 3 | 4 | const prettyStdOut = new PrettyStream() 5 | 6 | const log = bunyan.createLogger({ 7 | name: 'Next.js - Commerce', 8 | level: 'debug', 9 | streams: [ 10 | { 11 | level: 'debug', 12 | type: 'raw', 13 | stream: prettyStdOut, 14 | }, 15 | ], 16 | }) 17 | 18 | export default log 19 | -------------------------------------------------------------------------------- /lib/range-map.ts: -------------------------------------------------------------------------------- 1 | export default function rangeMap(n: number, fn: (i: number) => any) { 2 | const arr = [] 3 | while (n > arr.length) { 4 | arr.push(fn(arr.length)) 5 | } 6 | return arr 7 | } 8 | -------------------------------------------------------------------------------- /lib/resolve-swell-content.ts: -------------------------------------------------------------------------------- 1 | import { builder, Builder } from '@builder.io/react' 2 | import { getAsyncProps } from '@builder.io/utils' 3 | import builderConfig from '@config/builder' 4 | import { 5 | getCollection, 6 | getProduct, 7 | } from './swell/storefront-data-hooks/src/api/operations-swell' 8 | builder.init(builderConfig.apiKey) 9 | Builder.isStatic = true 10 | 11 | export async function resolveSwellContent( 12 | modelName: string, 13 | targetingAttributes?: any 14 | ) { 15 | let page = await builder 16 | .get(modelName, { 17 | userAttributes: targetingAttributes, 18 | includeRefs: true, 19 | preview: modelName, 20 | cachebust: true, 21 | } as any) 22 | .toPromise() 23 | 24 | if (page) { 25 | return await getAsyncProps(page, { 26 | async ProductGrid(props) { 27 | let products: any[] = [] 28 | if (props.productsList) { 29 | const promises = props.productsList 30 | .map((entry: any) => entry.product) 31 | .filter((handle: string | undefined) => typeof handle === 'string') 32 | .map( 33 | async (handle: string) => 34 | await getProduct({ slug: handle }) 35 | ) 36 | products = await Promise.all(promises) 37 | } 38 | return { 39 | // resolve the query as `products` for ssr 40 | // used for example in ProductGrid.tsx as initialProducts 41 | products, 42 | } 43 | }, 44 | async CollectionBox(props) { 45 | let collection = props.collection 46 | if (collection && typeof collection === 'string') { 47 | collection = await getCollection(builderConfig, { 48 | handle: collection, 49 | }) 50 | } 51 | return { 52 | collection, 53 | } 54 | }, 55 | async ProductBox(props) { 56 | let product = props.product 57 | if (product && typeof product === 'string') { 58 | product = await getProduct({ 59 | slug: product, 60 | }) 61 | } 62 | return { 63 | product, 64 | } 65 | }, 66 | 67 | async ProductCollectionGrid({ collection }) { 68 | if (collection && typeof collection === 'string') { 69 | const { products } = await getCollection(builderConfig, { 70 | handle: collection, 71 | }) 72 | return { 73 | products, 74 | } 75 | } 76 | }, 77 | }) 78 | } 79 | return null 80 | } 81 | -------------------------------------------------------------------------------- /lib/swell/storefront-data-hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { CommerceProvider } from './src/CommerceProvider' 2 | export * from './src/hooks' 3 | -------------------------------------------------------------------------------- /lib/swell/storefront-data-hooks/src/CommerceProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import swell from 'swell-js'; 3 | import { Context } from './Context' 4 | import swellConfig from '@config/swell' 5 | import { Cart } from './types' 6 | import useSWR from 'swr' 7 | export interface CommerceProviderProps { 8 | children: React.ReactNode 9 | } 10 | 11 | export function CommerceProvider({ 12 | children, 13 | }: CommerceProviderProps) { 14 | 15 | useSWR('swell', async () => { 16 | await swell.init(swellConfig.storeId, swellConfig.publicKey) 17 | }) 18 | const [cart, setCart] = useState(null) 19 | 20 | return ( 21 | 28 | {children} 29 | 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /lib/swell/storefront-data-hooks/src/Context.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Cart } from './types' 3 | 4 | interface ContextShape { 5 | swell: any 6 | cart: Cart | null 7 | setCart: React.Dispatch> 8 | } 9 | 10 | export const Context = React.createContext({ 11 | swell: null, 12 | cart: null, 13 | setCart: () => { 14 | throw Error('You forgot to wrap this in a Provider object') 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /lib/swell/storefront-data-hooks/src/api/operations-swell.ts: -------------------------------------------------------------------------------- 1 | import swell from 'swell-js' 2 | import swellConfig from '@config/swell' 3 | import image from 'next/image'; 4 | import { ProductResult, Product, Image } from '@lib/swell/storefront-data-hooks/src/types' 5 | 6 | export interface BuillderConfig { 7 | apiKey: string 8 | productsModel: string 9 | collectionsModel: string 10 | isDemo?: boolean 11 | } 12 | 13 | export interface CollectionProductsQuery { 14 | handle: string 15 | limit?: number 16 | cursor?: string 17 | apiKey: string 18 | } 19 | 20 | function normalizeProduct(product: ProductResult): Product { 21 | const variants = product.variants?.results ?? []; 22 | const images = product.images?.map((image: Image) => ({ ...image, src: image.file.url})) ?? [] 23 | return { ...product, variants, images }; 24 | } 25 | 26 | function normalizeProducts(productResults: ProductResult[]): Product[] { 27 | return productResults.map((product) => { 28 | return normalizeProduct(product); 29 | }); 30 | } 31 | 32 | export async function searchProducts( 33 | searchString: string, 34 | limit = 100, 35 | offset = 0 36 | ) { 37 | 38 | await swell.init(swellConfig.storeId, swellConfig.publicKey) 39 | const products = await swell.products.list({ 40 | search: searchString, 41 | limit, 42 | }) 43 | return products?.results ? normalizeProducts(products?.results) : [] 44 | } 45 | 46 | 47 | export async function getAllProducts( 48 | // limit = 100, 49 | // offset = 0 50 | // TODO: add in these params 51 | ) { 52 | await swell.init(swellConfig.storeId, swellConfig.publicKey) 53 | 54 | const productResults = await swell.products.list() 55 | return productResults ? normalizeProducts(productResults?.results) : []; 56 | } 57 | 58 | export async function getAllProductPaths( 59 | limit?: number 60 | ): Promise { 61 | 62 | const products: any[] = await getAllProducts() 63 | return products?.map((entry: any) => entry.slug) || [] 64 | } 65 | 66 | export async function getProduct(options: { id?: string; slug?: string; withContent?: boolean } 67 | ) { 68 | await swell.init(swellConfig.storeId, swellConfig.publicKey) 69 | if (Boolean(options.id) === Boolean(options.slug)) { 70 | throw new Error('Either a slug or id is required') 71 | } 72 | 73 | const result = await swell.products.get(options.id || options.slug, { expand: ['variants']}); 74 | return result ? normalizeProduct(result) : null; 75 | } 76 | 77 | 78 | // TODO: add in collection functions 79 | 80 | export async function getAllCollections( 81 | config: BuillderConfig, 82 | limit = 20, 83 | offset = 0, 84 | fields?: string 85 | ) { 86 | await swell.init(swellConfig.storeId, swellConfig.publicKey) 87 | const categories = await swell.categories.list({ 88 | // limit 89 | }) 90 | return categories?.results 91 | } 92 | 93 | export async function getAllCollectionPaths( 94 | config: BuillderConfig, 95 | limit?: number 96 | ): Promise { 97 | const collections: any[] = await getAllCollections(config, limit) 98 | return collections?.map((entry) => entry.slug) || [] 99 | } 100 | 101 | export async function getCollection( 102 | config: BuillderConfig, 103 | options: { 104 | id?: string 105 | handle?: string 106 | productsQuery?: Omit 107 | } 108 | ) { 109 | if (Boolean(options.id) === Boolean(options.handle)) { 110 | throw new Error('Either a handle or id is required') 111 | } 112 | 113 | const query = options.id || options.handle; 114 | await swell.init(swellConfig.storeId, swellConfig.publicKey) 115 | const category = await swell.categories.get(query, { expand: ['products'] }) 116 | const products = category?.products?.results ? normalizeProducts(category?.products?.results) : [] 117 | // const { page, count } = products; 118 | // TODO: add pagination logic 119 | const hasNextPage = false; 120 | const nextPageCursor = null; 121 | 122 | return { 123 | ...category, 124 | products, 125 | nextPageCursor, 126 | hasNextPage, 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /lib/swell/storefront-data-hooks/src/api/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'swell-js'; -------------------------------------------------------------------------------- /lib/swell/storefront-data-hooks/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { useCart } from './useCart' 2 | export { useCartCount } from './useCartCount' 3 | export { useAddItemToCart } from './useAddItemToCart' 4 | export { useAddItemsToCart } from './useAddItemsToCart' 5 | export { useRemoveItemFromCart } from './useRemoveItemFromCart' 6 | export { useCartItems } from './useCartItems' 7 | export { useCheckoutUrl } from './useCheckoutUrl' 8 | export { useGetLineItem } from './useGetLineItem' 9 | export { useUpdateItemQuantity } from './useUpdateItemQuantity' 10 | -------------------------------------------------------------------------------- /lib/swell/storefront-data-hooks/src/hooks/useAddItemToCart.ts: -------------------------------------------------------------------------------- 1 | import { OptionInput } from '../types' 2 | import { useContext } from 'react' 3 | import { Context } from '../Context' 4 | 5 | export function useAddItemToCart() { 6 | const { swell, setCart } = useContext(Context) 7 | async function addItemToCart( 8 | product_id: string, 9 | quantity: number, 10 | options?: OptionInput[] 11 | ) { 12 | 13 | const newCart = await swell.cart.addItem({ 14 | product_id, 15 | quantity, 16 | options 17 | }) 18 | setCart(newCart) 19 | return newCart 20 | } 21 | 22 | return addItemToCart 23 | } 24 | -------------------------------------------------------------------------------- /lib/swell/storefront-data-hooks/src/hooks/useAddItemsToCart.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | import { Context } from '../Context' 3 | import { LineItemPatch } from '../types' 4 | 5 | export function useAddItemsToCart() { 6 | const { swell, setCart } = useContext(Context) 7 | 8 | async function addItemsToCart(items: LineItemPatch[]) { 9 | 10 | if (items.length < 1) { 11 | throw new Error( 12 | 'Must include at least one line item, empty line items found' 13 | ) 14 | } 15 | 16 | items.forEach((item) => { 17 | if (item.product_id == null) { 18 | throw new Error(`Missing productId in item`) 19 | } 20 | 21 | if (item.quantity == null) { 22 | throw new Error( 23 | `Missing quantity in item with product id: ${item.product_id}` 24 | ) 25 | } else if (typeof item.quantity != 'number') { 26 | throw new Error( 27 | `Quantity is not a number in item with product id: ${item.product_id}` 28 | ) 29 | } else if (item.quantity < 1) { 30 | throw new Error( 31 | `Quantity must not be less than one in item with product id: ${item.product_id}` 32 | ) 33 | } 34 | }) 35 | 36 | const newCart = await swell.cart.setItems(items) 37 | setCart(newCart); 38 | } 39 | 40 | return addItemsToCart 41 | } 42 | -------------------------------------------------------------------------------- /lib/swell/storefront-data-hooks/src/hooks/useCart.ts: -------------------------------------------------------------------------------- 1 | import { useContext, useState, useEffect } from 'react' 2 | import { Context } from '../Context' 3 | import { Cart } from '../types' 4 | 5 | export function useCart(): Cart | null { 6 | const { swell, cart, setCart } = useContext(Context) 7 | 8 | useEffect(() => { 9 | const fetchData = async () => { 10 | 11 | try { 12 | const result = await swell.cart.get(); 13 | setCart(result); 14 | } catch(error) {} 15 | } 16 | 17 | fetchData(); 18 | }, []) 19 | 20 | return cart; 21 | } 22 | -------------------------------------------------------------------------------- /lib/swell/storefront-data-hooks/src/hooks/useCartCount.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | import { Context } from '../Context' 3 | 4 | export async function useCartCount() { 5 | const { cart } = useContext(Context) 6 | if (cart == null || cart.item_quantity < 1) { 7 | return 0 8 | } 9 | 10 | return cart.item_quantity; 11 | } 12 | -------------------------------------------------------------------------------- /lib/swell/storefront-data-hooks/src/hooks/useCartItems.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | import { Context } from '../Context' 3 | 4 | export function useCartItems() { 5 | 6 | const { cart, setCart } = useContext(Context) 7 | 8 | if (!cart || !Array.isArray(cart.items)) { 9 | return 10 | } 11 | 12 | return cart?.items 13 | } 14 | -------------------------------------------------------------------------------- /lib/swell/storefront-data-hooks/src/hooks/useCheckoutUrl.ts: -------------------------------------------------------------------------------- 1 | import { useContext, useState, useEffect } from 'react' 2 | import { Context } from '../Context' 3 | 4 | export function useCheckoutUrl(): string | null { 5 | 6 | const { cart } = useContext(Context) 7 | if (!cart) { 8 | return null; 9 | } 10 | 11 | return cart?.checkout_url; 12 | } 13 | -------------------------------------------------------------------------------- /lib/swell/storefront-data-hooks/src/hooks/useGetLineItem.ts: -------------------------------------------------------------------------------- 1 | import { useCartItems } from './useCartItems' 2 | 3 | export function useGetLineItem() { 4 | const cartItems = useCartItems() 5 | 6 | function getLineItem(itemId: string | number): any | null { 7 | if (cartItems && cartItems.length < 1) { 8 | return null 9 | } 10 | 11 | const item = cartItems?.find((cartItem) => { 12 | // eslint-disable-next-line @typescript-eslint/ban-ts-ignore 13 | // @ts-ignore 14 | return cartItem.id === itemId 15 | }) 16 | 17 | if (item == null) { 18 | return null 19 | } 20 | 21 | return item 22 | } 23 | 24 | return getLineItem 25 | } -------------------------------------------------------------------------------- /lib/swell/storefront-data-hooks/src/hooks/useRemoveItemFromCart.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | import { Context } from '../Context' 3 | 4 | export function useRemoveItemFromCart() { 5 | const { swell, setCart } = useContext(Context) 6 | async function removeItemFromCart(itemId: number | string) { 7 | if (itemId === '' || itemId == null) { 8 | throw new Error('ItemId must not be blank or null') 9 | } 10 | const newCart = await swell.cart.removeItem(itemId); 11 | setCart(newCart) 12 | } 13 | 14 | return removeItemFromCart 15 | } 16 | -------------------------------------------------------------------------------- /lib/swell/storefront-data-hooks/src/hooks/useUpdateItemQuantity.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | import { Context } from '../Context' 3 | 4 | import { useGetLineItem } from './useGetLineItem' 5 | 6 | export function useUpdateItemQuantity() { 7 | const { swell, setCart } = useContext(Context) 8 | const getLineItem = useGetLineItem() 9 | 10 | async function updateItemQuantity( 11 | itemId: string | number, 12 | quantity: number 13 | ) { 14 | if (itemId == null) { 15 | throw new Error('Must provide an item id') 16 | } 17 | 18 | if (quantity == null || Number(quantity) < 0) { 19 | throw new Error('Quantity must be greater than 0') 20 | } 21 | 22 | const lineItem = getLineItem(itemId) 23 | 24 | if (lineItem == null) { 25 | throw new Error(`Item with id ${itemId} not in cart`) 26 | } 27 | let newCart; 28 | if (quantity == 0 || Number(quantity) == 0) { 29 | newCart = await swell.cart.removeItem(itemId) 30 | } else { 31 | newCart = await swell.cart.updateItem(itemId, { quantity }) 32 | } 33 | setCart(newCart); 34 | } 35 | 36 | return updateItemQuantity 37 | } 38 | -------------------------------------------------------------------------------- /lib/swell/storefront-data-hooks/src/types.ts: -------------------------------------------------------------------------------- 1 | export interface OptionInput { 2 | [key: string]: string 3 | } 4 | 5 | export interface LineItemPatch { 6 | product_id: string 7 | quantity: number 8 | options?: OptionInput[] 9 | } 10 | 11 | export interface Product { 12 | id: string 13 | description: string 14 | name: string 15 | slug: string 16 | currency: string 17 | price: number 18 | images: any[] 19 | options: ProductOption[] 20 | variants: Variant[] 21 | } 22 | 23 | interface VariantResults { 24 | results: Variant[] 25 | } 26 | export interface ProductResult { 27 | id: string 28 | description: string 29 | name: string 30 | slug: string 31 | currency: string 32 | price: number 33 | options: ProductOption[] 34 | images: any[] 35 | variants: VariantResults 36 | } 37 | 38 | export interface Variant { 39 | id: string 40 | option_value_ids: string[] 41 | name: string 42 | price?: number 43 | stock_status?: string 44 | } 45 | 46 | 47 | export type CartItem = { 48 | id: string 49 | product: Product 50 | price: number 51 | variant: { 52 | name?: string 53 | sku?: string 54 | id: string 55 | } 56 | quantity: number 57 | } 58 | 59 | export type Cart = { 60 | id: string 61 | account_id: number 62 | currency: string 63 | tax_included_total: number 64 | sub_total: number 65 | tax_total: number 66 | shipment_total: number 67 | grand_total: number 68 | discount_total: number 69 | quantity: number 70 | items: CartItem[] 71 | item_quantity: number 72 | checkout_url: string 73 | date_created: string 74 | discounts?: { id: number; amount: number }[] | null 75 | } 76 | 77 | export type Image = { 78 | file: { 79 | url: String 80 | height: Number 81 | width: Number 82 | } 83 | id: string 84 | } 85 | 86 | export type OptionValue = { 87 | id: string 88 | name: string 89 | value: string 90 | } 91 | 92 | export type ProductOption = { 93 | id: string 94 | name: string 95 | values: OptionValue[] 96 | } 97 | -------------------------------------------------------------------------------- /lib/swell/storefront-data-hooks/src/utils/product.ts: -------------------------------------------------------------------------------- 1 | import { Product } from '@lib/swell/storefront-data-hooks/src/types' 2 | 3 | export function prepareVariantsWithOptions( 4 | product: Product 5 | ) { 6 | return product.variants.map((variant) => { 7 | const optionsDictionary = variant.option_value_ids?.reduce((optionValues: any, optionId: string) => { 8 | product.options.find((option) => { 9 | const matchingOptionValue = option.values.find(value => { 10 | return value?.id === optionId 11 | }) 12 | if (matchingOptionValue) { 13 | optionValues[`${option?.name?.toLowerCase()}`] = matchingOptionValue?.name 14 | } 15 | }); 16 | return optionValues 17 | }, {}); 18 | return { 19 | ...optionsDictionary, 20 | ...variant, 21 | } 22 | }) as any[] 23 | } 24 | 25 | export const getPrice = (price: string | number | undefined, currency: string) => 26 | Intl.NumberFormat(undefined, { 27 | currency, 28 | minimumFractionDigits: 2, 29 | style: 'currency', 30 | }).format(parseFloat(price ? price + '' : '0')) 31 | 32 | export function prepareVariantsImages( 33 | variants: any[], 34 | optionKey: any 35 | ): any[] { 36 | const imageDictionary = variants.reduce( 37 | (images, variant) => { 38 | if (variant[optionKey] && variant.images) { 39 | images[variant[optionKey]] = variant.images[0].file.url 40 | } 41 | return images 42 | }, 43 | {} 44 | ) 45 | 46 | const images = Object.keys(imageDictionary).map((key) => { 47 | return { 48 | [optionKey]: key, 49 | src: imageDictionary[key] ?? 'https://via.placeholder.com/1050x1050', 50 | } 51 | }) 52 | 53 | 54 | return images 55 | } 56 | -------------------------------------------------------------------------------- /lib/to-pixels.ts: -------------------------------------------------------------------------------- 1 | // Convert numbers or strings to pixel value 2 | // Helpful for styled-jsx when using a prop 3 | // height: ${toPixels(height)}; (supports height={20} and height="20px") 4 | 5 | const toPixels = (value: string | number) => { 6 | if (typeof value === 'number') { 7 | return `${value}px` 8 | } 9 | 10 | return value 11 | } 12 | 13 | export default toPixels 14 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Vercel, Inc. 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 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const bundleAnalyzer = require('@next/bundle-analyzer')({ 2 | enabled: !!process.env.BUNDLE_ANALYZE, 3 | }) 4 | 5 | module.exports = bundleAnalyzer({ 6 | target: 'serverless', 7 | images: { 8 | domains: ['cdn.builder.io', 'cdn.schema.io', 'via.placeholder.com'], 9 | }, 10 | async headers() { 11 | return [ 12 | { 13 | source: '/:path*', 14 | headers: [ 15 | { 16 | key: 'Content-Security-Policy', 17 | value: 18 | 'frame-ancestors https://*.builder.io https://builder.io http://localhost:1234', 19 | }, 20 | ], 21 | }, 22 | ] 23 | }, 24 | env: { 25 | // expose env to the browser 26 | BUILDER_PUBLIC_KEY: process.env.BUILDER_PUBLIC_KEY, 27 | SWELL_STORE_ID: process.env.SWELL_STORE_ID, 28 | SWELL_PUBLIC_KEY: process.env.SWELL_PUBLIC_KEY, 29 | IS_DEMO: process.env.IS_DEMO, 30 | } 31 | }) 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-commerce", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "dev": "next dev --port=3004", 6 | "build": "next build", 7 | "start": "next start --port=3005", 8 | "analyze": "BUNDLE_ANALYZE=both yarn build", 9 | "find:unused": "next-unused", 10 | "prettier": "prettier" 11 | }, 12 | "prettier": { 13 | "semi": false, 14 | "singleQuote": true 15 | }, 16 | "next-unused": { 17 | "alias": { 18 | "@lib/*": [ 19 | "lib/*" 20 | ], 21 | "@assets/*": [ 22 | "assets/*" 23 | ], 24 | "@config/*": [ 25 | "config/*" 26 | ], 27 | "@components/*": [ 28 | "components/*" 29 | ], 30 | "@utils/*": [ 31 | "utils/*" 32 | ] 33 | }, 34 | "debug": true, 35 | "include": [ 36 | "components", 37 | "lib", 38 | "pages", 39 | "sections" 40 | ], 41 | "exclude": [], 42 | "entrypoints": [ 43 | "pages" 44 | ] 45 | }, 46 | "dependencies": { 47 | "@builder.io/react": "^1.1.44", 48 | "@builder.io/utils": "^1.0.3", 49 | "@builder.io/widgets": "^1.2.19", 50 | "@netlify/plugin-nextjs": "^3.1.0-experimental-odb.2", 51 | "@reach/portal": "^0.11.2", 52 | "@tailwindcss/ui": "^0.6.2", 53 | "@testing-library/react-hooks": "^3.7.0", 54 | "@theme-ui/components": "^0.6.2", 55 | "@theme-ui/match-media": "^0.7.2", 56 | "@theme-ui/preset-base": "^0.6.0", 57 | "@theme-ui/presets": "^0.6.2", 58 | "@types/body-scroll-lock": "^2.6.1", 59 | "@types/lodash.throttle": "^4.1.6", 60 | "@types/qs": "^6.9.5", 61 | "@types/react-sticky": "^6.0.3", 62 | "@types/traverse": "^0.6.32", 63 | "@vercel/fetch": "^6.1.0", 64 | "atob": "^2.1.2", 65 | "body-scroll-lock": "^3.1.5", 66 | "bowser": "^2.11.0", 67 | "cheerio": "^1.0.0-rc.6", 68 | "classnames": "^2.2.6", 69 | "css-color-names": "^1.0.1", 70 | "email-validator": "^2.0.4", 71 | "jest": "^26.6.3", 72 | "js-cookie": "^2.2.1", 73 | "keen-slider": "^5.2.4", 74 | "lodash.random": "^3.2.0", 75 | "lodash.throttle": "^4.1.1", 76 | "next": "^10.1.2", 77 | "next-seo": "^4.11.0", 78 | "next-themes": "^0.0.4", 79 | "postcss-nesting": "^7.0.1", 80 | "pure-react-carousel": "^1.27.6", 81 | "qs": "^6.9.6", 82 | "react": "^17.0.2", 83 | "react-dom": "^17.0.2", 84 | "react-intersection-observer": "^8.30.1", 85 | "react-json-tree": "^0.13.0", 86 | "react-merge-refs": "^1.1.0", 87 | "react-spring": "^9.1.1", 88 | "react-spring-modal": "^2.0.7", 89 | "react-sticky": "^6.0.3", 90 | "react-ticker": "^1.2.2", 91 | "swell-js": "^3.10.7", 92 | "swr": "^1.0.1", 93 | "tailwindcss": "^1.9", 94 | "theme-ui": "^0.6.2", 95 | "traverse": "^0.6.6" 96 | }, 97 | "devDependencies": { 98 | "@next/bundle-analyzer": "^10.0.1", 99 | "@types/atob": "^2.1.2", 100 | "@types/bunyan": "^1.8.6", 101 | "@types/bunyan-prettystream": "^0.1.31", 102 | "@types/classnames": "^2.2.10", 103 | "@types/js-cookie": "^2.2.6", 104 | "@types/lodash.random": "^3.2.6", 105 | "@types/node": "^14.11.2", 106 | "@types/react": "^16.9.49", 107 | "bunyan": "^1.8.14", 108 | "bunyan-prettystream": "^0.1.3", 109 | "next-unused": "^0.0.3", 110 | "postcss-flexbugs-fixes": "^4.2.1", 111 | "postcss-preset-env": "^6.7.0", 112 | "prettier": "^2.1.2", 113 | "typescript": "^4.0.3" 114 | }, 115 | "resolutions": { 116 | "webpack": "^5.0.0-beta.30" 117 | }, 118 | "license": "MIT" 119 | } 120 | -------------------------------------------------------------------------------- /pages/[[...path]].tsx: -------------------------------------------------------------------------------- 1 | import type { 2 | GetStaticPathsContext, 3 | GetStaticPropsContext, 4 | InferGetStaticPropsType, 5 | } from 'next' 6 | import { NextSeo } from 'next-seo' 7 | import { useRouter } from 'next/router' 8 | import { Layout } from '@components/common' 9 | import { BuilderComponent, Builder, builder } from '@builder.io/react' 10 | import builderConfig from '@config/builder' 11 | import DefaultErrorPage from 'next/error' 12 | import Head from 'next/head' 13 | import { resolveSwellContent } from '@lib/resolve-swell-content' 14 | 15 | builder.init(builderConfig.apiKey) 16 | import '../blocks/ProductGrid/ProductGrid.builder' 17 | import '../blocks/CollectionView/CollectionView.builder' 18 | import { useThemeUI } from '@theme-ui/core' 19 | import { Link } from '@components/ui' 20 | import { Themed } from '@theme-ui/mdx' 21 | import { getLayoutProps } from '@lib/get-layout-props' 22 | 23 | const isProduction = process.env.NODE_ENV === 'production' 24 | 25 | export async function getStaticProps({ 26 | params, 27 | }: GetStaticPropsContext<{ path: string[] }>) { 28 | const page = await resolveSwellContent('page', { 29 | urlPath: '/' + (params?.path?.join('/') || ''), 30 | }) 31 | 32 | return { 33 | props: { 34 | page, 35 | ...(await getLayoutProps()), 36 | }, 37 | // Next.js will attempt to re-generate the page: 38 | // - When a request comes in 39 | // - At most once every 30 seconds 40 | revalidate: 30, 41 | } 42 | } 43 | 44 | export async function getStaticPaths({ locales }: GetStaticPathsContext) { 45 | const pages = await builder.getAll('page', { 46 | options: { noTargeting: true }, 47 | apiKey: builderConfig.apiKey, 48 | }) 49 | 50 | return { 51 | paths: pages.map((page) => `${page.data?.url}`), 52 | fallback: true, 53 | } 54 | } 55 | 56 | export default function Path({ 57 | page, 58 | }: InferGetStaticPropsType) { 59 | const router = useRouter() 60 | const { theme } = useThemeUI() 61 | if (router.isFallback) { 62 | return

    Loading...

    63 | } 64 | // This includes setting the noindex header because static files always return a status 200 but the rendered not found page page should obviously not be indexed 65 | if (!page && !Builder.isEditing && !Builder.isPreviewing) { 66 | return ( 67 | <> 68 | 69 | 70 | 71 | 72 | {Builder.isBrowser && } 73 | 74 | ) 75 | } 76 | 77 | const { title, description, image } = page?.data! || {} 78 | Builder.isStatic = true; 79 | return ( 80 |
    81 | {title && ( 82 | 101 | )} 102 | { 107 | // nextjs link doesn't handle hash links well if it's on the same page (starts with #) 108 | if (props.target === '_blank' || props.href?.startsWith('#') ) { 109 | return 110 | } 111 | return 112 | }} 113 | {...(page && { content: page })} 114 | /> 115 |
    116 | ) 117 | } 118 | 119 | Path.Layout = Layout 120 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '@assets/main.css' 2 | import 'keen-slider/keen-slider.min.css' 3 | 4 | import { FC } from 'react' 5 | import type { AppProps } from 'next/app' 6 | 7 | import { builder, Builder } from '@builder.io/react' 8 | import builderConfig from '@config/builder' 9 | builder.init(builderConfig.apiKey) 10 | 11 | import '../blocks/ProductGrid/ProductGrid.builder' 12 | import '../blocks/CollectionView/CollectionView.builder' 13 | import '../blocks/ProductView/ProductView.builder' 14 | 15 | 16 | Builder.register('insertMenu', { 17 | name: 'Swell Collection Components', 18 | items: [ 19 | { name: 'CollectionBox', label: 'Collection' }, 20 | { name: 'CollectionView' }, 21 | { name: 'ProductCollectionGrid' }, 22 | ], 23 | }) 24 | 25 | Builder.register('insertMenu', { 26 | name: 'Swell Products Components', 27 | items: [ 28 | { name: 'ProductView' }, 29 | { name: 'ProductBox' }, 30 | { name: 'ProductGrid' } 31 | ], 32 | }) 33 | 34 | const Noop: FC = ({ children }) => <>{children} 35 | 36 | export default function MyApp({ Component, pageProps }: AppProps) { 37 | const Layout = (Component as any).Layout || Noop 38 | 39 | return ( 40 | <> 41 | 42 | 43 | 44 | 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { 2 | Html, 3 | Head, 4 | Main, 5 | NextScript, 6 | DocumentContext, 7 | } from 'next/document' 8 | import cheerio from 'cheerio' 9 | 10 | /** 11 | * See this issue for more details https://github.com/emotion-js/emotion/issues/2040 12 | * Theme-ui using emotion which render styles inside template tags causing it not to apply when rendering 13 | * A/B test variations on the server, this fixes this issue by extracting those styles and appending them to body 14 | */ 15 | const extractABTestingStyles = (body: string) => { 16 | let globalStyles = '' 17 | 18 | if (body.includes(' { 22 | const str = $(element).html() 23 | const styles = cheerio.load(String(str))('style') 24 | globalStyles += styles 25 | .toArray() 26 | .map((el) => $(el).html()) 27 | .join(' ') 28 | }) 29 | } 30 | return globalStyles 31 | } 32 | 33 | class MyDocument extends Document { 34 | static async getInitialProps(ctx: DocumentContext) { 35 | const originalRenderPage = ctx.renderPage 36 | 37 | let globalStyles = '' 38 | ctx.renderPage = async (options) => { 39 | const render = await originalRenderPage(options) 40 | globalStyles = extractABTestingStyles(render.html) 41 | return render 42 | } 43 | const initialProps = await Document.getInitialProps(ctx) 44 | return { 45 | ...initialProps, 46 | globalStyles, 47 | } 48 | } 49 | render() { 50 | return ( 51 | 52 | 53 | 54 | 59 |
    60 | 61 | 62 | 63 | ) 64 | } 65 | } 66 | 67 | export default MyDocument 68 | -------------------------------------------------------------------------------- /pages/cart.tsx: -------------------------------------------------------------------------------- 1 | import { CartSidebarView } from '@components/cart' 2 | import { Layout } from '@components/common' 3 | const Cart = () => 4 | export default Cart 5 | Cart.Layout = Layout 6 | -------------------------------------------------------------------------------- /pages/collection/[handle].tsx: -------------------------------------------------------------------------------- 1 | import type { 2 | GetStaticPathsContext, 3 | GetStaticPropsContext, 4 | InferGetStaticPropsType, 5 | } from 'next' 6 | import { useRouter } from 'next/router' 7 | import { Layout } from '@components/common' 8 | import { BuilderComponent, Builder, builder } from '@builder.io/react' 9 | import { resolveSwellContent } from '@lib/resolve-swell-content' 10 | import builderConfig from '@config/builder' 11 | import { 12 | getCollection, 13 | getAllCollectionPaths, 14 | } from '@lib/swell/storefront-data-hooks/src/api/operations-swell' 15 | import DefaultErrorPage from 'next/error' 16 | import Head from 'next/head' 17 | import { useThemeUI } from '@theme-ui/core' 18 | import { getLayoutProps } from '@lib/get-layout-props' 19 | 20 | builder.init(builderConfig.apiKey!) 21 | Builder.isStatic = true 22 | 23 | const builderModel = 'collection-page' 24 | 25 | export async function getStaticProps({ 26 | params, 27 | }: GetStaticPropsContext<{ handle: string }>) { 28 | const collection = await getCollection(builderConfig, { 29 | handle: params?.handle, 30 | }) 31 | 32 | const page = await resolveSwellContent(builderModel, { 33 | collectionHandle: params?.handle, 34 | }) 35 | 36 | return { 37 | props: { 38 | page: page || null, 39 | collection: collection || null, 40 | ...(await getLayoutProps()), 41 | }, 42 | } 43 | } 44 | 45 | export async function getStaticPaths({ locales }: GetStaticPathsContext) { 46 | const paths = await getAllCollectionPaths(builderConfig) 47 | return { 48 | paths: paths.map((path) => `/collection/${path}`), 49 | fallback: 'blocking', 50 | } 51 | } 52 | 53 | export default function Handle({ 54 | collection, 55 | page, 56 | }: InferGetStaticPropsType) { 57 | const router = useRouter() 58 | const isLive = !Builder.isEditing && !Builder.isPreviewing 59 | const { theme } = useThemeUI() 60 | if (!collection && isLive) { 61 | return ( 62 | <> 63 | 64 | 65 | 66 | 67 | 68 | 69 | ) 70 | } 71 | 72 | return router.isFallback && isLive ? ( 73 |

    Loading...

    // TODO (BC) Add Skeleton Views 74 | ) : ( 75 | 82 | ) 83 | } 84 | 85 | Handle.Layout = Layout 86 | -------------------------------------------------------------------------------- /pages/product/[handle].tsx: -------------------------------------------------------------------------------- 1 | import type { 2 | GetStaticPathsContext, 3 | GetStaticPropsContext, 4 | InferGetStaticPropsType, 5 | } from 'next' 6 | import { useRouter } from 'next/router' 7 | import { Layout } from '@components/common' 8 | import { BuilderComponent, Builder, builder } from '@builder.io/react' 9 | import { resolveSwellContent } from '@lib/resolve-swell-content' 10 | import '../../blocks/ProductView/ProductView.builder' 11 | import builderConfig from '@config/builder' 12 | import { 13 | getAllProductPaths, 14 | getProduct, 15 | } from '@lib/swell/storefront-data-hooks/src/api/operations-swell' 16 | import DefaultErrorPage from 'next/error' 17 | import Head from 'next/head' 18 | import { useThemeUI } from 'theme-ui' 19 | import { getLayoutProps } from '@lib/get-layout-props' 20 | builder.init(builderConfig.apiKey!) 21 | Builder.isStatic = true 22 | 23 | const builderModel = 'product-page' 24 | 25 | export async function getStaticProps(context: GetStaticPropsContext<{ handle: string }>) { 26 | const product = await getProduct({ 27 | slug: context.params?.handle, 28 | }) 29 | const page = await resolveSwellContent(builderModel, { 30 | productHandle: context.params?.handle, 31 | }) 32 | 33 | return { 34 | props: { 35 | page: page || null, 36 | product: product || null, 37 | ...(await getLayoutProps()), 38 | }, 39 | } 40 | } 41 | 42 | export async function getStaticPaths({ locales }: GetStaticPathsContext) { 43 | const paths = await getAllProductPaths() 44 | return { 45 | // TODO: update to /product 46 | paths: paths?.map((path) => `/product/${path}`) ?? [], 47 | fallback: 'blocking', 48 | } 49 | } 50 | 51 | export default function Handle({ 52 | product, 53 | page, 54 | }: InferGetStaticPropsType) { 55 | Builder.isStatic = true 56 | const router = useRouter() 57 | const isLive = !Builder.isEditing && !Builder.isPreviewing 58 | const { theme } = useThemeUI() 59 | // This includes setting the noindex header because static files always return a status 200 but the rendered not found page page should obviously not be indexed 60 | if (!product && isLive) { 61 | return ( 62 | <> 63 | 64 | 65 | 66 | 67 | 68 | 69 | ) 70 | } 71 | 72 | return router.isFallback && isLive ? ( 73 |

    Loading...

    // TODO (BC) Add Skeleton Views 74 | ) : ( 75 | 82 | ) 83 | } 84 | 85 | Handle.Layout = Layout 86 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | 'tailwindcss', 4 | 'postcss-nesting', 5 | 'postcss-flexbugs-fixes', 6 | [ 7 | 'postcss-preset-env', 8 | { 9 | autoprefixer: { 10 | flexbox: 'no-2009', 11 | }, 12 | stage: 2, 13 | features: { 14 | 'custom-properties': false, 15 | }, 16 | }, 17 | ], 18 | ], 19 | } 20 | -------------------------------------------------------------------------------- /public/bg-products.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /public/cursor-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swellstores/nextjs-builder/36db193c59ee789141185331d701bb61fbac9d8f/public/cursor-left.png -------------------------------------------------------------------------------- /public/cursor-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swellstores/nextjs-builder/36db193c59ee789141185331d701bb61fbac9d8f/public/cursor-right.png -------------------------------------------------------------------------------- /public/flag-en-us.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/flag-es-ar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /public/flag-es-co.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /public/flag-es.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swellstores/nextjs-builder/36db193c59ee789141185331d701bb61fbac9d8f/public/icon-144x144.png -------------------------------------------------------------------------------- /public/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swellstores/nextjs-builder/36db193c59ee789141185331d701bb61fbac9d8f/public/icon-192x192.png -------------------------------------------------------------------------------- /public/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swellstores/nextjs-builder/36db193c59ee789141185331d701bb61fbac9d8f/public/icon-512x512.png -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swellstores/nextjs-builder/36db193c59ee789141185331d701bb61fbac9d8f/public/icon.png -------------------------------------------------------------------------------- /public/jacket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swellstores/nextjs-builder/36db193c59ee789141185331d701bb61fbac9d8f/public/jacket.png -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Next.js Commerce", 3 | "short_name": "Next.js Commerce", 4 | "description": "Next.js Commerce -> https://www.nextjs.org/commerce", 5 | "display": "standalone", 6 | "start_url": "/", 7 | "theme_color": "#fff", 8 | "background_color": "#000000", 9 | "orientation": "portrait", 10 | "icons": [ 11 | { 12 | "src": "/icon-192x192.png", 13 | "type": "image/png", 14 | "sizes": "192x192" 15 | }, 16 | { 17 | "src": "/icon-512x512.png", 18 | "type": "image/png", 19 | "sizes": "512x512" 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /public/slider-arrows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swellstores/nextjs-builder/36db193c59ee789141185331d701bb61fbac9d8f/public/slider-arrows.png -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | future: { 3 | purgeLayersByDefault: true, 4 | applyComplexClasses: true, 5 | }, 6 | purge: { 7 | content: [ 8 | './pages/**/*.{js,ts,jsx,tsx}', 9 | './blocks/**/*.{js,ts,jsx,tsx}', 10 | './components/**/*.{js,ts,jsx,tsx}', 11 | ], 12 | }, 13 | theme: { 14 | extend: { 15 | maxWidth: { 16 | '8xl': '1920px', 17 | }, 18 | colors: { 19 | primary: 'var(--primary)', 20 | 'primary-2': 'var(--primary-2)', 21 | secondary: 'var(--secondary)', 22 | 'secondary-2': 'var(--secondary-2)', 23 | hover: 'var(--hover)', 24 | 'hover-1': 'var(--hover-1)', 25 | 'hover-2': 'var(--hover-2)', 26 | 'accents-0': 'var(--accents-0)', 27 | 'accents-1': 'var(--accents-1)', 28 | 'accents-2': 'var(--accents-2)', 29 | 'accents-3': 'var(--accents-3)', 30 | 'accents-4': 'var(--accents-4)', 31 | 'accents-5': 'var(--accents-5)', 32 | 'accents-6': 'var(--accents-6)', 33 | 'accents-7': 'var(--accents-7)', 34 | 'accents-8': 'var(--accents-8)', 35 | 'accents-9': 'var(--accents-9)', 36 | violet: 'var(--violet)', 37 | 'violet-light': 'var(--violet-light)', 38 | pink: 'var(--pink)', 39 | cyan: 'var(--cyan)', 40 | blue: 'var(--blue)', 41 | green: 'var(--green)', 42 | red: 'var(--red)', 43 | }, 44 | textColor: { 45 | base: 'var(--text-base)', 46 | primary: 'var(--text-primary)', 47 | secondary: 'var(--text-secondary)', 48 | }, 49 | boxShadow: { 50 | 'outline-2': '0 0 0 2px var(--accents-2)', 51 | magical: 52 | 'rgba(0, 0, 0, 0.02) 0px 30px 30px, rgba(0, 0, 0, 0.03) 0px 0px 8px, rgba(0, 0, 0, 0.05) 0px 1px 0px', 53 | }, 54 | lineHeight: { 55 | 'extra-loose': '2.2', 56 | }, 57 | scale: { 58 | 120: '1.2', 59 | }, 60 | }, 61 | }, 62 | plugins: [require('@tailwindcss/ui')], 63 | } 64 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "esnext", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "paths": { 18 | "@lib/*": ["lib/*"], 19 | "@assets/*": ["assets/*"], 20 | "@config/*": ["config/*"], 21 | "@components/*": ["components/*"], 22 | "@utils/*": ["utils/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"], 26 | "exclude": ["node_modules"] 27 | } 28 | --------------------------------------------------------------------------------