├── .gitignore ├── README.md ├── assets └── frontend.png ├── data └── production.tar.gz ├── package.json ├── renovate.json ├── sanity-template.json └── template ├── .gitignore ├── .npmrc ├── README.md ├── client.js ├── components ├── articlePane.tsx ├── breadcrumbs.tsx ├── cart.tsx ├── cartProductDisplay.tsx ├── globalStyle.ts ├── index.ts ├── indexArticleGrid.tsx ├── indexFeaturePane.tsx ├── listItemCard.tsx ├── listItemGroup.tsx ├── navbar.tsx ├── productCardFeature.tsx ├── productDisplay.tsx ├── productsDisplay.tsx ├── responsiveFixedRatioImage.tsx ├── shopGrid.tsx ├── shopTheStory.tsx ├── socialBar.tsx ├── solidBlockFeature.tsx ├── subsectionBar.tsx ├── textOverlayFeature.tsx └── textUnderFeature.tsx ├── contexts └── bigcommerce-context.js ├── env.example ├── lerna.json ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── [hub] │ ├── [subhub] │ │ ├── [slug].tsx │ │ └── index.tsx │ └── index.tsx ├── _app.tsx ├── _document.js ├── api │ └── bigcommerce.js ├── index.tsx └── shop │ ├── [slug].tsx │ ├── campaign │ └── [slug].tsx │ └── index.tsx ├── public ├── blank.png ├── favicon.ico └── vercel.svg ├── studio ├── README.md ├── config │ ├── .checksums │ └── @sanity │ │ ├── data-aspects.json │ │ ├── default-layout.json │ │ ├── default-login.json │ │ ├── form-builder.json │ │ └── vision.json ├── env.example ├── package.json ├── plugins │ └── .gitkeep ├── sanity.json ├── schemas │ ├── documents │ │ ├── article.js │ │ ├── campaign.js │ │ ├── category.js │ │ ├── person.js │ │ ├── product.js │ │ ├── route.js │ │ ├── siteSettings.js │ │ └── subsection.js │ ├── objects │ │ └── excerptPortableText.js │ ├── pages │ │ ├── hr.js │ │ ├── listItem.js │ │ ├── productCardFeature.js │ │ ├── productsDisplay.js │ │ ├── solidBlockFeature.js │ │ └── textOverlayFeature.js │ ├── schema.js │ └── utils.js ├── src │ ├── bigCommerceSync.js │ ├── deskStructure.js │ ├── initialValueTemplates.js │ └── preview.js ├── tsconfig.json └── yarn.lock ├── styles ├── Home.module.css └── globals.css ├── theme ├── color.ts ├── fonts.ts ├── index.ts └── theme.ts ├── tsconfig.json ├── types.d.ts └── utils ├── helpers.js ├── sanity.js └── sanityGroqQueries.js /.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 | static 12 | 13 | # next.js 14 | .next 15 | out 16 | 17 | #dont embed data in repo 18 | *.ndjson 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # local env files 33 | .env.local 34 | .env.development.local 35 | .env.development 36 | .env.test.local 37 | .env.production.local 38 | 39 | # vercel 40 | .vercel 41 | 42 | #vim 43 | .swp 44 | .swo 45 | 46 | #data 47 | *.ndjson 48 | 49 | template/public/studio 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BigCommerce / Next.js Starter 2 | 3 | This starter is built to showcase a mix of editorial and e-commerce, taking advantage of page-building components, BigCommerce data integration, and internationalization tooling. 4 | 5 | ![](https://raw.githubusercontent.com/sanity-io/sanity-template-bigcommerce-editorial/main/assets/frontend.png 'A frontend screenshot of this starter') 6 | 7 | ## BigCommerce 8 | 9 | [BigCommerce](https://bigcommerce.com) is a leading software-as-a-service (SaaS) ecommerce platform that empowers merchants of all sizes to build, innovate and grow their businesses online. As a leading open SaaS solution, BigCommerce provides merchants sophisticated enterprise-grade functionality, customization and performance with simplicity and ease-of-use. 10 | 11 | ## Table of contents 12 | 13 | - [Features](#features) 14 | - [Getting started](#getting-started) 15 | - [Importing Data](#importing-data) 16 | - [Internationalization](#internationalization) 17 | - [Contributing](#contributing) 18 | - [License](#license) 19 | 20 | ## Features 21 | 22 | - Styled with [Sanity UI](https://sanity.io/ui), an ergonomic React library for quickly building and prototyping accessible web apps. 23 | - Cart powered by [BigCommerce](https://bigcommerce.com) merchant APIs. 24 | - BigCommerce products in the studio, and a script to pull just the data you need from BigCommerce's GraphQL endpoint. 25 | - Rich content building in articles, product detail pages, and campaign pages in the Sanity Studio. 26 | - I18n support. 27 | - Vercel deployment. 28 | 29 | ## Getting started 30 | 31 | The quickest way to get up and running is to go to https://www.sanity.io/create?template=sanity-io/sanity-template-bigcommerce-editorial and create a new project by following the instructions on Sanity. 32 | 33 | You can also clone this repo and do some configuration locally -- we'll guide you through the steps on importing your data! 34 | 35 | ### Installation guide 36 | 37 | 1. Clone this repository ([learn how to do this](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository)). 38 | 39 | 2. Be sure you have Sanity installed! Run `npm install -g @sanity/cli` if you don't. 40 | 41 | 3. If you came from the one-click starter, add your own `projectTitle`, `projectId,` and `dataset` in `/studio/sanity.json`. You can also find these on [manage.sanity.io](https://manage.sanity.io). If you're starting from cloning to this repo, run `sanity init` in that `/studio` folder. 42 | 43 | 4. Add CORS origins in your settings for this studio at [manage.sanity.io](https://manage.sanity.io), at least for `localhost:3000` (The one-click starter should have added this for you if that's how you started). If you had the one-click starter, also add whatever Vercel URL is created. Remember to check 'Allow Credentials'! 44 | 45 | 5. Ensure your studio is ready to run locally by running the following. 46 | 47 | ```bash 48 | npm install && cd /studio && sanity install 49 | ``` 50 | 51 | 5. Get set up with BigCommerce. If you don't have an account, start one. Then go to Advanced Settings/API Accounts. I'd recommend 2 separate, specific tokens, since you'll be interfacing with the API in two different, potentially sensitive ways. One should have a 'Cart' scope and the other should have Storefront API Tokens and Products scope. For the rest of this readme I'll refer to them as 'cart token' and 'import token'. 52 | 53 | 6. Populate your environment variables. There is an `env.example` file in the main folder, and another in the Sanity studio. Rename them to `.env.development`. Here's some tips on filling out the main file: 54 | 55 | - `SANITY_API_TOKEN=`if you don't have one, set one up on manage.sanity.io! 56 | - `NEXT_PUBLIC_SANITY_DATASET`=This came from the last step -- you usually want 'production' 57 | - `NEXT_PUBLIC_SANITY_PROJECT_ID=` This also came from the last step -- you can also always find this on manage.sanity.io. 58 | - `BIGCOMMERCE_API_TOKEN=` This is your cart token from BigCommerce. 59 | - `BIGCOMMERCE_API_URL=` It's usually like https://api.bigcommerce.com/stores/{your store hash}/v3. See below for tips on finding your store hash! 60 | 61 | 62 | Here are the guidelines for the studio `.env.development` file: 63 | 64 | - `SANITY_STUDIO_BIGCOMMERCE_STORE_HASH=` You can find this anywhere you're logged into your BigCommerce account -- for example, if the URL in my browser is https://store-rix57ghiz3.mybigcommerce.com/manage/dashboard, "rix57ghiz3" is the value I should put here. 65 | - `SANITY_STUDIO_BIGCOMMERCE_STORE_API_TOKEN=` This is the "import" token you made in the last step. 66 | 67 | It's also worthwhile to add these to your Vercel environment! 68 | 69 | 7. Be sure you have `concurrently` installed (`npm install -g concurrently`). Run the command below to start the development server: 70 | 71 | ```bash 72 | npm run dev 73 | ``` 74 | 75 | This will run the frontend at `localhost:3000` and studio at `localhost:3333`. 76 | 77 | ## Importing data 78 | 79 | 1. If you set up from the Sanity starters page, you don't need to import the base data. If you started by cloning this repo, then extract the `production.tar.gz` file in `/data` directory with: 80 | 81 | ```bash 82 | tar -xf production.tar.gz 83 | ``` 84 | 85 | This will provide you with a folder like `production-export-xxx`. Then go to your /studio folder and run `sanity dataset import {production-export-xxxfolder}/data.ndjson` 86 | 87 | 88 | 2. Now that your keys and base data are all set, you can import data from BigCommerce! Go to your studio folder and run `sanity exec src/bigCommerceSync.js`. If you receive undefined errors for any of the environment variables, try setting your sanity env with `export SANITY_ACTIVE_ENV=development` from the command line. 89 | 90 | When the script completes successfully, it will also provide you with an `data.ndjson` file. Go ahead and import that as well (the two imports should coexist happily together in the same dataset!) 91 | 92 | ## Internationalization 93 | 94 | I18n is currently only available on product pages, as a guideline. Set the "override" fields in products in Sanity and use Next.js routing (e.g., `localhost:3000/fr/shop/product-slug` to see the French version of a product. (These are already provided for you in the "sample" products that come with the starter!) 95 | 96 | ## License 97 | 98 | This repository is published under the [MIT](LICENSE) license. 99 | 100 | -------------------------------------------------------------------------------- /assets/frontend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanity-io/sanity-template-bigcommerce-editorial/208dce0bd84e57953cc11fc01e4c47e62f2a715f/assets/frontend.png -------------------------------------------------------------------------------- /data/production.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanity-io/sanity-template-bigcommerce-editorial/208dce0bd84e57953cc11fc01e4c47e62f2a715f/data/production.tar.gz -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sanity-template-bigcommerce-editorial", 3 | "version": "1.0.0", 4 | "description": "A lifestyle blog with editorial material and e-commerce functionality. Built with Next.js and Sanity, and featuring integration with BigCommerce, embedded calls to purchasing action, and international routing", 5 | "main": "index.js", 6 | "scripts": { 7 | "watch": "sanity-template build && cp .env.local.test build/.env.local && sanity-template watch --template-values template-values.json", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC" 13 | } 14 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "github>sanity-io/renovate-presets:sanity-template" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /sanity-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "title": "Lifestyled: Editorial and E-Commerce", 4 | "description": "A lifestyle blog with editorial material and e-commerce functionality. Built with BigCommerce, Next.js and Sanity, it features rich content, embedded calls to purchasing action, and international routing", 5 | "previewMedia": { 6 | "type": "image", 7 | "src": "assets/frontend.png", 8 | "alt": "Next.js frontend with Sanity data displaying" 9 | }, 10 | "technologies": [ 11 | { 12 | "id": "vercel", 13 | "name": "Vercel", 14 | "url": "https://vercel.com/" 15 | }, 16 | { 17 | "id": "nextjs", 18 | "name": "Next.js", 19 | "url": "https://nextjs.org" 20 | }, 21 | { 22 | "id": "bigcommerce", 23 | "name": "BigCommerce", 24 | "url": "https://bigcommerce.com" 25 | } 26 | ], 27 | "deployment": { 28 | "provider": "vercel", 29 | "studio": { "basePath": "/studio" }, 30 | "envVars": { 31 | "projectId": ["NEXT_PUBLIC_SANITY_PROJECT_ID"], 32 | "dataset": ["NEXT_PUBLIC_SANITY_DATASET"] 33 | }, 34 | "tokens": [ 35 | { 36 | "label": "livePreview", 37 | "role": "write", 38 | "envVar": "SANITY_API_TOKEN" 39 | } 40 | ], 41 | "corsOrigins": [ 42 | { 43 | "origin": "http://localhost:3000", 44 | "allowCredentials": true 45 | } 46 | ] 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /template/.gitignore: -------------------------------------------------------------------------------- 1 | .vercel 2 | -------------------------------------------------------------------------------- /template/.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true 2 | -------------------------------------------------------------------------------- /template/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | -------------------------------------------------------------------------------- /template/client.js: -------------------------------------------------------------------------------- 1 | import sanityClient from '@sanity/client' 2 | 3 | export default sanityClient({ 4 | dataset: process.env.NEXT_PUBLIC_SANITY_DATASET, 5 | projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID, 6 | useCdn: false // `false` if you want to ensure fresh data 7 | }) 8 | -------------------------------------------------------------------------------- /template/components/articlePane.tsx: -------------------------------------------------------------------------------- 1 | import {Heading, Stack, Box } from '@sanity/ui' 2 | import {Article} from '../types' 3 | import Link from 'next/link' 4 | import { urlFor } from '$utils/sanity' 5 | 6 | export function ArticlePane({article} : {article: Article}) { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {article.title} 16 | 17 | 18 | 19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /template/components/breadcrumbs.tsx: -------------------------------------------------------------------------------- 1 | import { Article } from '../types' 2 | import Link from 'next/link' 3 | import { Box, Text } from '@sanity/ui' 4 | 5 | export function Breadcrumbs({article}: {article: Article}) { 6 | 7 | return ( 8 | 9 | 10 | 11 | 12 | { `${article.category.name} >>` } 13 | 14 | 15 | { ` ${article.subsection.name} >>` } 16 | 17 | 18 | { ` ${article.title} ` } 19 | 20 | 21 | 22 | 23 | ) 24 | } 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /template/components/cart.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Dialog, Box, Text } from '@sanity/ui' 3 | 4 | import { useStore, useToggleCart } from '../contexts/bigcommerce-context' 5 | import { CartProductDisplay } from '$components' 6 | 7 | export const Cart = () => { 8 | const { isCartOpen, cart } = useStore() 9 | const toggleCart = useToggleCart() 10 | let cartDisplay; 11 | 12 | const products = cart.line_items.map((product, i) => ( 13 | 17 | )) 18 | 19 | if (isCartOpen) { 20 | cartDisplay = ( 21 | 27 | 29 | { products } 30 | 31 | 33 | 34 | Cart Total: ${cart.total} 35 | 36 | 37 | 38 | ) 39 | } else { 40 | cartDisplay = 41 | } 42 | 43 | return ( 44 | 45 | {cartDisplay} 46 | 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /template/components/cartProductDisplay.tsx: -------------------------------------------------------------------------------- 1 | import { Text, Box, Inline, Heading, Button } from '@sanity/ui' 2 | import { urlFor } from '$utils/sanity' 3 | import { ResponsiveFixedRatioImage } from '$components' 4 | import { BsTrash2 } from 'react-icons/bs' 5 | import { Product } from '../types' 6 | import { useDeleteItem } from '../contexts/bigcommerce-context' 7 | 8 | export function CartProductDisplay({product}: {product: Product}) { 9 | 10 | const deleteItem = useDeleteItem() 11 | 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {product.manufacturer}
21 |
22 | 23 | 24 | {product.name}
25 |
26 | 27 | ${product.price}
28 |
29 | 30 | Quantity: {product.quantity}
31 |
32 |
33 |
34 | 35 | 41 | 42 |
43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /template/components/globalStyle.ts: -------------------------------------------------------------------------------- 1 | import {Theme} from '@sanity/ui' 2 | import {createGlobalStyle, css} from 'styled-components' 3 | 4 | export const GlobalStyle = createGlobalStyle((props: {theme: Theme}) => { 5 | // 6 | //TODO: control in studio? 7 | const {theme} = props 8 | const colorBase = theme.sanity.color.base 9 | const color = {fg: colorBase.fg, bg: "#FCFCFF"} 10 | 11 | 12 | return css` 13 | 14 | html, 15 | body, 16 | #__next { 17 | height: 100%; 18 | } 19 | 20 | body { 21 | background-color: ${color.bg}; 22 | color: ${color.fg}; 23 | -webkit-font-smoothing: antialiased; 24 | margin: 0; 25 | } 26 | 27 | ` 28 | }) 29 | -------------------------------------------------------------------------------- /template/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './globalStyle' 2 | export * from './indexArticleGrid' 3 | export * from './indexFeaturePane' 4 | export * from './articlePane' 5 | export * from './subsectionBar' 6 | export * from './navbar' 7 | export * from './solidBlockFeature' 8 | export * from './listItemGroup' 9 | export * from './listItemCard' 10 | export * from './productDisplay' 11 | export * from './productsDisplay' 12 | export * from './shopGrid' 13 | export * from './socialBar' 14 | export * from './breadcrumbs' 15 | export * from './textUnderFeature' 16 | export * from './textOverlayFeature' 17 | export * from './productCardFeature' 18 | export * from './cart' 19 | export * from './responsiveFixedRatioImage' 20 | export * from './shopTheStory' 21 | export * from './cartProductDisplay' 22 | -------------------------------------------------------------------------------- /template/components/indexArticleGrid.tsx: -------------------------------------------------------------------------------- 1 | import { Grid, Box } from '@sanity/ui' 2 | import { Feature } from '../types' 3 | import { urlFor } from '$utils/sanity' 4 | import { IndexFeaturePane } from '$components' 5 | 6 | export function IndexArticleGrid({features}: {features: Feature[]}) { 7 | 8 | return ( 9 | 10 | 15 | 16 | 17 | 18 | 19 | { features[1] && ( 20 | 21 | ) 22 | } 23 | 24 | { features[2] && ( 25 | 26 | ) 27 | } 28 | 29 | 30 | 31 | ) 32 | 33 | } 34 | -------------------------------------------------------------------------------- /template/components/indexFeaturePane.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import { Heading, Button } from '@sanity/ui' 3 | import { Feature } from '../types' 4 | import { urlFor } from '$utils/sanity' 5 | import styled from 'styled-components' 6 | 7 | const PaneContainer = styled.div` 8 | height: 100%; 9 | width: 100%; 10 | background: black; 11 | overflow: hidden; 12 | position: relative; 13 | ` 14 | 15 | const PaneImage = styled.img` 16 | height: 100%; 17 | width: 100%; 18 | object-fit: cover; 19 | opacity: 0.65; 20 | ` 21 | 22 | const OverlayText = styled.div` 23 | color: white; 24 | position: absolute; 25 | top: 20%; 26 | left: 5%; 27 | width: 50%; 28 | ` 29 | 30 | export function IndexFeaturePane({feature, headingSize}: {feature: Feature, headingSize: number[]}) { 31 | let imageUrl: string = "/blank.png" 32 | if (feature.image && feature.image.asset._ref) { 33 | imageUrl = urlFor(feature.image).url() as string 34 | } 35 | 36 | return ( 37 | 38 | 39 | 40 | 41 | 42 | { feature.title } 43 | 44 | 45 | 46 | 47 | ) 48 | 49 | } 50 | -------------------------------------------------------------------------------- /template/components/listItemCard.tsx: -------------------------------------------------------------------------------- 1 | import { Heading, Text, Box} from '@sanity/ui' 2 | import { ListItem } from '../types' 3 | import { ProductsDisplay } from '$components' 4 | import { urlFor, PortableText } from '$utils/sanity' 5 | 6 | export function ListItemCard({item, groupParent} 7 | : {item: ListItem, groupParent: boolean}) { 8 | 9 | let display; 10 | 11 | if (item.products) { 12 | if (item.orientation == 'horizontal' || !groupParent) { 13 | if (item.productDisplaySize == 'small') { 14 | display = () 15 | } else { 16 | display = ( 17 | <> 18 | 19 | 20 | 21 | ) 22 | } 23 | } else { 24 | display = ( 25 | <> 26 | 27 | 28 | 29 | ) 30 | } 31 | } else { 32 | display = ( 33 | ) 34 | } 35 | 36 | return ( 37 | 38 | {item.title} 39 | 40 | { display } 41 | 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /template/components/listItemGroup.tsx: -------------------------------------------------------------------------------- 1 | import {Box, Grid} from '@sanity/ui' 2 | import { ListItem } from '../types' 3 | import { ListItemCard } from '$components' 4 | 5 | export function ListItemGroup({listItems}: {listItems: ListItem[]}) { 6 | const cols = (listItems.length >= 4) ? 4 : listItems.length 7 | return ( 8 | 9 | 10 | { listItems.map((item, j) => ( 11 | )) 12 | } 13 | 14 | 15 | ) 16 | 17 | } 18 | -------------------------------------------------------------------------------- /template/components/navbar.tsx: -------------------------------------------------------------------------------- 1 | import {Button, Card, Box, Stack, Inline, Heading, Flex} from '@sanity/ui' 2 | import { MdShoppingCart } from 'react-icons/md' 3 | import {Category} from '../types' 4 | import Link from 'next/link' 5 | import { useToggleCart } from '../contexts/bigcommerce-context' 6 | 7 | export function NavBar({categories, selectedCategoryName} 8 | : {categories: Category[], selectedCategoryName?: String}) { 9 | 10 | //TODO: add cartCount 11 | const toggleCart = useToggleCart() 12 | 13 | const shopButton = ( 14 | 15 |