├── .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 | 
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 |
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 |
16 |
17 | )
18 |
19 | const navButtons = [shopButton, ...categories.map((category, i) => (
20 | //TODO: use MenuButton and Menu to cover subcategories
21 |
22 |
23 |
24 | ))]
25 |
26 | return (
27 |
28 |
29 |
30 |
31 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | Lifestyled.
40 |
41 |
42 |
43 |
44 | { navButtons }
45 |
46 |
47 |
48 |
49 |
50 | )
51 | }
52 |
53 |
--------------------------------------------------------------------------------
/template/components/productCardFeature.tsx:
--------------------------------------------------------------------------------
1 | import {Heading, Box, Flex, Text, Button} from '@sanity/ui'
2 | import { Product } from '../types'
3 | import { ProductsDisplay } from '$components'
4 | import { PortableText } from '$utils/sanity'
5 |
6 | export function ProductCardFeature(
7 | {title, text, products}: {title: string, text: any | any[], products: Product[]}) {
8 | return (
9 |
10 |
11 | {title}
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | )
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/template/components/productDisplay.tsx:
--------------------------------------------------------------------------------
1 | import { Stack, Flex, Text, Box } from '@sanity/ui'
2 | import { urlFor } from '$utils/sanity'
3 | import { ResponsiveFixedRatioImage } from '$components'
4 | import Link from 'next/link'
5 |
6 | export function ProductDisplay({product, displayHorizontal, shopNow, width}
7 | : {product: any, displayHorizontal: boolean, shopNow: boolean, width: number}) {
8 |
9 | const imgBox = (
10 |
11 |
12 |
13 | )
14 |
15 | const productInfo = (
16 |
17 |
18 |
19 | {product.manufacturer}
20 |
21 |
22 |
23 | {product.name}
24 |
25 |
26 |
27 | ${product.price}
28 |
29 |
30 |
31 |
32 | { (shopNow) ? "Shop now" : "More..." }
33 |
34 |
35 |
36 |
37 | )
38 |
39 | if (displayHorizontal) {
40 | return (
41 |
42 | { imgBox }
43 | { productInfo }
44 |
45 | )
46 | } else {
47 | return (
48 |
49 |
50 | { imgBox }
51 |
52 | { productInfo }
53 |
54 |
55 |
56 | )
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/template/components/productsDisplay.tsx:
--------------------------------------------------------------------------------
1 | import { Flex, Stack, Grid, Text, Box } from '@sanity/ui'
2 | import { ProductDisplay } from '$components'
3 | import { urlFor, PortableText } from '$utils/sanity'
4 | import Link from 'next/link'
5 | import { Product } from '../types'
6 |
7 | export function ProductsDisplay({products, fullSize, copy}
8 | : {products: Product[], fullSize: boolean, copy: any[] | any}) {
9 | let displayHorizontal: boolean;
10 | let productDisplays;
11 |
12 | if (!products) { return }
13 |
14 | if (fullSize) {
15 |
16 | //if there's only one product and products are on their own row,
17 | //put its info beside it to fill up space
18 | displayHorizontal = (products.length == 1)
19 |
20 | productDisplays = products.map((product, i) => (
21 | ))
27 |
28 |
29 | //4 products get a grid, fewer get a flex
30 | if (products.length < 4) {
31 | return ( {productDisplays} )
32 | } else {
33 | return ( {productDisplays.slice(0, 4)} )
34 | }
35 | }
36 |
37 |
38 | //products not on own row
39 | else {
40 | //if there's multiple products, put its info beside it to avoid filling vertical space
41 | displayHorizontal = (products.length > 1)
42 | productDisplays = products.map((product, i) => (
43 | ))
49 |
50 | const finalDisplay = {productDisplays.slice(0,3)}
51 |
52 | if (copy && typeof(copy) != 'undefined') {
53 | return (
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | {finalDisplay}
62 |
63 |
64 | )
65 | } else {
66 | return ( {finalDisplay} )
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/template/components/responsiveFixedRatioImage.tsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export function ResponsiveFixedRatioImage({imageUrl}: {imageUrl: string}) {
4 | return (
5 |
10 |

20 |
21 |
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/template/components/shopGrid.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Grid, Heading } from '@sanity/ui'
2 | import { Product } from '../types'
3 | import { ProductDisplay } from '$components'
4 |
5 |
6 | export function ShopGrid({sectionTitle, products}
7 | : {sectionTitle: string, products: Product[]}) {
8 |
9 | return (
10 |
11 |
12 | {sectionTitle}
13 |
14 |
15 | { products.map(prod => (
16 | )) }
22 |
23 |
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/template/components/shopTheStory.tsx:
--------------------------------------------------------------------------------
1 | import { Grid, Heading, Card, Box } from '@sanity/ui'
2 | import { StoryProducts } from '../types'
3 | import { ProductDisplay } from '$components'
4 |
5 | export function ShopTheStory({products}: {products: StoryProducts[]}) {
6 |
7 | //have to unnest out of containing blocks
8 | const productDisplays = products.filter(elem => elem.products).map(elem => (
9 | elem.products.map((product, i) => (
10 |
16 | ))
17 | ))
18 |
19 | return (
20 |
21 | Shop It Now!
22 |
23 |
24 | { productDisplays }
25 |
26 |
27 |
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/template/components/socialBar.tsx:
--------------------------------------------------------------------------------
1 | import { FaPinterest, FaFacebook, FaRegEnvelope } from 'react-icons/fa'
2 | import { Inline, Text } from '@sanity/ui'
3 |
4 | export function SocialBar() {
5 | return (
6 |
8 | Share:
9 |
10 |
11 |
12 |
13 |
14 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/template/components/solidBlockFeature.tsx:
--------------------------------------------------------------------------------
1 | import {Heading, Box, Flex, Text, Button} from '@sanity/ui'
2 | import { Color, Image } from '../types'
3 | import Link from 'next/link'
4 | import { urlFor, PortableText } from '$utils/sanity'
5 |
6 | export function SolidBlockFeature(
7 | {title, text, image, url, orientation, textColor, blockColor}
8 | : {title: string, text: any | any[], image: Image, url: string, orientation: string,
9 | textColor: Color, blockColor: Color}) {
10 |
11 | const solidBlockStyle = { align: "center",
12 | backgroundColor: (blockColor ? blockColor.hex :"#32021f"),
13 | color: (textColor ? textColor.hex : 'white'),
14 | minWidth: '350px' }
15 |
16 | const solidBlock = (
17 |
19 |
20 |
21 |
22 | { title }
23 |
24 |
25 |
26 |
27 |
28 | { url ?
29 | (
30 |
36 | ) : <>>
37 | }
38 |
39 | )
40 |
41 | const imageBlock = (
42 |
43 |
45 |
46 | )
47 |
48 | let content;
49 |
50 | if (orientation == 'right') {
51 | content = [imageBlock, solidBlock]
52 | } else {
53 | content = [solidBlock, imageBlock]
54 | }
55 |
56 | return (
57 |
58 | { content }
59 |
60 | )
61 | }
62 |
--------------------------------------------------------------------------------
/template/components/subsectionBar.tsx:
--------------------------------------------------------------------------------
1 | import {Container, Heading, Card, Flex, Grid } from '@sanity/ui'
2 | import {SubsectionArticles} from '../types'
3 | import Link from 'next/link'
4 | import { ArticlePane } from '$components'
5 |
6 | export function SubsectionBar({hub, subsectionArticles}
7 | : {hub: string, subsectionArticles: SubsectionArticles}) {
8 |
9 | const articlePanes = subsectionArticles.articles.map((article, i) => (
10 | )
11 | )
12 |
13 | const heading = ({ subsectionArticles.name })
14 |
15 | return (
16 |
17 |
18 | { (subsectionArticles.slug && subsectionArticles.slug != 'undefined') ?
19 | (
20 | { heading }
21 | ) :
22 | <>{ heading }>
23 | }
24 |
25 |
26 | { articlePanes }
27 |
28 |
29 |
30 | )
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/template/components/textOverlayFeature.tsx:
--------------------------------------------------------------------------------
1 | import {Heading, Box, Flex, Text, Button} from '@sanity/ui'
2 | import {Article, Image} from '../types'
3 | import Link from 'next/link'
4 | import { urlFor, PortableText } 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 TextOverlayFeature({title, text, image, url, fullSize}
31 | : {title: string, text: any | any[], image: Image, url: string, fullSize: boolean}) {
32 |
33 | const imageUrl = ((fullSize) ?
34 | urlFor(image).url() : urlFor(image).height(600).url())!!
35 |
36 | return (
37 |
38 |
39 |
40 |
41 |
42 | {title}
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | { url ?
51 | (
52 |
58 | ) : <>>
59 | }
60 |
61 |
62 |
63 |
64 | )
65 | }
66 |
--------------------------------------------------------------------------------
/template/components/textUnderFeature.tsx:
--------------------------------------------------------------------------------
1 | import {Heading, Stack, Box, Flex, Text, Button} from '@sanity/ui'
2 | import {Article} from '../types'
3 | import Link from 'next/link'
4 | import { urlFor, PortableText } from '$utils/sanity'
5 |
6 | export function TextUnderFeature({title, text, image, url}
7 | : {title: string, text: any | any[], image: string, url: string}) {
8 |
9 | return (
10 |
11 |
12 |
15 |
16 |
17 |
18 |
19 |
20 | {title}
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | { url ?
30 | (
31 |
37 | ) : <>>
38 | }
39 |
40 |
41 |
42 |
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/template/contexts/bigcommerce-context.js:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, useEffect, useState } from 'react'
2 | import { getClient } from '../utils/sanity'
3 | import { singleProductQuery } from '../utils/sanityGroqQueries'
4 |
5 | const BC_CART_ID = 'bc_cart_id'
6 |
7 | const initialStoreState = {
8 | isCartOpen: false,
9 | isUpdating: false,
10 | cart: {
11 | id: null,
12 | line_items: [],
13 | total: 0
14 | }
15 | }
16 |
17 | const BigCommerceContext = createContext({
18 | store: initialStoreState,
19 | setStore: () => null
20 | })
21 |
22 | /* ---------- */
23 | /* Actions */
24 | /* ---------- */
25 |
26 | const setCartState = async (cart, setStore, openCart) => {
27 |
28 | // Save cart to localstorage
29 | if (typeof window !== `undefined` && typeof cart !== 'undefined') {
30 | localStorage.setItem(BC_CART_ID, cart.id)
31 | }
32 |
33 | //fetch full product data from Sanity
34 | const line_items = await Promise.all(
35 | Object.values(cart.line_items).map(async itemCategory => (
36 | await Promise.all(itemCategory.map(async item => {
37 | const sanityID = `imported-BC-${item.product_id}`
38 | const sanityProduct = await getClient().fetch(singleProductQuery(sanityID))
39 | return { ...sanityProduct, quantity: item.quantity, lineID: item.id }
40 | }))
41 | ))
42 | )
43 |
44 | // update state
45 | setStore((prevState) => {
46 | return {
47 | ...prevState,
48 | isUpdating: false,
49 | isCartOpen: openCart ? true : prevState.isCartOpen,
50 | cart: {
51 | id: cart.id,
52 | line_items: line_items.flat(),
53 | total: cart.cart_amount
54 | },
55 | }
56 | })
57 | }
58 |
59 |
60 | /* ------------------ */
61 | /* Context Wrapper */
62 | /* ------------------ */
63 |
64 | const BigCommerceContextProvider = ({ children }) => {
65 | const [store, setStore] = useState(initialStoreState)
66 | const [initStore, setInitStore] = useState(false)
67 |
68 | useEffect(() => {
69 |
70 | if (initStore === false) {
71 | const initializeCart = async () => {
72 | const existingCartID =
73 | typeof window !== 'undefined'
74 | ? localStorage.getItem(BC_CART_ID)
75 | : false
76 |
77 | if (existingCartID && existingCartID != 'undefined' && existingCartID != 'null') {
78 | try {
79 | // fetch cart from BC
80 | const existingCart = await fetch(`/api/bigcommerce?cartID=${existingCartID}`)
81 | .then(res => res.json())
82 | .then(res => res.data)
83 |
84 | setCartState(existingCart, setStore)
85 |
86 | return
87 |
88 | //TODO: in a real world context, you probably want to check if
89 | //all line items are available, the cart is not already purchased, etc.
90 |
91 | } catch (e) {
92 | //endpoint came back with error, remove the invalid cart id from localStorage
93 | localStorage.setItem(BC_CART_ID, null)
94 | }
95 | }
96 |
97 | //if no cart id, create a new cart
98 | const newCart = await fetch('/api/bigcommerce', {method: 'POST'})
99 | .then(res => res.json())
100 | .then(res => res.data)
101 |
102 | setCartState(newCart, setStore)
103 | }
104 |
105 | initializeCart()
106 | setInitStore(true)
107 | }
108 | }, [initStore, store, setStore])
109 |
110 | return (
111 |
116 | {children}
117 |
118 | )
119 | }
120 |
121 |
122 | /* ------------------ */
123 | /* Context Helpers */
124 | /* ------------------ */
125 | function useStore() {
126 | const { store } = useContext(BigCommerceContext)
127 | return store
128 | }
129 |
130 | function useToggleCart() {
131 | const {
132 | store: { isCartOpen },
133 | setStore,
134 | } = useContext(BigCommerceContext)
135 |
136 | async function toggleCart() {
137 | setStore((prevState) => {
138 | return { ...prevState, isCartOpen: !isCartOpen }
139 | })
140 | }
141 | return toggleCart
142 | }
143 |
144 |
145 | // Add an item to the checkout cart
146 | function useAddItem() {
147 | const {
148 | store: { cart },
149 | setStore,
150 | } = useContext(BigCommerceContext)
151 |
152 | async function addItem(sanityProductID) {
153 |
154 | //start update process
155 | setStore((prevState) => {
156 | return { ...prevState, isUpdating: true }
157 | })
158 |
159 | //make valid input for the add line item endpoint
160 | const BCItem = {
161 | //variant_id is required by BC products with variants
162 | //be sure to add it in your use case!
163 | product_id: parseInt(sanityProductID.replace('imported-BC-', '')),
164 | quantity: 1
165 | }
166 |
167 |
168 | // Add it to the BC cart
169 | const newCart = await fetch(`/api/bigcommerce?cartID=${cart.id}`, {
170 | method: 'PUT',
171 | body: JSON.stringify({line_items: [BCItem]})
172 | })
173 | .then(res => res.json())
174 | .then(res => res.data)
175 |
176 | // Update our global store states
177 | setCartState(newCart, setStore, true)
178 | }
179 |
180 | return addItem
181 |
182 | }
183 |
184 | // Add an item to the checkout cart
185 | function useDeleteItem() {
186 | const {
187 | store: { cart },
188 | setStore,
189 | } = useContext(BigCommerceContext)
190 |
191 | async function deleteItem(sanityProductID) {
192 |
193 | //start update process
194 | setStore((prevState) => {
195 | return { ...prevState, isUpdating: true }
196 | })
197 |
198 | //transform ID again
199 | const bigCommerceID = sanityProductID.replace('imported-BC-', '')
200 |
201 | // Remove it from the BC cart
202 | const newCart = await fetch(`/api/bigcommerce?cartID=${cart.id}&itemID=${bigCommerceID}`, {
203 | method: 'DELETE'
204 | })
205 | .then(res => res.json())
206 | .then(res => res.data)
207 |
208 | // Update our global store states
209 | setCartState(newCart, setStore, true)
210 | }
211 |
212 | return deleteItem
213 |
214 | }
215 |
216 | export {
217 | BigCommerceContextProvider,
218 | useStore,
219 | useToggleCart,
220 | useAddItem,
221 | useDeleteItem
222 | }
223 |
--------------------------------------------------------------------------------
/template/env.example:
--------------------------------------------------------------------------------
1 | SANITY_API_TOKEN=
2 | NEXT_PUBLIC_SANITY_DATASET=
3 | NEXT_PUBLIC_SANITY_PROJECT_ID=
4 | BIGCOMMERCE_API_TOKEN=
5 | BIGCOMMERCE_API_URL=
6 |
--------------------------------------------------------------------------------
/template/lerna.json:
--------------------------------------------------------------------------------
1 | {
2 | "packages": [
3 | "studio"
4 | ],
5 | "version": "0.0.0"
6 | }
7 |
--------------------------------------------------------------------------------
/template/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/template/next.config.js:
--------------------------------------------------------------------------------
1 | const STUDIO_REWRITE = {
2 | source: '/studio/:path*',
3 | destination:
4 | process.env.NODE_ENV === 'development'
5 | ? 'http://localhost:3333/studio/:path*'
6 | : '/studio/index.html',
7 | }
8 |
9 | module.exports = {
10 | //subpath routing, per https://nextjs.org/docs/advanced-features/i18n-routing
11 | i18n: {
12 | locales: ['en-US', 'fr', 'es'],
13 | defaultLocale: 'en-US',
14 | },
15 | env: {
16 | BIGCOMMERCE_API_TOKEN: process.env.BIGCOMMERCE_API_TOKEN,
17 | BIGCOMMERCE_API_URL: process.env.BIGCOMMERCE_API_URL
18 | },
19 | rewrites: () => [STUDIO_REWRITE]
20 | }
21 |
--------------------------------------------------------------------------------
/template/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sanity-template-bigcommerce-editorial",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "concurrently \"next\" \"cd studio && sanity start\"",
7 | "start": "next start",
8 | "build": "npm run build:sanity && npm run build:web",
9 | "build:web": "next build",
10 | "build:sanity": "cd studio && npx sanity build ../public/studio -y && cd ..",
11 | "postinstall": "lerna bootstrap"
12 | },
13 | "dependencies": {
14 | "@sanity/block-content-to-react": "^2.0.7",
15 | "@sanity/client": "^2.1.4",
16 | "@sanity/image-url": "^0.140.19",
17 | "@sanity/ui": "^0.37.12",
18 | "concurrently": "^7.3.0",
19 | "next": "^12.2.5",
20 | "next-sanity": "^0.1.8",
21 | "react": "^18.2.0",
22 | "react-dom": "^18.2.0",
23 | "react-icons": "^4.2.0",
24 | "styled-components": "^5.2.1"
25 | },
26 | "devDependencies": {
27 | "@types/node": "^14.14.20",
28 | "@types/react": "^17.0.0",
29 | "@types/styled-components": "^5.1.8",
30 | "typescript": "^4.1.3"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/template/pages/[hub]/[subhub]/[slug].tsx:
--------------------------------------------------------------------------------
1 | import { GetStaticProps, GetStaticPaths } from 'next'
2 | import Error from 'next/error'
3 | import { useRouter } from 'next/router'
4 |
5 | import { Stack, Card, Box, Heading, Text, Container, Flex } from '@sanity/ui'
6 | import { NavBar, SocialBar, Breadcrumbs, ShopTheStory } from '$components'
7 | import { Category, Article, ArticleSlug } from '../../../types'
8 | import { getClient, urlFor, PortableText, usePreviewSubscription } from '$utils/sanity'
9 | import { handleGroupedItems } from '$utils/helpers'
10 | import { createArticlePageQuery, productQuery } from '$utils/sanityGroqQueries'
11 |
12 | export default function ArticlePage({categories, articleData, preview}
13 | : {categories: Category[], articleData: Article, preview: boolean}) {
14 |
15 | const router = useRouter();
16 |
17 | const {data: article} = usePreviewSubscription(createArticlePageQuery(router.query.slug), {
18 | params: {slug: router.query.slug},
19 | initialData: articleData,
20 | enabled: preview || !!router.query.preview,
21 | })
22 |
23 | if (!router.isFallback && !article?.slug) {
24 | return ;
25 | } else if (router.isFallback) {
26 | return Loading...
27 | }
28 |
29 | const content = handleGroupedItems(
30 | article.content, "listItem", {_key: "orientation", _value: "vertical"})
31 |
32 | if (!!article.storyProducts) {
33 | article.storyProducts = article.storyProducts.filter(obj => !!obj.products)
34 | }
35 |
36 | return (
37 | <>
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
48 |
49 |
50 |
51 |
52 | { article.title }
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | { (article.storyProducts
63 | && !!article.storyProducts[0]) && (
64 |
65 |
66 |
67 | ) }
68 |
69 |
70 | >
71 | )
72 | }
73 |
74 | export const getStaticPaths: GetStaticPaths = async (context) => {
75 |
76 | const articleSlugs = await getClient().fetch(
77 | `*[_type == "article"]{
78 | 'slug': slug.current,
79 | 'subhub': subsection->slug.current,
80 | 'hub': subsection->category->slug.current
81 | }`)
82 |
83 | const paths = {paths: articleSlugs.map(
84 | (slugs: ArticleSlug) => ({params: slugs}))}
85 | return {
86 | ...paths,
87 | fallback: true
88 | }
89 | }
90 |
91 |
92 | export const getStaticProps: GetStaticProps = async ({params, preview}) => {
93 |
94 | const query = createArticlePageQuery(params?.slug)
95 | const article = await getClient(preview).fetch(query)
96 |
97 | return ({
98 | props: {
99 | categories: await getClient(preview).fetch(`*[_type == "category"]{name,'slug': slug.current}`),
100 | articleData: article
101 | },
102 | revalidate: 60
103 | })
104 | }
105 |
106 |
--------------------------------------------------------------------------------
/template/pages/[hub]/[subhub]/index.tsx:
--------------------------------------------------------------------------------
1 | import { GetStaticPaths, GetStaticProps } from 'next'
2 | import Error from 'next/error'
3 | import { useRouter } from 'next/router'
4 | import { Category, SubsectionArticles } from '../../../types'
5 |
6 | import { Box, Container, Heading, Inline, Grid } from '@sanity/ui'
7 | import { NavBar, ArticlePane } from '$components'
8 |
9 | import { getClient, usePreviewSubscription } from '$utils/sanity'
10 | import { GiWomanElfFace } from 'react-icons/gi'
11 | import { subsectionArticleQuery } from '$utils/sanityGroqQueries'
12 |
13 |
14 | export default function Subhub({categories, subsectionArticleData, preview}
15 | : {categories: Category[],
16 | subsectionArticleData: SubsectionArticles,
17 | preview: boolean}) {
18 |
19 | const router = useRouter();
20 | if (!router.isFallback && !subsectionArticleData?.name) {
21 | return ;
22 | } else if (router.isFallback) {
23 | return Loading...
24 | }
25 |
26 | const hub = router.query.hub ?? ""
27 | const subhub = router.query.subhub ?? ""
28 |
29 | const {data: subsectionArticles} = usePreviewSubscription(subsectionArticleQuery, {
30 | params: {slug: subhub},
31 | initialData: subsectionArticleData,
32 | enabled: preview || !!router.query.preview,
33 | })
34 |
35 | const subsectionArticleDisplays = subsectionArticles.articles.map(
36 | (article, i) => (
37 |
38 | )
39 | )
40 |
41 | return (
42 | <>
43 |
44 |
45 |
46 |
47 |
48 |
49 | {subsectionArticles.name}
50 |
51 |
52 |
53 |
54 | { subsectionArticleDisplays }
55 |
56 |
57 | >
58 | )
59 | }
60 |
61 | export const getStaticPaths: GetStaticPaths = async (context) => {
62 |
63 | const subsections = await getClient().fetch(
64 | `*[_type == 'subsection']{
65 | 'subhub': slug.current,
66 | 'hub': category->slug.current
67 | }`)
68 |
69 | return {
70 | ...{paths: subsections.map(({subhub, hub}
71 | : {subhub: string, hub: string}) => (
72 | {params: {subhub: subhub, hub: hub}}))},
73 | fallback: true
74 | }
75 | }
76 |
77 | export const getStaticProps: GetStaticProps = async ({params, preview = false }) => {
78 |
79 | const subsectionArticles = await getClient(preview).fetch(subsectionArticleQuery,
80 | {slug: params?.subhub})
81 |
82 | return ({
83 | props: {
84 | categories: await getClient(preview).fetch(`*[_type == "category"]{name,'slug': slug.current}`),
85 | subsectionArticleData: subsectionArticles
86 | },
87 | revalidate: 60
88 | })
89 | }
90 |
--------------------------------------------------------------------------------
/template/pages/[hub]/index.tsx:
--------------------------------------------------------------------------------
1 | import { GetStaticPaths, GetStaticProps } from 'next'
2 | import Error from 'next/error'
3 | import { useRouter } from 'next/router'
4 | import { Category, SubsectionArticles, CategoryFeature } from '../../types'
5 |
6 | import { Box, Container, Heading, Inline } from '@sanity/ui'
7 | import { NavBar, SubsectionBar } from '$components'
8 | import { handleBlockFeature, coalesceCampaignAndFeature } from '$utils/helpers'
9 |
10 | import { getClient, usePreviewSubscription } from '$utils/sanity'
11 | import { GiWomanElfFace } from 'react-icons/gi'
12 | import { categoryAndFeaturedArticleQuery,
13 | subsectionArticleQueryHasFeature,
14 | subsectionArticleQueryNoFeature } from '$utils/sanityGroqQueries'
15 |
16 |
17 | export default function Hub({categories, subsectionArticleData, categoryData, preview}
18 | : {categories: Category[],
19 | subsectionArticleData: SubsectionArticles[],
20 | categoryData: CategoryFeature,
21 | preview: boolean}) {
22 |
23 | const router = useRouter();
24 | if (!router.isFallback && !categoryData?.categoryId) {
25 | return ;
26 | } else if (router.isFallback) {
27 | return Loading...
28 | }
29 | const hub = router.query.hub ?? ""
30 |
31 | const {data: category} = usePreviewSubscription(categoryAndFeaturedArticleQuery, {
32 | params: {hub: hub},
33 | initialData: categoryData,
34 | enabled: preview || !!router.query.preview,
35 | })
36 |
37 | let featuredArticleDisplay = ()
38 | if (category.featuredArticle) {
39 | const formattedFeature = coalesceCampaignAndFeature(category.featuredArticle)
40 | featuredArticleDisplay = handleBlockFeature(category.featuredArticleDisplay, formattedFeature)
41 | }
42 |
43 |
44 | //TODO: we could probably have more elegant handling for no features
45 | let subsectionArticles = subsectionArticleData
46 | if (category.featuredArticle) {
47 | const {data: subsectionArticles} = usePreviewSubscription(subsectionArticleQueryHasFeature, {
48 | params: {id: category.categoryId,
49 | featuredArticleId: category.featuredArticle._id},
50 | initialData: subsectionArticleData,
51 | enabled: preview || !!router.query.preview,
52 | })
53 | } else {
54 | const {data: subsectionArticles} = usePreviewSubscription(subsectionArticleQueryNoFeature, {
55 | params: {id: category.categoryId},
56 | initialData: subsectionArticleData,
57 | enabled: preview || !!router.query.preview,
58 | })
59 | }
60 |
61 |
62 | const subsectionRows = subsectionArticles
63 | .filter(sub => sub.articles.length)
64 | .map((subsection, i) => (
65 |
66 | )
67 | )
68 |
69 | return (
70 | <>
71 |
72 |
73 |
74 |
75 |
76 |
77 | {category.name}
78 |
79 |
80 |
81 | { featuredArticleDisplay }
82 |
83 | { subsectionRows }
84 |
85 |
86 | >
87 | )
88 | }
89 |
90 | export const getStaticPaths: GetStaticPaths = async (context) => {
91 |
92 | const categories = await getClient().fetch(
93 | `*[_type == "category"].slug.current`)
94 |
95 | return {
96 | ...{paths: categories.map((cat: string) => ({params: {hub: cat}}))},
97 | fallback: true
98 | }
99 | }
100 |
101 | export const getStaticProps: GetStaticProps = async ({params, preview = false }) => {
102 |
103 | const category = await getClient(preview).fetch(
104 | categoryAndFeaturedArticleQuery, {hub: params?.hub})
105 |
106 | if (!category) {
107 | return {notFound: true}
108 | }
109 |
110 | let subsectionArticles;
111 | if (category.featuredArticle) {
112 | subsectionArticles = await getClient(preview).fetch(
113 | subsectionArticleQueryHasFeature, {id: category.categoryId,
114 | featuredArticleId: category.featuredArticle._id})
115 | } else {
116 | subsectionArticles = await getClient(preview).fetch(
117 | subsectionArticleQueryNoFeature, {id: category.categoryId})
118 | }
119 |
120 | return ({
121 | props: {
122 | categories: await getClient(preview).fetch(`*[_type == "category"]{name,'slug': slug.current}`),
123 | subsectionArticleData: subsectionArticles,
124 | categoryData: category,
125 | preview
126 | },
127 | revalidate: 60
128 | })
129 | }
130 |
131 |
--------------------------------------------------------------------------------
/template/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import {AppProps} from 'next/app'
2 | import Head from 'next/head'
3 | import React from 'react'
4 |
5 | import { BigCommerceContextProvider } from '../contexts/bigcommerce-context'
6 | import {sanityTheme} from '$theme'
7 | import {ThemeProvider, Box} from '@sanity/ui'
8 | import {GlobalStyle, Cart} from '$components'
9 |
10 | function App({Component, pageProps, router}: AppProps) {
11 |
12 | return (
13 |
14 |
15 |
16 |
17 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | )
31 | }
32 |
33 | export default App
34 |
--------------------------------------------------------------------------------
/template/pages/_document.js:
--------------------------------------------------------------------------------
1 | import Document from 'next/document'
2 | import { ServerStyleSheet } from 'styled-components'
3 |
4 | export default class MyDocument extends Document {
5 | static async getInitialProps(ctx) {
6 | const sheet = new ServerStyleSheet()
7 | const originalRenderPage = ctx.renderPage
8 |
9 | try {
10 | ctx.renderPage = () =>
11 | originalRenderPage({
12 | enhanceApp: (App) => (props) =>
13 | sheet.collectStyles(),
14 | })
15 |
16 | const initialProps = await Document.getInitialProps(ctx)
17 | return {
18 | ...initialProps,
19 | styles: (
20 | <>
21 | {initialProps.styles}
22 | {sheet.getStyleElement()}
23 | >
24 | ),
25 | }
26 | } finally {
27 | sheet.seal()
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/template/pages/api/bigcommerce.js:
--------------------------------------------------------------------------------
1 | const bigCommerceHeaders = {
2 | headers: {
3 | "Accept": "application/json",
4 | "Content-Type": "application/json",
5 | "X-Auth-Token": process.env.BIGCOMMERCE_API_TOKEN,
6 | }
7 | }
8 |
9 | export const config = {
10 | api: {
11 | bodyParser: true,
12 | },
13 | }
14 | export default async function BigCommerceCart(req, res) {
15 |
16 | const { query: {cartID, itemID}, body } = req
17 | let bcCartResponse;
18 |
19 | try {
20 | if (req.method == 'POST') {
21 | bcCartResponse = await fetch(`${process.env.BIGCOMMERCE_API_URL}/carts`, {
22 | method: "POST",
23 | ...bigCommerceHeaders,
24 | body: JSON.stringify({line_items: []})
25 | })
26 | .then(response => response.json());
27 |
28 | }
29 |
30 | else if (req.method == 'GET') {
31 | if (!cartID || cartID == 'undefined' || cartID == 'null') {
32 | return res.status(400).json({error: 'Cart ID not found.'})
33 | }
34 | bcCartResponse = await fetch(`${process.env.BIGCOMMERCE_API_URL}/carts/${cartID}`, {
35 | ...bigCommerceHeaders
36 | })
37 | .then(response => response.json());
38 |
39 | //theres a cart ID in localStorage, but not found in BC, make a new one
40 | if (bcCartResponse.status == 404) {
41 | bcCartResponse = await fetch(`${process.env.BIGCOMMERCE_API_URL}/carts`, {
42 | method: "POST",
43 | ...bigCommerceHeaders,
44 | body: JSON.stringify({line_items: []})
45 | })
46 | .then(response => response.json());
47 | }
48 | }
49 |
50 | else if (req.method == 'PUT') {
51 | if (!cartID || cartID == 'undefined' || cartID == 'null') {
52 | return res.status(400).json({error: 'Cart ID not found.'})
53 | }
54 |
55 | bcCartResponse = await fetch(`${process.env.BIGCOMMERCE_API_URL}/carts/${cartID}/items`, {
56 | method: 'POST',
57 | ...bigCommerceHeaders,
58 | body: body
59 | })
60 | .then(response => response.json());
61 | }
62 |
63 | else if (req.method == 'DELETE') {
64 | if (!cartID || cartID == 'undefined' || cartID == 'null') {
65 | return res.status(400).json({error: 'Cart ID not found.'})
66 | }
67 |
68 | bcCartResponse = await fetch(`${process.env.BIGCOMMERCE_API_URL}/carts/${cartID}/items/${itemID}`, {
69 | method: 'DELETE',
70 | ...bigCommerceHeaders
71 | })
72 |
73 | //per BC's docs, deleting the last line item deletes the cart https://developer.bigcommerce.com/api-reference/store-management/carts/cart-items/deletecartlineitem, so make a new one
74 | if (bcCartResponse.status == 204) {
75 | bcCartResponse = await fetch(`${process.env.BIGCOMMERCE_API_URL}/carts`, {
76 | method: "POST",
77 | ...bigCommerceHeaders,
78 | body: JSON.stringify({line_items: []})
79 | })
80 | .then(response => response.json());
81 | } else {
82 | bcCartResponse = bcCartResponse.json()
83 | }
84 |
85 | }
86 |
87 | else {
88 | return res.status(400).json({error: 'Invalid request method -- check the code in the BigCommerce context provider.'})
89 | }
90 |
91 |
92 | //TODO: check status of bcCartResponse (400, etc.) and return meaningful message
93 | return res.status(200).json(bcCartResponse)
94 |
95 |
96 | } catch (error) {
97 | console.log(error)
98 | return res.status(400).json({error: 'An error occurred. Please check your console log for more details.'})
99 | }
100 |
101 | }
102 |
--------------------------------------------------------------------------------
/template/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import { GetStaticProps } from 'next'
2 | import { useRouter } from 'next/router'
3 | import { groq } from 'next-sanity'
4 |
5 | import { Card, Container, Flex, Stack, Text } from '@sanity/ui'
6 |
7 | import { Category, Article, MonthArticle } from '../types'
8 | import { getClient, usePreviewSubscription } from '$utils/sanity'
9 | import { coalesceCampaignAndFeature } from '$utils/helpers'
10 | import { IndexArticleGrid, NavBar, SubsectionBar } from '$components'
11 | import { indexQuery } from '$utils/sanityGroqQueries'
12 |
13 |
14 | function IndexPage({categories, featuredArticleData, recentArticleData, preview}
15 | : {categories: Category[], featuredArticleData: Article[], recentArticleData: Article[], preview: boolean}) {
16 |
17 | const router = useRouter()
18 | const hub = router.query.hub ?? ""
19 |
20 | const {data: {featuredArticles, recentArticles}} = usePreviewSubscription(indexQuery, {
21 | initialData: {
22 | featuredArticles: featuredArticleData,
23 | recentArticles: recentArticleData},
24 | enabled: preview || !!router.query.preview,
25 | })
26 |
27 |
28 | const articlesByMonth: MonthArticle[] = []
29 | recentArticles.forEach(article => {
30 | const date = new Date(article.publishedDate)
31 | const month = new Intl.DateTimeFormat('en', { month: 'long' }).format(date);
32 | const year = new Intl.DateTimeFormat('en', { year: 'numeric' }).format(date);
33 | const stringDate = `${month} ${year}`
34 | if (!articlesByMonth.length || articlesByMonth[articlesByMonth.length - 1].name != stringDate) {
35 | articlesByMonth.push({
36 | name: stringDate,
37 | articles: [article]
38 | })
39 | } else {
40 | articlesByMonth[articlesByMonth.length - 1].articles.push(article)
41 | }
42 | })
43 |
44 | const formattedFeatures = featuredArticles.map(feat => coalesceCampaignAndFeature(feat))
45 |
46 | return (
47 | <>
48 |
49 |
50 |
51 |
52 |
53 | { articlesByMonth.map((month, i) => (
54 |
55 |
56 | ))}
57 |
58 |
59 |
60 | >
61 | )
62 | }
63 |
64 | export const getStaticProps: GetStaticProps = async ({params, preview = false }) => {
65 | const {featuredArticles, recentArticles} = await getClient(preview).fetch(indexQuery)
66 | return ({
67 | props: {
68 | categories: await getClient(preview).fetch(`*[_type == "category"]{name,'slug': slug.current}`),
69 | featuredArticleData: featuredArticles,
70 | recentArticleData: recentArticles,
71 | preview: preview
72 | },
73 | revalidate: 60
74 | })
75 | }
76 |
77 | export default IndexPage;
78 |
79 |
80 | //logic for featured index articles
81 | //either they're set or are the most recent across the board
82 | //all set come before auto
83 |
84 |
--------------------------------------------------------------------------------
/template/pages/shop/[slug].tsx:
--------------------------------------------------------------------------------
1 | import { GetStaticProps, GetStaticPaths } from 'next'
2 | import Error from 'next/error'
3 | import { useRouter } from 'next/router'
4 |
5 | import { Stack, Inline, Button, Box, Heading, Text, Badge, Flex, Grid } from '@sanity/ui'
6 |
7 | import { Product, Category, Slug } from '../../types'
8 | import { useStore, useAddItem } from '../../contexts/bigcommerce-context'
9 |
10 | import { getClient, urlFor, PortableText, usePreviewSubscription } from '$utils/sanity'
11 | import { handleLocaleField } from '$utils/helpers'
12 | import { NavBar, SubsectionBar, ResponsiveFixedRatioImage } from '$components'
13 |
14 | import { productDetailPageQuery } from '$utils/sanityGroqQueries'
15 |
16 | export default function ProductPage({categories, productData, preview}
17 | : {categories: Category[], productData: Product, preview: boolean}) {
18 |
19 | const router = useRouter();
20 | if (!router.isFallback && !productData?.slug) {
21 | return ;
22 | } else if (router.isFallback) {
23 | return Loading...
24 | }
25 |
26 | const {data: product} = usePreviewSubscription(productDetailPageQuery, {
27 | params: {slug: router.query.slug},
28 | initialData: productData,
29 | enabled: preview || !!router.query.preview,
30 | })
31 |
32 | const addItemToCart = useAddItem()
33 |
34 | return (
35 | <>
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | { product.manufacturer }
45 |
46 |
47 | { handleLocaleField('name', product, router.locale) }
48 |
49 |
50 |
51 | US ${product.price}
52 | Free Shipping!
53 |
54 |
55 |
56 |
69 |
70 |
71 |
75 | >
76 | )
77 | }
78 |
79 | export const getStaticPaths: GetStaticPaths = async ({locales}) => {
80 | const productSlugs: Slug[] = await getClient().fetch(
81 | `*[_type == "product"]{
82 | 'slug': slug.current
83 | }`)
84 |
85 | const typedLocales = locales ?? []
86 |
87 | const slugsWithLocales = typedLocales.map(locale => (
88 | productSlugs.map(slug => (
89 | {params: {...slug}, locale: locale}
90 | ))
91 | )).flat()
92 |
93 | return {
94 | paths: slugsWithLocales,
95 | fallback: false
96 | }
97 | }
98 |
99 | export const getStaticProps: GetStaticProps = async ({params, preview = false}) => {
100 | const product = await getClient(preview).fetch(productDetailPageQuery,
101 | {slug: params?.slug})
102 |
103 | return ({
104 | props: {
105 | categories: await getClient(preview).fetch(`*[_type == "category"]{name,'slug': slug.current}`),
106 | productData: product,
107 | preview
108 | },
109 | revalidate: 60
110 | })
111 | }
112 |
--------------------------------------------------------------------------------
/template/pages/shop/campaign/[slug].tsx:
--------------------------------------------------------------------------------
1 | import { GetStaticProps, GetStaticPaths } from 'next'
2 | import Error from 'next/error'
3 | import { useRouter } from 'next/router'
4 | import { Stack, Box } from '@sanity/ui'
5 |
6 | import { Slug, Category, Campaign, Color } from '../../../types'
7 | import { getClient, urlFor, PortableText, usePreviewSubscription } from '$utils/sanity'
8 | import { campaignQuery } from '$utils/sanityGroqQueries'
9 |
10 | import { NavBar, ShopGrid, SolidBlockFeature } from '$components'
11 | import { handleBlockFeature } from '$utils/helpers'
12 |
13 |
14 | export default function CampaignPage({categories, campaignData, preview}
15 | : {categories: Category[], campaignData: Campaign, preview: boolean}) {
16 |
17 | const router = useRouter();
18 | if (!router.isFallback && !campaignData?.slug) {
19 | return ;
20 | } else if (router.isFallback) {
21 | return Loading...
22 | }
23 |
24 | const {data: campaign} = usePreviewSubscription(campaignQuery, {
25 | params: {slug: campaignData.slug},
26 | initialData: campaignData,
27 | enabled: preview || !!router.query.preview,
28 | })
29 |
30 | const parsedContent = campaign.content.map(({_type, ...block}
31 | : {_type: string}, i: number) => (
32 |
33 | { handleBlockFeature(_type, block, true) }
34 |
35 | ))
36 |
37 | const blockColor: Color = {hex: '#FFF'}
38 | const textColor: Color = {hex: '#000'}
39 |
40 | const campaignFeatureProps = {
41 | ...{title: campaign.title, text: campaign.text, image: campaign.image},
42 | url: "",
43 | blockColor,
44 | textColor,
45 | orientation: 'right'
46 | }
47 |
48 | return (
49 | <>
50 |
51 |
52 | { (campaign.hideLeadBlock) ? : }
53 | { parsedContent }
54 |
55 |
56 | >
57 | )
58 | }
59 |
60 | export const getStaticPaths: GetStaticPaths = async (context) => {
61 | const campaignSlugs: Slug[] = await getClient().fetch(
62 | `*[_type == "campaign"]{
63 | 'slug': slug.current
64 | }`)
65 |
66 | const paths = {paths: campaignSlugs.map(
67 | (slugObj) => ({params: slugObj}))}
68 |
69 | return { ...paths, fallback: false }
70 | }
71 |
72 | export const getStaticProps: GetStaticProps = async ({params, preview = false}) => {
73 | const campaign = await getClient(preview).fetch(campaignQuery, {slug: params?.slug})
74 |
75 |
76 | return ({
77 | props: {
78 | categories: await getClient(preview).fetch(`*[_type == "category"]{name,'slug': slug.current}`),
79 | campaignData: campaign,
80 | preview
81 | },
82 | revalidate: 60
83 | })
84 | }
85 |
--------------------------------------------------------------------------------
/template/pages/shop/index.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Grid, Heading } from '@sanity/ui'
2 | import { GetStaticPaths, GetStaticProps } from 'next'
3 | import { sanityClient } from '$utils/sanity'
4 | import { Category, Product } from '../../types'
5 | import { NavBar, ShopGrid } from '$components'
6 | import { shopQuery } from '$utils/sanityGroqQueries'
7 |
8 | export default function Shop({categories, products}
9 | : {categories: Category[], products: Product[]}) {
10 |
11 | //TODO: promotion/campaign up top
12 | return (
13 | <>
14 |
15 |
16 | )
17 |
18 | >
19 | )
20 | }
21 |
22 | export const getStaticProps: GetStaticProps = async (context) => {
23 |
24 | return ({
25 | props: {
26 | categories: await sanityClient.fetch(`*[_type == 'category']{name,'slug': slug.current}`),
27 | products: await sanityClient.fetch(shopQuery)
28 | },
29 | revalidate: 60
30 | })
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/template/public/blank.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanity-io/sanity-template-bigcommerce-editorial/208dce0bd84e57953cc11fc01e4c47e62f2a715f/template/public/blank.png
--------------------------------------------------------------------------------
/template/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sanity-io/sanity-template-bigcommerce-editorial/208dce0bd84e57953cc11fc01e4c47e62f2a715f/template/public/favicon.ico
--------------------------------------------------------------------------------
/template/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/template/studio/README.md:
--------------------------------------------------------------------------------
1 | # Sanity Clean Content Studio
2 |
3 | This is the studio portion of this starter. If you need more information, check the following links!
4 |
5 | - [Read “getting started” in the docs](https://www.sanity.io/docs/introduction/getting-started?utm_source=readme)
6 | - [Join the community Slack](https://slack.sanity.io/?utm_source=readme)
7 | - [Extend and build plugins](https://www.sanity.io/docs/content-studio/extending?utm_source=readme)
8 |
--------------------------------------------------------------------------------
/template/studio/config/.checksums:
--------------------------------------------------------------------------------
1 | {
2 | "#": "Used by Sanity to keep track of configuration file checksums, do not delete or modify!",
3 | "@sanity/default-layout": "bb034f391ba508a6ca8cd971967cbedeb131c4d19b17b28a0895f32db5d568ea",
4 | "@sanity/default-login": "6fb6d3800aa71346e1b84d95bbcaa287879456f2922372bb0294e30b968cd37f",
5 | "@sanity/form-builder": "b38478227ba5e22c91981da4b53436df22e48ff25238a55a973ed620be5068aa",
6 | "@sanity/data-aspects": "d199e2c199b3e26cd28b68dc84d7fc01c9186bf5089580f2e2446994d36b3cb6",
7 | "@sanity/vision": "da5b6ed712703ecd04bf4df560570c668aa95252c6bc1c41d6df1bda9b8b8f60"
8 | }
9 |
--------------------------------------------------------------------------------
/template/studio/config/@sanity/data-aspects.json:
--------------------------------------------------------------------------------
1 | {
2 | "listOptions": {}
3 | }
4 |
--------------------------------------------------------------------------------
/template/studio/config/@sanity/default-layout.json:
--------------------------------------------------------------------------------
1 | {
2 | "toolSwitcher": {
3 | "order": [],
4 | "hidden": []
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/template/studio/config/@sanity/default-login.json:
--------------------------------------------------------------------------------
1 | {
2 | "providers": {
3 | "mode": "append",
4 | "redirectOnSingle": false,
5 | "entries": []
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/template/studio/config/@sanity/form-builder.json:
--------------------------------------------------------------------------------
1 | {
2 | "images": {
3 | "directUploads": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/template/studio/config/@sanity/vision.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultApiVersion": "2021-10-21"
3 | }
4 |
--------------------------------------------------------------------------------
/template/studio/env.example:
--------------------------------------------------------------------------------
1 | SANITY_STUDIO_BIGCOMMERCE_STORE_HASH=
2 | SANITY_STUDIO_BIGCOMMERCE_STORE_API_TOKEN=
3 | SANITY_STUDIO_PREVIEW_URL=http://localhost:3000
4 |
--------------------------------------------------------------------------------
/template/studio/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sanity-template-bigcommerce-editorial",
3 | "private": true,
4 | "version": "1.0.0",
5 | "description": "",
6 | "main": "package.json",
7 | "author": "Carolina Gonzalez ",
8 | "license": "UNLICENSED",
9 | "scripts": {
10 | "start": "sanity start",
11 | "test": "sanity check"
12 | },
13 | "keywords": [
14 | "sanity"
15 | ],
16 | "dependencies": {
17 | "@sanity/base": "^2.30.4",
18 | "@sanity/cli": "^2.30.3",
19 | "@sanity/color-input": "^2.30.4",
20 | "@sanity/core": "^2.30.3",
21 | "@sanity/default-layout": "^2.30.4",
22 | "@sanity/default-login": "^2.30.4",
23 | "@sanity/desk-tool": "^2.30.4",
24 | "@sanity/vision": "^2.30.4",
25 | "ndjson": "^2.0.0",
26 | "node-fetch": "^1.7.3",
27 | "prop-types": "^15.6",
28 | "react": "^16.9",
29 | "react-dom": "^16.2",
30 | "react-icons": "^4.2.0",
31 | "sanity-mobile-preview": "^1.0.7",
32 | "sanity-plugin-asset-source-unsplash": "^0.1.3",
33 | "styled-components": "^5.2.0"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/template/studio/plugins/.gitkeep:
--------------------------------------------------------------------------------
1 | User-specific packages can be placed here
2 |
--------------------------------------------------------------------------------
/template/studio/sanity.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "project": {
4 | "name": "sanity-template-bigcommerce-editorial",
5 | "basePath": "/studio"
6 | },
7 | "api": {
8 | "projectId": "rrw497vy",
9 | "dataset": "production"
10 | },
11 | "plugins": [
12 | "@sanity/base",
13 | "@sanity/default-layout",
14 | "@sanity/default-login",
15 | "@sanity/desk-tool",
16 | "asset-source-unsplash",
17 | "@sanity/color-input"
18 | ],
19 | "env": {
20 | "development": {
21 | "plugins": [
22 | "@sanity/vision"
23 | ]
24 | }
25 | },
26 | "parts": [
27 | {
28 | "name": "part:@sanity/base/schema",
29 | "path": "./schemas/schema"
30 | },
31 | {
32 | "name": "part:@sanity/base/initial-value-templates",
33 | "path": "./src/initialValueTemplates.js"
34 | },
35 | {
36 | "name": "part:@sanity/desk-tool/structure",
37 | "path": "./src/deskStructure.js"
38 | }
39 | ]
40 | }
41 |
--------------------------------------------------------------------------------
/template/studio/schemas/documents/article.js:
--------------------------------------------------------------------------------
1 | export default {
2 | title: "Article",
3 | name: "article",
4 | type: "document",
5 | fields: [
6 | {
7 | title: 'Subsection',
8 | name: 'subsection',
9 | type: 'reference',
10 | to: [{type: "subsection"}]
11 | },
12 | {
13 | title: "Hero Image",
14 | name: "heroImage",
15 | type: "image",
16 | description: "The lead image for this page. Also used in thumbnails, etc.",
17 | options: {
18 | crop: true,
19 | hotspot: true
20 | }
21 | },
22 | {
23 | title: "Title",
24 | name: "title",
25 | type: "string",
26 | description: "The title of this page (this will show up in your browser heading and internal links)",
27 | validation: (Rule) => Rule.required(),
28 | },
29 | {
30 | title: "slug",
31 | name: "slug",
32 | type: "slug",
33 | description: "The slug for this page",
34 | options: {
35 | source: "title",
36 | },
37 | validation: (Rule) => Rule.required(),
38 | },
39 | {
40 | title: 'Published date',
41 | name: 'publishedDate',
42 | description: "Date to start showing this article",
43 | type: 'date',
44 | },
45 | {
46 | title: "Authors",
47 | name: "authors",
48 | type: "array",
49 | of: [
50 | {type: "reference",
51 | to: [{type: "person"}]}
52 | ]
53 | },
54 | {
55 | name: 'excerpt',
56 | type: 'excerptPortableText',
57 | title: 'Excerpt',
58 | description:
59 | 'This ends up on summary pages, on Google, when people share your post in social media.',
60 | },
61 | // {
62 | // title: "Include Author Block",
63 | // name: "includeAuthorBlock",
64 | // type: "boolean",
65 | // description: "Flag to include the authors' images and bio (note: bio only shows up for single authors)"
66 | // },
67 | {
68 | title: 'Content',
69 | name: 'content',
70 | type: 'array',
71 | of: [{type: 'block'},
72 | {type: 'listItem'},
73 | {type: 'hr'},
74 | {type: 'productsDisplay'}]
75 | },
76 | ]
77 | }
78 |
--------------------------------------------------------------------------------
/template/studio/schemas/documents/campaign.js:
--------------------------------------------------------------------------------
1 | export default {
2 | title: "Campaign",
3 | name: "campaign",
4 | type: "document",
5 | fields: [
6 | {
7 | title: "Title",
8 | name: "title",
9 | type: "string",
10 | description: "The title of this campaign (this will show up in your browser heading and internal links)",
11 | validation: (Rule) => Rule.required(),
12 | },
13 | {
14 | title: "Hero image",
15 | name: "heroImage",
16 | type: "image",
17 | description: "The image used to promote this campaign (for example, if featured on index page or hubs)",
18 | options: {
19 | crop: true,
20 | hotspot: true
21 | }
22 | },
23 | {
24 | title: "Text",
25 | name: "text",
26 | type: "excerptPortableText",
27 | description: "Short descriptive text that may be used to promote this campaign",
28 | },
29 | {
30 | title: "slug",
31 | name: "slug",
32 | type: "slug",
33 | description: "The slug for this campaign",
34 | options: {
35 | source: "title",
36 | },
37 | validation: (Rule) => Rule.required(),
38 | },
39 | {
40 | title: 'Published date',
41 | name: 'publishedDate',
42 | description: "Date to start showing this campaign",
43 | type: 'date',
44 | },
45 | {
46 | title: 'Hide lead block',
47 | name: 'hideLeadBlock',
48 | description: 'Toggle to show if you want to display the hero image, title, and description at the top of the campaign page',
49 | type: 'boolean',
50 | },
51 | {
52 | title: 'Content',
53 | name: 'content',
54 | type: 'array',
55 | description: "The content to show on the campaign page. The image and text you use for the first block will also be used internally across the site.",
56 | of: [
57 | //todo: consider block quotes -- maybe just an object that allows you to do a text block?
58 | {type: 'productCardFeature'},
59 | {type: 'solidBlockFeature'},
60 | {type: 'textOverlayFeature'},
61 | ]
62 | },
63 | {
64 | title: 'Products',
65 | name: 'products',
66 | description: "The products featured on this campaign",
67 | type: 'array',
68 | of: [{type: 'reference', to: [{type: 'product'}]}]
69 | }
70 | ]
71 | }
72 |
--------------------------------------------------------------------------------
/template/studio/schemas/documents/category.js:
--------------------------------------------------------------------------------
1 | export default {
2 | title: "Category",
3 | name: "category",
4 | type: "document",
5 | fields: [
6 | {
7 | title: "name",
8 | name: "name",
9 | type: "string",
10 | description: "The name of this category",
11 | validation: (Rule) => Rule.required(),
12 | },
13 | {
14 | title: "slug",
15 | name: "slug",
16 | type: "slug",
17 | description: "The slug for this category; where it is routable on the main site.",
18 | options: {
19 | source: "name",
20 | },
21 | validation: (Rule) => Rule.required(),
22 | },
23 | {
24 | title: "Featured Article",
25 | name: "featuredArticle",
26 | description: "The featured article for this category",
27 | type: "reference",
28 | to: [{type: "article"}, {type: 'campaign'}]
29 | },
30 | {
31 | title: "Featured Article Display",
32 | name: "featuredArticleDisplay",
33 | type: "string",
34 | description: "Determines how the featured article will be displayed on the hub page.",
35 | options: {
36 | list: [
37 | {title: 'Text Below', value: 'textBelowFeature'},
38 | {title: '50/50 Card', value: 'solidBlockFeature'},
39 | {title: 'Text Overlay', value: 'textOverlayFeature'}
40 | ],
41 | },
42 | },
43 | ]
44 | }
45 |
--------------------------------------------------------------------------------
/template/studio/schemas/documents/person.js:
--------------------------------------------------------------------------------
1 | export default {
2 | title: "Person",
3 | name: "person",
4 | type: "document",
5 | fields: [
6 | {
7 | title: "Name",
8 | name: "name",
9 | type: "string",
10 | },
11 | {
12 | title: "Picture",
13 | name: "picture",
14 | type: "image",
15 | description: "The portrait for this editor.",
16 | },
17 | {
18 | title: "Bio",
19 | name: "bio",
20 | type: "text",
21 | description: "A short biography for this editor, to appear in articles, hub pages, etc.",
22 | },
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/template/studio/schemas/documents/product.js:
--------------------------------------------------------------------------------
1 | const fieldsetOptions = {options: {collapsible: true, collapsed: true}}
2 |
3 | export default {
4 | title: "Product",
5 | name: "product",
6 | type: "document",
7 | fieldsets: [
8 | {name: 'locale_fr', title: 'French language overrides', ...fieldsetOptions},
9 | {name: 'locale_es', title: 'Spanish language overrides', ...fieldsetOptions},
10 | ],
11 | fields: [
12 | {
13 | title: "Name",
14 | name: "name",
15 | type: "string",
16 | description: "The name of this product",
17 | validation: (Rule) => Rule.required(),
18 | },
19 | {
20 | title: "Product image",
21 | name: "productImage",
22 | type: "image",
23 | description: "The manufacturer photo for this image.",
24 | options: {
25 | crop: true,
26 | hotspot: true
27 | }
28 | },
29 | {
30 | title: "slug",
31 | name: "slug",
32 | type: "slug",
33 | description: "The slug for the dedicated product page",
34 | options: {
35 | source: "name",
36 | },
37 | validation: (Rule) => Rule.required(),
38 | },
39 | {
40 | title: "SKU",
41 | name: "sku",
42 | type: "string",
43 | description: "The sku of this product",
44 | validation: (Rule) => Rule.required(),
45 | },
46 | {
47 | title: "Description",
48 | name: "description",
49 | type: 'array',
50 | of: [{type: 'block'}],
51 | description: "The description for this product",
52 | validation: (Rule) => Rule.required(),
53 | },
54 | {
55 | title: "Price",
56 | name: "price",
57 | type: "number",
58 | description: "The price of this product (note, this is the first price in USD that we could find in BigCommerce -- there may be other prices available!)",
59 | validation: (Rule) => Rule.required(),
60 | },
61 | {
62 | title: "Manufacturer",
63 | name: "manufacturer",
64 | type: "string",
65 | description: "The manufacturer of this product",
66 | validation: (Rule) => Rule.required(),
67 | },
68 | {
69 | title: "Category",
70 | name: "category",
71 | description: "Category this product belongs to (used for editorial embeds, etc.)",
72 | type: "reference",
73 | to: [{type: "category"}]
74 | },
75 | {
76 | title: "Name (French)",
77 | name: "locale_fr_name",
78 | type: "string",
79 | description: "Le nom en francais",
80 | fieldset: 'locale_fr'
81 | },
82 | {
83 | title: "Description (French)",
84 | name: "locale_fr_description",
85 | description: "La description en francais",
86 | type: 'array',
87 | of: [{type: 'block'}],
88 | fieldset: 'locale_fr'
89 | },
90 | {
91 | title: "Name (Spanish)",
92 | name: "locale_es_name",
93 | type: "string",
94 | description: "El nombre en español",
95 | fieldset: 'locale_es'
96 | },
97 | {
98 | title: "Description (Spanish)",
99 | name: "locale_es_description",
100 | description: "La descripción en español",
101 | type: 'array',
102 | of: [{type: 'block'}],
103 | fieldset: 'locale_es'
104 | },
105 | ]
106 | }
107 |
--------------------------------------------------------------------------------
/template/studio/schemas/documents/route.js:
--------------------------------------------------------------------------------
1 | // import { MdLink } from "react-icons/md"
2 | import {slugifier} from '../utils'
3 |
4 | export default {
5 | name: 'route',
6 | type: 'document',
7 | title: 'Page route',
8 | fieldsets: [
9 | {
10 | title: 'Visibility',
11 | name: 'visibility',
12 | },
13 | ],
14 | fields: [
15 | {
16 | name: 'slug',
17 | type: 'slug',
18 | description: 'This is the website path the page will accessible on',
19 | title: 'Path',
20 | validation: (Rule) =>
21 | Rule.required().custom((slug) => {
22 | if (slug && slug.current && slug.current === '/') {
23 | return 'Cannot be /'
24 | }
25 | return true
26 | }),
27 | options: {
28 | source: (doc, options) => options.parent.page,
29 | slugify: slugifier
30 | }
31 | },
32 | // {
33 | // title: 'Open graph',
34 | // name: 'openGraph',
35 | // description: 'These values populate meta tags',
36 | // type: 'openGraph',
37 | // },
38 | {
39 | title: 'Include in sitemap',
40 | description: 'For search engines. Will be generateed to /sitemap.xml',
41 | name: 'includeInSitemap',
42 | type: 'boolean',
43 | fieldset: 'visibility'
44 | },
45 | {
46 | title: 'Disallow in robots.txt',
47 | description: 'Hide this route for search engines like google',
48 | name: 'disallowRobots',
49 | type: 'boolean',
50 | fieldset: 'visibility'
51 | },
52 | /*
53 | // This can be used by a server-side rendered website. We plan to figure out proper JAMstack support
54 | {
55 | name: 'queries',
56 | type: 'array',
57 | description: 'Used to return personalized content based on paid search terms and remarketing',
58 | of: [
59 | {
60 | type: 'string'
61 | }
62 | ],
63 | options: {
64 | layout: 'tags'
65 | }
66 | }, */
67 | // {
68 | // name: 'campaign',
69 | // type: 'string',
70 | // title: 'Campaign',
71 | // description: 'UTM for campaings'
72 | // },
73 | /*
74 | // This can be used by a server-side rendered website. We plan to figure out proper JAMstack support
75 | {
76 | name: 'experiment',
77 | type: 'experiment',
78 | description: 'Use this to A/B/n test this route towards different pages',
79 | }, */
80 | ],
81 | initialValue: {
82 | useSiteTitle: false,
83 | },
84 | preview: {
85 | select: {
86 | title: 'slug.current',
87 | subtitle: 'page.title',
88 | },
89 | prepare({ title, subtitle }) {
90 | return {
91 | title: ['/', title].join(''),
92 | subtitle,
93 | }
94 | },
95 | },
96 | }
97 |
--------------------------------------------------------------------------------
/template/studio/schemas/documents/siteSettings.js:
--------------------------------------------------------------------------------
1 | export default {
2 | name: 'siteSettings',
3 | type: 'document',
4 | title: 'Site Settings',
5 | // __experimental_actions: ['update', /* 'create', 'delete', */ 'publish'],
6 | fields: [
7 | {
8 | title: 'Featured Articles',
9 | name: 'featuredArticles',
10 | description: 'Which articles are featured on the hero spot on the front page? (If 3 items are not set, we will use the most recent articles to fill the gap)',
11 | type: 'array',
12 | of: [{type: 'reference', to: [{type: 'article'}, {type: 'campaign'}]}],
13 | validation: Rule => Rule.max(3)
14 | },
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/template/studio/schemas/documents/subsection.js:
--------------------------------------------------------------------------------
1 | export default {
2 | title: "Subsection",
3 | name: "subsection",
4 | type: "document",
5 | fields: [
6 | {
7 | title: "name",
8 | name: "name",
9 | type: "string",
10 | description: "The name of this subsection",
11 | validation: (Rule) => Rule.required(),
12 | },
13 | {
14 | title: "slug",
15 | name: "slug",
16 | description: "The slug for this subsection",
17 | type: "slug",
18 | options: {
19 | source: "name",
20 | },
21 | validation: (Rule) => Rule.required(),
22 | },
23 | {
24 | title: "Category",
25 | name: "category",
26 | description: "The category this subsection belongs to",
27 | validation: (Rule) => Rule.required(),
28 | type: "reference",
29 | to: [{type: "category"}]
30 | }
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/template/studio/schemas/objects/excerptPortableText.js:
--------------------------------------------------------------------------------
1 | export default {
2 | name: 'excerptPortableText',
3 | type: 'array',
4 | title: 'Excerpt',
5 | of: [
6 | {
7 | title: 'Block',
8 | type: 'block',
9 | styles: [{title: 'Normal', value: 'normal'}],
10 | lists: [],
11 | marks: {
12 | decorators: [
13 | {title: 'Strong', value: 'strong'},
14 | {title: 'Emphasis', value: 'em'},
15 | {title: 'Code', value: 'code'}
16 | ],
17 | annotations: []
18 | }
19 | }
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/template/studio/schemas/pages/hr.js:
--------------------------------------------------------------------------------
1 | export default {
2 | title: "Section break",
3 | name: "hr",
4 | type: "document",
5 | fields: [
6 | {
7 | title: "Section break",
8 | name: "hr",
9 | type: "boolean",
10 | },
11 | ],
12 | initialValue: {
13 | hr: true
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/template/studio/schemas/pages/listItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default {
4 | title: "List Item",
5 | name: "listItem",
6 | type: "document",
7 | fields: [
8 | {
9 | title: "Title",
10 | name: "title",
11 | type: "string",
12 | description: "The title of this list item",
13 | validation: (Rule) => Rule.required(),
14 | },
15 | {
16 | title: "Orientation",
17 | name: "orientation",
18 | type: "string",
19 | description: "The orientation of this list item",
20 | options: {
21 | list: ["horizontal", "vertical"],
22 | },
23 | validation: (Rule) => Rule.required(),
24 | },
25 | {
26 | title: "Text",
27 | name: "text",
28 | description: "The text of this list item",
29 | type: 'array',
30 | of: [{type: 'block'}],
31 | validation: (Rule) => Rule.required(),
32 | },
33 | {
34 | title: "Product Display Size",
35 | name: "productDisplaySize",
36 | type: "string",
37 | description: "Determines whether the items display in large format below the list item or small format to the side of the list item (only applicable in horizontal list items)",
38 | options: {
39 | list: ["small", "large"],
40 | },
41 | },
42 | {
43 | title: "Products",
44 | name: "products",
45 | description: "The products for this list item",
46 | type: "array",
47 | of: [{type: "reference",
48 | to: [
49 | {type: "product"}
50 | ]
51 | }]
52 | }
53 | ],
54 | initialValue: {
55 | orientation: "horizontal",
56 | productDisplaySize: "small",
57 | }
58 | }
59 |
60 |
--------------------------------------------------------------------------------
/template/studio/schemas/pages/productCardFeature.js:
--------------------------------------------------------------------------------
1 | export default {
2 | title: "Product Card Feature",
3 | name: "productCardFeature",
4 | type: "object",
5 | fields: [
6 | {
7 | title: "Title",
8 | name: "title",
9 | type: "string",
10 | description: "The title of this block",
11 | validation: (Rule) => Rule.required(),
12 | },
13 | {
14 | title: "Text",
15 | name: "text",
16 | description: "The text of this block",
17 | type: 'array',
18 | of: [{type: 'block'}],
19 | },
20 | {
21 | title: "Products",
22 | name: "products",
23 | description: "The products for this list item",
24 | type: "array",
25 | of: [{type: "reference",
26 | to: [
27 | {type: "product"}
28 | ]
29 | }]
30 | }
31 | ],
32 | }
33 |
--------------------------------------------------------------------------------
/template/studio/schemas/pages/productsDisplay.js:
--------------------------------------------------------------------------------
1 | export default {
2 | title: "Products Display",
3 | name: "productsDisplay",
4 | type: "document",
5 | fields: [
6 | {
7 | title: 'Copy',
8 | name: 'copy',
9 | description: 'Text to displays alongside the product; filling out this field will make products display to the right side of your text. Leave blank to ensure products display in their own row.',
10 | type: 'array',
11 | of: [{type: 'block'}]
12 | },
13 | {
14 | title: "Products",
15 | name: "products",
16 | description: "The products for this list item",
17 | type: "array",
18 | of: [{type: "reference",
19 | to: [
20 | {type: "product"}
21 | ]
22 | }]
23 | }
24 | ],
25 | }
26 |
--------------------------------------------------------------------------------
/template/studio/schemas/pages/solidBlockFeature.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default {
4 | title: "Solid Block Feature",
5 | name: "solidBlockFeature",
6 | type: "object",
7 | fields: [
8 | {
9 | title: "Title",
10 | name: "title",
11 | type: "string",
12 | description: "The title of this block",
13 | validation: (Rule) => Rule.required(),
14 | },
15 | {
16 | title: "Text",
17 | name: "text",
18 | description: "The text of this block",
19 | type: 'array',
20 | of: [{type: 'block'}],
21 | },
22 | {
23 | title: "Image",
24 | name: "image",
25 | type: "image",
26 | description: "The image to be used for this block",
27 | options: {
28 | crop: true,
29 | hotspot: true
30 | }
31 | },
32 | //TODO: reference to article, product, or campaign
33 | {
34 | title: "Text Orientation",
35 | name: "orientation",
36 | type: "string",
37 | description: "The orientation of the text on the block (e.g, should text appear to the left or right of the image?",
38 | options: {
39 | list: ["left", "right"],
40 | },
41 | },
42 | {
43 | title: "Text Color",
44 | name: "textColor",
45 | type: "color",
46 | description: "The color that text should appear in",
47 | },
48 | {
49 | title: "Block Color",
50 | name: "blockColor",
51 | type: "color",
52 | description: "The background color of the block",
53 | },
54 | ]
55 | }
56 |
57 |
--------------------------------------------------------------------------------
/template/studio/schemas/pages/textOverlayFeature.js:
--------------------------------------------------------------------------------
1 | export default {
2 | title: "Text Overlay Feature",
3 | name: "textOverlayFeature",
4 | type: "object",
5 | fields: [
6 | {
7 | title: "Title",
8 | name: "title",
9 | type: "string",
10 | description: "The title of this block",
11 | validation: (Rule) => Rule.required(),
12 | },
13 | {
14 | title: "Text",
15 | name: "text",
16 | description: "The text of this block",
17 | type: 'array',
18 | of: [{type: 'block'}],
19 | },
20 | {
21 | title: "Image",
22 | name: "image",
23 | type: "image",
24 | description: "The image to be used for this block",
25 | options: {
26 | crop: true,
27 | hotspot: true
28 | }
29 | },
30 | ]
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/template/studio/schemas/schema.js:
--------------------------------------------------------------------------------
1 | import createSchema from 'part:@sanity/base/schema-creator'
2 | import schemaTypes from 'all:part:@sanity/base/schema-type'
3 |
4 | import excerptPortableText from './objects/excerptPortableText'
5 | import article from './documents/article'
6 | import person from './documents/person'
7 | import category from './documents/category'
8 | import subsection from './documents/subsection'
9 | import product from './documents/product'
10 | import campaign from './documents/campaign'
11 | import siteSettings from './documents/siteSettings'
12 |
13 | import listItem from './pages/listItem'
14 | import hr from './pages/hr'
15 | import productsDisplay from './pages/productsDisplay'
16 | import solidBlockFeature from './pages/solidBlockFeature'
17 | import textOverlayFeature from './pages/textOverlayFeature'
18 | import productCardFeature from './pages/productCardFeature'
19 |
20 | // Then we give our schema to the builder and provide the result to Sanity
21 | export default createSchema({
22 | // We name our schema
23 | name: 'default',
24 | // Then proceed to concatenate our document type
25 | // to the ones provided by any plugins that are installed
26 | types: schemaTypes
27 | .concat([
28 | excerptPortableText,
29 | article,
30 | person,
31 | category,
32 | subsection,
33 | product,
34 | campaign,
35 | siteSettings,
36 | productsDisplay,
37 | listItem,
38 | hr,
39 | solidBlockFeature,
40 | textOverlayFeature,
41 | productCardFeature
42 | ])
43 | })
44 |
--------------------------------------------------------------------------------
/template/studio/schemas/utils.js:
--------------------------------------------------------------------------------
1 | import client from 'part:@sanity/base/client'
2 |
3 | export function slugifier(input) {
4 | const query = '*[_id == $id][0]'
5 | const params = {id: input._ref}
6 | return client.fetch(query, params).then(doc => {
7 | return doc.title.toLowerCase().replace(/\s+/g, '-').slice(0, 200);
8 | });
9 | }
10 |
--------------------------------------------------------------------------------
/template/studio/src/bigCommerceSync.js:
--------------------------------------------------------------------------------
1 | const fetch = require('node-fetch')
2 | import Schema from '@sanity/schema'
3 | import blockTools from '@sanity/block-tools'
4 | const jsdom = require('jsdom')
5 | const fileSystem = require( "fs" );
6 | const ndjson = require( "ndjson" );
7 | const {JSDOM} = jsdom
8 |
9 | const storeHash = process.env.SANITY_STUDIO_BIGCOMMERCE_STORE_HASH
10 | const pageSize = 1000
11 | let cursorStr = ""
12 |
13 | const slugify = (input) => input
14 | .toLowerCase()
15 | .replace(/\s+/g, '-')
16 | .slice(0, 200)
17 |
18 | const miniProductSchema = Schema.compile({
19 | name: 'site',
20 | types: [
21 | {type: 'object',
22 | name: 'product',
23 | fields: [
24 | {
25 | title: 'Description',
26 | name: 'description',
27 | type: 'array',
28 | of: [{type: 'block'}]
29 | }
30 | ]}
31 | ]
32 | })
33 |
34 | const blockContentType = miniProductSchema.get('product')
35 | .fields.find(field => field.name === 'description').type
36 |
37 | const paginatedProductQuery = `
38 | query paginateProducts(
39 | $pageSize: Int = ${pageSize}
40 | $cursor: String ${cursorStr || ""}
41 | ) {
42 | site {
43 | products (first: $pageSize, after:$cursor) {
44 | pageInfo {
45 | startCursor
46 | endCursor
47 | }
48 | edges {
49 | cursor
50 | node {
51 | entityId
52 | name
53 | sku
54 | description
55 | prices {
56 | price {
57 | currencyCode
58 | value
59 | }
60 | }
61 | defaultImage {
62 | urlOriginal
63 | }
64 | brand {
65 | name
66 | }
67 | }
68 | }
69 | }
70 | }
71 | }
72 | `
73 |
74 | const getToken = async () => (
75 | fetch(`https://api.bigcommerce.com/stores/${storeHash}/v3/storefront/api-token`, {
76 | method: 'POST',
77 | headers: {
78 | 'Content-Type': 'application/json',
79 | 'X-Auth-Token': process.env.SANITY_STUDIO_BIGCOMMERCE_STORE_API_TOKEN
80 | },
81 | body: JSON.stringify({
82 | channel_id: 1,
83 | expires_at: Math.floor((Date.now() / 1000)) + 3600,
84 | allowed_cors_origins: ["http://localhost:3333"]
85 | })
86 | })
87 | .then(res => res.json())
88 | .then(res => res.data.token)
89 | )
90 |
91 |
92 | const getProducts = async (token) => (
93 | fetch(`https://store-${storeHash}.mybigcommerce.com/graphql`, {
94 | method: 'POST',
95 | headers: {
96 | 'Content-Type': 'application/json',
97 | 'Authorization': `Bearer ${token}`
98 | },
99 | body: JSON.stringify({
100 | query: paginatedProductQuery
101 | })
102 | })
103 | // .then(res => console.log(res))
104 | .then(res => res.json())
105 | .then(res => res.data)
106 | )
107 |
108 | const reshapeNode = ({node}) => {
109 | return (
110 | {
111 | _id: `imported-BC-${node.entityId}`,
112 | _type: 'product',
113 | name: node.name,
114 | slug: {current: slugify(node.name)},
115 | sku: node.sku,
116 | description: blockTools.htmlToBlocks(
117 | node.description || "",
118 | blockContentType,
119 | {parseHtml: (html) => new JSDOM(html).window.document}
120 | ),
121 | price: node.prices?.price?.value,
122 | manufacturer: node.brand?.name,
123 | productImage: {
124 | _type: 'image',
125 | _sanityAsset: `image@${node.defaultImage?.urlOriginal}`
126 | }
127 | })
128 | }
129 |
130 |
131 |
132 | const main = async () => {
133 | const token = await getToken()
134 | let {site: {products: {pageInfo, edges}}} = await getProducts(token)
135 |
136 | const transformStream = ndjson.stringify();
137 | const outputStream = transformStream.pipe(
138 | fileSystem.createWriteStream( __dirname + "/data.ndjson" ) );
139 |
140 | cursorStr = `= ${pageInfo.endCursor}`
141 | const transformed = edges.map(reshapeNode)
142 | transformed.forEach(document => {
143 | transformStream.write(document)
144 | })
145 |
146 | transformStream.end();
147 | outputStream.on(
148 | "finish",
149 | function handleFinish() {
150 | console.log(`dumped to ${ __dirname}/data.ndjson!`);
151 | }
152 | )
153 | }
154 |
155 | main()
156 |
--------------------------------------------------------------------------------
/template/studio/src/deskStructure.js:
--------------------------------------------------------------------------------
1 | import S from '@sanity/desk-tool/structure-builder'
2 | import client from 'part:@sanity/base/client'
3 | import { MdEdit,
4 | MdRemoveRedEye,
5 | MdStayPrimaryPortrait,
6 | MdTune,
7 | MdFormatAlignLeft,
8 | MdPerson,
9 | MdShoppingCart,
10 | MdTrendingUp,
11 | MdSettings
12 | } from "react-icons/md"
13 | import { IFramePreview, MobilePreview } from './preview'
14 | import React from 'react'
15 |
16 | const BASE_PREVIEW_URL = (process.env.NODE_ENV == 'production') ?
17 | '../../' : 'http://localhost:3000'
18 |
19 | async function categoriesToListItems() {
20 | const query = `*[_type=='category' && !(_id in path("drafts.**"))]{
21 | name, _id, slug,
22 | 'subsections': *[_type=='subsection' && references(^._id) && !(_id in path("drafts.**"))]{name, _id, slug}
23 | }`
24 | const categories = await client.fetch(query)
25 |
26 | return categories.map(cat => (
27 | S.listItem()
28 | .title(cat.name)
29 | .id(cat._id)
30 | .child(
31 | S.list()
32 | .initialValueTemplates([
33 | S.initialValueTemplateItem('subsection-with-category', {categoryId: cat._id})
34 | ])
35 | .title(cat.name)
36 | .id(cat._id)
37 | .items([
38 | S.documentListItem()
39 | .schemaType('category')
40 | .title(`${cat.name} Hub Options`)
41 | .id(cat._id)
42 | .child(
43 | S.document()
44 | .schemaType('category')
45 | .documentId(cat._id)
46 | .views([
47 | S.view.form(),
48 | S.view.component(document => IFramePreview(BASE_PREVIEW_URL, document))
49 | .title('Web Preview')
50 | .icon(MdRemoveRedEye),
51 | S.view.component(document => MobilePreview(BASE_PREVIEW_URL, document))
52 | .title('Mobile Preview')
53 | .icon(MdStayPrimaryPortrait)
54 | ])
55 | ),
56 | ...createSubsectionListItems(cat.slug.current, cat.subsections)
57 | ])
58 | )
59 | )
60 | )
61 | }
62 |
63 | function createSubsectionListItems(categorySlug, subsections) {
64 | return subsections.map(sub => (
65 | S.listItem()
66 | .title(sub.name)
67 | .id(sub._id)
68 | .child(
69 | S.documentTypeList('article')
70 | .title(sub.name)
71 | .filter("subsection._ref == $id")
72 | .params({id: sub._id})
73 | .initialValueTemplates([
74 | S.initialValueTemplateItem('article-with-subsection', {subsectionId: sub._id})
75 | ])
76 | .child(id =>
77 | S.document()
78 | .schemaType('article')
79 | .documentId(id)
80 | .views([
81 | S.view.form(),
82 | S.view.component(document =>
83 | IFramePreview(BASE_PREVIEW_URL, document, `${categorySlug}/${sub.slug.current}`))
84 | .title('Web Preview')
85 | .icon(MdRemoveRedEye),
86 | S.view.component(document =>
87 | MobilePreview(BASE_PREVIEW_URL, document, `${categorySlug}/${sub.slug.current}`))
88 | .title('Mobile Preview')
89 | .icon(MdStayPrimaryPortrait)
90 | ])
91 | )
92 | )
93 | )
94 | )
95 | }
96 |
97 | async function buildList() {
98 | return (
99 | S.list()
100 | .title('Content')
101 | .items([
102 | S.listItem()
103 | .title('Site settings')
104 | .icon(MdTune)
105 | .child(
106 | S.editor()
107 | .schemaType('siteSettings')
108 | .documentId('siteSettings')
109 | .title('Index Page Settings')
110 | .views([
111 | S.view.form(),
112 | S.view.component(document =>
113 |
117 | )
118 | .title('Web Preview')
119 | .icon(MdRemoveRedEye)
120 | ])
121 | ),
122 | S.documentTypeListItem('subsection')
123 | .title('Subsection options')
124 | .icon(MdSettings),
125 | S.divider(),
126 | ...await categoriesToListItems(),
127 | S.divider(),
128 | S.documentTypeListItem('person')
129 | .title('Authors')
130 | .icon(MdPerson),
131 | S.divider(),
132 | S.listItem()
133 | .title('Shop')
134 | .icon(MdShoppingCart)
135 | .child(
136 | S.documentTypeList('product')
137 | .child(id =>
138 | S.document()
139 | .schemaType('product')
140 | .documentId(id)
141 | .views([
142 | S.view.form(),
143 | S.view.component(document => IFramePreview(
144 | BASE_PREVIEW_URL, document, 'shop'))
145 | .title('Web Preview')
146 | .icon(MdRemoveRedEye),
147 | S.view.component(document => MobilePreview(
148 | BASE_PREVIEW_URL, document, 'shop'))
149 | .title('Mobile Preview')
150 | .icon(MdStayPrimaryPortrait)
151 | ])
152 | )
153 | ),
154 | S.documentTypeListItem('campaign')
155 | .title('Campaigns')
156 | .icon(MdTrendingUp)
157 | .child(
158 | S.documentTypeList('campaign')
159 | .child(id =>
160 | S.document()
161 | .schemaType('campaign')
162 | .documentId(id)
163 | .views([
164 | S.view.form(),
165 | S.view.component(document => IFramePreview(
166 | BASE_PREVIEW_URL, document, 'shop/campaign'))
167 | .title('Web Preview')
168 | .icon(MdRemoveRedEye),
169 | S.view.component(document => MobilePreview(
170 | BASE_PREVIEW_URL, document, 'shop/campaign'))
171 | .title('Mobile Preview')
172 | .icon(MdStayPrimaryPortrait)
173 | ])
174 | )
175 | )
176 | ])
177 | )
178 | }
179 |
180 | export default buildList;
181 |
--------------------------------------------------------------------------------
/template/studio/src/initialValueTemplates.js:
--------------------------------------------------------------------------------
1 | import T from '@sanity/base/initial-value-template-builder'
2 |
3 | export default [
4 | ...T.defaults(),
5 | T.template({
6 | id: 'article-with-subsection',
7 | title: 'Article With Subsection',
8 | schemaType: 'article',
9 | parameters: [
10 | {
11 | name: 'subsectionId',
12 | title: 'Subsection ID',
13 | type: 'string'
14 | }
15 | ],
16 | value: params => ({
17 | subsection: {_type: 'reference', _ref: params.subsectionId}
18 | })
19 | }),
20 | T.template({
21 | id: 'subsection-with-category',
22 | title: 'Subsection With Category',
23 | schemaType: 'subsection',
24 | parameters: [
25 | {
26 | name: 'categoryId',
27 | title: 'Category ID',
28 | type: 'string'
29 | }
30 | ],
31 | value: params => ({
32 | category: {_type: 'reference', _ref: params.categoryId}
33 | })
34 | })
35 | ]
36 |
37 |
38 |
--------------------------------------------------------------------------------
/template/studio/src/preview.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import SanityMobilePreview from 'sanity-mobile-preview'
3 | import 'sanity-mobile-preview/dist/index.css?raw'
4 |
5 | export function IFramePreview(baseUrl, {document: {displayed: { slug = {}}}}, prefix){
6 |
7 | if (!slug || typeof(slug) == 'undefined') {
8 | return (
9 |
10 |
Please create a slug first for this document.
11 |
12 | )
13 | }
14 |
15 | let url;
16 |
17 | if (prefix && typeof(prefix) != 'undefined') {
18 | url = `${baseUrl}/${prefix}/${slug.current}?preview=true`
19 | } else {
20 | url = `${baseUrl}/${slug.current}?preview=true`
21 | }
22 |
23 | return (
24 |
28 | )
29 | }
30 |
31 | export function MobilePreview(baseUrl, document, prefix) {
32 | return (
33 |
34 | { IFramePreview(baseUrl, document, prefix) }
35 |
36 | )
37 | }
38 |
--------------------------------------------------------------------------------
/template/studio/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | // Note: This config is only used to help editors like VS Code understand/resolve
3 | // parts, the actual transpilation is done by babel. Any compiler configuration in
4 | // here will be ignored.
5 | "include": ["./node_modules/@sanity/base/types/**/*.ts", "./**/*.ts", "./**/*.tsx"]
6 | }
7 |
--------------------------------------------------------------------------------
/template/styles/Home.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | min-height: 100vh;
3 | padding: 0 0.5rem;
4 | display: flex;
5 | flex-direction: column;
6 | justify-content: center;
7 | align-items: center;
8 | }
9 |
10 | .main {
11 | padding: 5rem 0;
12 | flex: 1;
13 | display: flex;
14 | flex-direction: column;
15 | justify-content: center;
16 | align-items: center;
17 | }
18 |
19 | .footer {
20 | width: 100%;
21 | height: 100px;
22 | border-top: 1px solid #eaeaea;
23 | display: flex;
24 | justify-content: center;
25 | align-items: center;
26 | }
27 |
28 | .footer img {
29 | margin-left: 0.5rem;
30 | }
31 |
32 | .footer a {
33 | display: flex;
34 | justify-content: center;
35 | align-items: center;
36 | }
37 |
38 | .title a {
39 | color: #0070f3;
40 | text-decoration: none;
41 | }
42 |
43 | .title a:hover,
44 | .title a:focus,
45 | .title a:active {
46 | text-decoration: underline;
47 | }
48 |
49 | .title {
50 | margin: 0;
51 | line-height: 1.15;
52 | font-size: 4rem;
53 | }
54 |
55 | .title,
56 | .description {
57 | text-align: center;
58 | }
59 |
60 | .description {
61 | line-height: 1.5;
62 | font-size: 1.5rem;
63 | }
64 |
65 | .code {
66 | background: #fafafa;
67 | border-radius: 5px;
68 | padding: 0.75rem;
69 | font-size: 1.1rem;
70 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
71 | Bitstream Vera Sans Mono, Courier New, monospace;
72 | }
73 |
74 | .grid {
75 | display: flex;
76 | align-items: center;
77 | justify-content: center;
78 | flex-wrap: wrap;
79 | max-width: 800px;
80 | margin-top: 3rem;
81 | }
82 |
83 | .card {
84 | margin: 1rem;
85 | flex-basis: 45%;
86 | padding: 1.5rem;
87 | text-align: left;
88 | color: inherit;
89 | text-decoration: none;
90 | border: 1px solid #eaeaea;
91 | border-radius: 10px;
92 | transition: color 0.15s ease, border-color 0.15s ease;
93 | }
94 |
95 | .card:hover,
96 | .card:focus,
97 | .card:active {
98 | color: #0070f3;
99 | border-color: #0070f3;
100 | }
101 |
102 | .card h3 {
103 | margin: 0 0 1rem 0;
104 | font-size: 1.5rem;
105 | }
106 |
107 | .card p {
108 | margin: 0;
109 | font-size: 1.25rem;
110 | line-height: 1.5;
111 | }
112 |
113 | .logo {
114 | height: 1em;
115 | }
116 |
117 | @media (max-width: 600px) {
118 | .grid {
119 | width: 100%;
120 | flex-direction: column;
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/template/styles/globals.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | padding: 0;
4 | margin: 0;
5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
7 | }
8 |
9 | a {
10 | color: inherit;
11 | text-decoration: none;
12 | }
13 |
14 |
15 | * {
16 | box-sizing: border-box;
17 | }
18 |
19 |
--------------------------------------------------------------------------------
/template/theme/color.ts:
--------------------------------------------------------------------------------
1 | import {studioTheme} from '@sanity/ui'
2 |
3 | export const color = studioTheme.color
4 |
--------------------------------------------------------------------------------
/template/theme/fonts.ts:
--------------------------------------------------------------------------------
1 | import {ThemeFonts} from '@sanity/ui'
2 |
3 | export const fonts: ThemeFonts = {
4 | code: {
5 | family: '-apple-system-ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace',
6 | weights: {
7 | regular: 500,
8 | medium: 600,
9 | semibold: 700,
10 | bold: 800,
11 | },
12 | sizes: [
13 | {
14 | ascenderHeight: 3,
15 | descenderHeight: 3,
16 | fontSize: 10,
17 | iconSize: 17,
18 | lineHeight: 13,
19 | letterSpacing: 0,
20 | },
21 | {
22 | ascenderHeight: 4,
23 | descenderHeight: 4,
24 | fontSize: 13,
25 | iconSize: 21,
26 | lineHeight: 17,
27 | letterSpacing: 0,
28 | },
29 | {
30 | ascenderHeight: 5,
31 | descenderHeight: 5,
32 | fontSize: 16,
33 | iconSize: 25,
34 | lineHeight: 21,
35 | letterSpacing: 0,
36 | },
37 | {
38 | ascenderHeight: 6,
39 | descenderHeight: 6,
40 | fontSize: 19,
41 | iconSize: 29,
42 | lineHeight: 25,
43 | letterSpacing: 0,
44 | },
45 | {
46 | ascenderHeight: 7,
47 | descenderHeight: 7,
48 | fontSize: 22,
49 | iconSize: 33,
50 | lineHeight: 29,
51 | letterSpacing: 0,
52 | },
53 | ],
54 | },
55 | heading: {
56 | family:
57 | 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif',
58 | weights: {
59 | regular: 700,
60 | medium: 800,
61 | semibold: 900,
62 | bold: 900,
63 | },
64 | sizes: [
65 | {
66 | ascenderHeight: 3,
67 | descenderHeight: 3,
68 | fontSize: 16,
69 | iconSize: 17,
70 | lineHeight: 25,
71 | letterSpacing: 0,
72 | },
73 | {
74 | ascenderHeight: 5,
75 | descenderHeight: 5,
76 | fontSize: 24,
77 | iconSize: 33,
78 | lineHeight: 33,
79 | letterSpacing: 0,
80 | },
81 | {
82 | ascenderHeight: 6,
83 | descenderHeight: 6,
84 | fontSize: 33,
85 | iconSize: 41,
86 | lineHeight: 41,
87 | letterSpacing: 0,
88 | },
89 | {
90 | ascenderHeight: 7,
91 | descenderHeight: 7,
92 | fontSize: 42,
93 | iconSize: 49,
94 | lineHeight: 49,
95 | letterSpacing: 0,
96 | },
97 | {
98 | ascenderHeight: 11,
99 | descenderHeight: 11,
100 | fontSize: 49,
101 | iconSize: 57,
102 | lineHeight: 57,
103 | letterSpacing: 0,
104 | },
105 | {
106 | ascenderHeight: 15,
107 | descenderHeight: 15,
108 | fontSize: 59,
109 | iconSize: 65,
110 | lineHeight: 57,
111 | letterSpacing: 0,
112 | },
113 | ],
114 | },
115 | label: {
116 | family:
117 | 'Roboto Mono, Inter var, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif',
118 | weights: {
119 | regular: 600,
120 | medium: 700,
121 | semibold: 800,
122 | bold: 900,
123 | },
124 | sizes: [
125 | {
126 | ascenderHeight: 2,
127 | descenderHeight: 2,
128 | fontSize: 10,
129 | iconSize: 17,
130 | lineHeight: 11,
131 | letterSpacing: 0.5,
132 | },
133 | {
134 | ascenderHeight: 2,
135 | descenderHeight: 2,
136 | fontSize: 11,
137 | iconSize: 19,
138 | lineHeight: 12,
139 | letterSpacing: 0.5,
140 | },
141 | {
142 | ascenderHeight: 3,
143 | descenderHeight: 3,
144 | fontSize: 13,
145 | iconSize: 21,
146 | lineHeight: 15,
147 | letterSpacing: 0.5,
148 | },
149 | {
150 | ascenderHeight: 4,
151 | descenderHeight: 3,
152 | fontSize: 14,
153 | iconSize: 23,
154 | lineHeight: 17,
155 | letterSpacing: 0.5,
156 | },
157 | {
158 | ascenderHeight: 2,
159 | descenderHeight: 2,
160 | fontSize: 8.5,
161 | iconSize: 15,
162 | lineHeight: 10,
163 | letterSpacing: 0.5,
164 | },
165 | ],
166 | },
167 | text: {
168 | family:
169 | 'Inter var, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif',
170 | weights: {
171 | regular: 500,
172 | medium: 600,
173 | semibold: 700,
174 | bold: 800,
175 | },
176 | sizes: [
177 | {
178 | ascenderHeight: 5,
179 | descenderHeight: 5,
180 | fontSize: 10,
181 | iconSize: 17,
182 | lineHeight: 17,
183 | letterSpacing: 0,
184 | },
185 | {
186 | ascenderHeight: 6,
187 | descenderHeight: 6,
188 | fontSize: 12,
189 | iconSize: 21,
190 | lineHeight: 21,
191 | letterSpacing: 0,
192 | },
193 | {
194 | ascenderHeight: 7,
195 | descenderHeight: 7,
196 | fontSize: 16,
197 | iconSize: 25,
198 | lineHeight: 25,
199 | letterSpacing: 0,
200 | },
201 | {
202 | ascenderHeight: 8,
203 | descenderHeight: 8,
204 | fontSize: 18,
205 | iconSize: 29,
206 | lineHeight: 29,
207 | letterSpacing: 0,
208 | },
209 | {
210 | ascenderHeight: 9,
211 | descenderHeight: 9,
212 | fontSize: 21,
213 | iconSize: 33,
214 | lineHeight: 33,
215 | letterSpacing: 0,
216 | },
217 | ],
218 | },
219 | }
220 |
--------------------------------------------------------------------------------
/template/theme/index.ts:
--------------------------------------------------------------------------------
1 | export * from './theme'
2 |
--------------------------------------------------------------------------------
/template/theme/theme.ts:
--------------------------------------------------------------------------------
1 | import {RootTheme} from '@sanity/ui'
2 | import {color} from './color'
3 | import {fonts} from './fonts'
4 |
5 | export const sanityTheme: RootTheme = {
6 | avatar: {
7 | sizes: [
8 | {distance: -3, size: 23},
9 | {distance: -6, size: 35},
10 | {distance: -9, size: 55},
11 | ],
12 | },
13 | button: {
14 | textWeight: 'medium',
15 | },
16 | color,
17 | container: [480, 960, 1440, 1920, 2400],
18 | focusRing: {
19 | offset: 1,
20 | width: 2,
21 | },
22 | fonts,
23 | media: [320, 640, 960, 1280, 1600, 1920],
24 | radius: [0, 1, 3, 6, 9, 12, 21],
25 | shadows: [
26 | null,
27 | // 1
28 | // {umbra: [0, 2, 1, -1], penumbra: [0, 1, 1, 0], ambient: [0, 1, 3, 0]},
29 | {umbra: [0, 0, 0, 0], penumbra: [0, 0, 0, 0], ambient: [0, 0, 0, 0]},
30 | // 6
31 | {umbra: [0, 3, 5, -1], penumbra: [0, 6, 10, 0], ambient: [0, 1, 18, 0]},
32 | // 12
33 | {umbra: [0, 7, 8, -4], penumbra: [0, 12, 17, 2], ambient: [0, 5, 22, 4]},
34 | // 18
35 | {umbra: [0, 9, 11, -5], penumbra: [0, 18, 28, 2], ambient: [0, 7, 34, 6]},
36 | // 24
37 | {umbra: [0, 11, 15, -7], penumbra: [0, 24, 38, 3], ambient: [0, 9, 46, 8]},
38 | ],
39 | space: [0, 8, 12, 20, 32, 52, 84, 136, 220, 356],
40 | input: {
41 | border: {
42 | width: 1,
43 | },
44 | checkbox: {
45 | size: 17,
46 | },
47 | radio: {
48 | size: 17,
49 | markSize: 9,
50 | },
51 | switch: {
52 | width: 33,
53 | height: 17,
54 | padding: 4,
55 | transitionDurationMs: 150,
56 | transitionTimingFunction: 'ease-out',
57 | },
58 | },
59 | }
60 |
--------------------------------------------------------------------------------
/template/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "strict": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "noEmit": true,
14 | "esModuleInterop": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "jsx": "preserve",
20 | "baseUrl": ".",
21 | "paths": {
22 | "$components": [
23 | "./components"
24 | ],
25 | "$theme": [
26 | "./theme"
27 | ],
28 | "$utils": [
29 | "./utils"
30 | ],
31 | "$utils/*": [
32 | "./utils/*"
33 | ]
34 | },
35 | "incremental": true
36 | },
37 | "include": [
38 | "next-env.d.ts",
39 | "**/*.ts",
40 | "**/*.tsx"
41 | ],
42 | "exclude": [
43 | "node_modules"
44 | ]
45 | }
46 |
--------------------------------------------------------------------------------
/template/types.d.ts:
--------------------------------------------------------------------------------
1 | export type Category = {
2 | name: string,
3 | slug: string
4 | }
5 |
6 | export type Subsection = {
7 | name: string,
8 | slug: string
9 | }
10 |
11 | export type Color = {
12 | hex: string
13 | }
14 |
15 | export type ImageAsset = {
16 | _ref: string
17 | }
18 |
19 | export type Image = {
20 | asset: ImageAsset
21 | crop: any,
22 | hotspot: any
23 | }
24 |
25 | export type Product = {
26 | _id: string,
27 | image: Image,
28 | name: string,
29 | slug: string,
30 | description: string,
31 | price: string,
32 | manufacturer: string,
33 | relatedArticles: Article[],
34 | quantity: number?,
35 | lineID: string?
36 | }
37 |
38 | export type Article = {
39 | _id: string?,
40 | title: string,
41 | slug: string,
42 | image: Image,
43 | subsection: Subsection,
44 | category: Category,
45 | publishedDate: string, //comes in from Sanity this way
46 | storyProducts: StoryProducts[],
47 | excerpt: any[] | any?,
48 | content: any[] | any?
49 | }
50 |
51 | export type MonthArticle = {
52 | name: string,
53 | articles: Article[]
54 | }
55 |
56 | export type Feature = {
57 | title: string,
58 | text: any | any[],
59 | image: Image,
60 | url: string
61 | }
62 |
63 | export type ArticleSlug = {
64 | slug: string,
65 | subhub: string,
66 | hub: string
67 | }
68 |
69 | export type CategoryFeature = {
70 | categoryId: string,
71 | name: string,
72 | featuredArticleDisplay: string,
73 | featuredArticle: Article
74 | }
75 |
76 | export type SubsectionArticles = {
77 | name: string,
78 | slug: string?,
79 | articles: Article[]
80 | }
81 |
82 | export type ListItem = {
83 | _key: string,
84 | _type: string,
85 | orientation: string,
86 | text: any[] | any?,
87 | title: string,
88 | productDisplaySize: string,
89 | products: Product[]
90 | }
91 |
92 | export type StoryProducts = {
93 | products: Product[]
94 | }
95 |
96 | export type Campaign = {
97 | slug: string,
98 | image: Image,
99 | title: string,
100 | text: any[] | any?,
101 | content: any[] | any?,
102 | products: Product[],
103 | hideLeadBlock: boolean
104 | }
105 |
106 | export type Slug = {
107 | slug: string
108 | }
--------------------------------------------------------------------------------
/template/utils/helpers.js:
--------------------------------------------------------------------------------
1 | import {
2 | TextOverlayFeature,
3 | TextUnderFeature,
4 | SolidBlockFeature,
5 | ProductCardFeature,
6 | } from '../components'
7 |
8 | //grouped items might be list items, product displays, etc.
9 | export function handleGroupedItems(content, key, additionalFilter) {
10 | let finalBlocks = []
11 | let verticalGroup = {_key: null, _type: `${key}Group`, children: []}
12 | let startingItem = null
13 | if (!content) { return [] }
14 |
15 | content.forEach((block, i) => {
16 | if (block._type == key && block[additionalFilter._key] == additionalFilter._value) {
17 | if (!startingItem) {
18 | startingItem = i
19 | verticalGroup._key = `groupedItemContainer-${i}`
20 | }
21 | verticalGroup.children.push(block)
22 | } else {
23 | //verticals have ended, end the block
24 | if (startingItem) {
25 | //TODO: test if you really need the deep copy
26 | finalBlocks.push(JSON.parse(JSON.stringify(verticalGroup)))
27 | verticalGroup = {_key: null, _type: `${key}Group`, children: []}
28 | startingItem = null
29 | }
30 | finalBlocks.push(block)
31 | }
32 | })
33 |
34 | if (verticalGroup._key) {
35 | finalBlocks.push(JSON.parse(JSON.stringify(verticalGroup)))
36 | }
37 |
38 | return finalBlocks
39 | }
40 |
41 | export function handleBlockFeature(featureType, props, fullSize) {
42 | let featureDisplay;
43 |
44 | switch(featureType) {
45 | case 'textBelowFeature':
46 | featureDisplay =
47 | break;
48 | case 'textOverlayFeature':
49 | featureDisplay =
50 | break;
51 | case 'solidBlockFeature':
52 | featureDisplay =
53 | break;
54 | case 'productCardFeature':
55 | featureDisplay =
56 | break;
57 | default:
58 | featureDisplay =
59 | }
60 |
61 | return featureDisplay
62 |
63 | }
64 |
65 |
66 | export function handleLocaleField(fieldName, obj, locale) {
67 | if (!locale || locale == 'en-US') {
68 | return obj[fieldName]
69 | } else {
70 | const localeLabel = `locale_${locale}_${fieldName}`
71 | return obj[localeLabel] ?? obj[fieldName]
72 | }
73 | }
74 |
75 |
76 | //slightly different naming conventions and routing on articles and features, make them uniform!
77 | export function coalesceCampaignAndFeature(featuredArticle) {
78 | return {
79 | title: featuredArticle.title,
80 | text: (featuredArticle.excerpt) ? featuredArticle.excerpt : featuredArticle.text,
81 | url: (featuredArticle.category) ?`${featuredArticle.category.slug}/${featuredArticle.subsection.slug}/${featuredArticle.slug}` : `shop/campaign/${featuredArticle.slug}`,
82 | image: featuredArticle.image
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/template/utils/sanity.js:
--------------------------------------------------------------------------------
1 | import {
2 | createClient,
3 | createImageUrlBuilder,
4 | createPortableTextComponent,
5 | createPreviewSubscriptionHook
6 | } from "next-sanity";
7 |
8 | import {
9 | ListItemGroup,
10 | ListItemCard,
11 | ProductsDisplay,
12 | TextOverlayFeature
13 | } from '../components'
14 |
15 | const config = {
16 | projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
17 | dataset: process.env.NEXT_PUBLIC_SANITY_DATASET,
18 | useCdn: process.env.NODE_ENV === 'production',
19 |
20 | }
21 |
22 | export const sanityClient = createClient(config);
23 | //TODO: put placeholder "problem fetching image!" as default
24 | export const urlFor = (source) => createImageUrlBuilder(config).image(source);
25 |
26 | // Set up a preview client with serverless authentication for drafts
27 | export const previewClient = createClient({
28 | ...config,
29 | useCdn: false
30 | })
31 |
32 | // Helper function for easily switching between normal client and preview client
33 | export const getClient = (usePreview) => (usePreview ? previewClient : sanityClient)
34 |
35 | // Set up the live preview subsscription hook
36 | export const usePreviewSubscription = createPreviewSubscriptionHook(config);
37 |
38 | export const PortableText = createPortableTextComponent({
39 | ...config,
40 | serializers: {
41 | types: {
42 | listItemGroup: props => (),
43 | listItem: props => (),
44 | productsDisplay: props => (),
48 | hr: props => (
),
49 | textOverlayFeature: props => (),
50 | undefined: props => ()
51 | }
52 | },
53 | });
54 |
55 |
--------------------------------------------------------------------------------
/template/utils/sanityGroqQueries.js:
--------------------------------------------------------------------------------
1 | import { groq } from "next-sanity"
2 |
3 | /* ------------------------------------------------------------------ */
4 | /* Fragments -- partial queries for hubpages, inline references, etc */
5 | /* ------------------------------------------------------------------ */
6 |
7 | export const productQuery = `
8 | _id,
9 | 'slug': slug.current,
10 | 'image': {"asset": {"_ref": productImage.asset._ref}, "crop": productImage.crop, "hotspot": productImage.hotspot},
11 | name,
12 | description,
13 | price,
14 | manufacturer
15 | `
16 |
17 | export const articleDisplayQuery = `
18 | _id,
19 | title,
20 | text,
21 | "slug": slug.current,
22 | 'image': {"asset": {"_ref": heroImage.asset._ref}, "crop": heroImage.crop, "hotspot": heroImage.hotspot},
23 | "subsection": subsection->{name, "slug": slug.current},
24 | "category": subsection->category->{name, "slug": slug.current},
25 | excerpt,
26 | publishedDate
27 | `
28 |
29 | /* ------------ */
30 | /* Page queries */
31 | /* ------------ */
32 |
33 |
34 | export const indexQuery = `
35 | {"featuredArticles": *[_type == 'siteSettings'][0]{featuredArticles[]->{
36 | ${articleDisplayQuery}
37 | }}.featuredArticles[],
38 | "recentArticles": *[_type == 'article'] | order(publishedDate desc)[0...20]{
39 | ${articleDisplayQuery}
40 | },
41 | } `
42 |
43 | export const categoryAndFeaturedArticleQuery = groq`
44 | *[_type == 'category' && slug.current == $hub][0]
45 | {
46 | "categoryId": _id,
47 | name,
48 | featuredArticleDisplay,
49 | featuredArticle->{
50 | ${articleDisplayQuery}
51 | }
52 | }`
53 |
54 | export const subsectionArticleQueryHasFeature = groq`
55 | *[_type == 'subsection' && category._ref == $id]
56 | {
57 | name,
58 | "slug": slug.current,
59 | "articles": *[_type == 'article'
60 | && references(^._id) && _id != $featuredArticleId]
61 | | order('publishedDate' desc)[0...2]{
62 | ${articleDisplayQuery}
63 | }
64 | }`
65 |
66 | export const subsectionArticleQueryNoFeature = groq`
67 | *[_type == 'subsection' && category._ref == $id]
68 | {
69 | name,
70 | "slug": slug.current,
71 | "articles": *[_type == 'article' && references(^._id)]
72 | | order('publishedDate' desc)[0...2]{
73 | ${articleDisplayQuery}
74 | }
75 | }`
76 |
77 | export const subsectionArticleQuery = groq`
78 | *[_type == 'subsection' && slug.current == $slug][0]
79 | {
80 | name,
81 | "slug": slug.current,
82 | "articles": *[_type == 'article' && references(^._id)]
83 | | order('publishedDate' desc)[0...2]{
84 | ${articleDisplayQuery}
85 | }
86 | }`
87 |
88 | //need to format this way because of odd behavior in previewSubscription in next-sanity
89 | export const createArticlePageQuery = (slug) => `
90 | *[_type == "article" && slug.current == '${slug}'][0]{
91 | title,
92 | "slug": slug.current,
93 | "subsection": subsection->{name, "slug": slug.current},
94 | "category": subsection->category->{name, "slug": slug.current},
95 | "storyProducts": content[]{
96 | _type == 'listItem' || _type == 'productsDisplay'=>{
97 | products[]->{
98 | ${productQuery}
99 | }
100 | },
101 | },
102 | content[]{
103 | ...,
104 | _type == 'listItem' || _type == 'productsDisplay'=>{
105 | products[]->{
106 | ${productQuery}
107 | }
108 | },
109 | },
110 | "image": {"asset": heroImage.asset, "crop": heroImage.crop, "hotspot": heroImage.hotspot}
111 | }`
112 |
113 |
114 |
115 | /* ---------------- */
116 | /* Commerce queries */
117 | /* ---------------- */
118 |
119 | //for use in cart etc
120 | export const singleProductQuery = (id) => (
121 | `*[_type == 'product' && _id == '${id}'][0]
122 | { ${productQuery} }`
123 | )
124 |
125 | //TODO: break down into categories and serve in frontend that way
126 | export const shopQuery = `
127 | *[_type == 'product']{${productQuery}}
128 | `
129 |
130 | export const productDetailPageQuery = groq`
131 | *[_type == 'product' && slug.current == $slug][0]
132 | {
133 | ${productQuery},
134 | locale_es_name,
135 | locale_es_description,
136 | locale_fr_name,
137 | locale_fr_description,
138 | 'relatedArticles': *[_type == 'article' && references(^._id)][0..2]
139 | { ${articleDisplayQuery} }
140 | }
141 | `
142 |
143 | export const campaignQuery = groq`
144 | *[_type == 'campaign' && slug.current == $slug][0]
145 | {
146 | 'slug': slug.current,
147 | 'image': {"asset": {"_ref": heroImage.asset._ref}, "crop": heroImage.crop, "hotspot": heroImage.hotspot},
148 | title,
149 | text,
150 | hideLeadBlock,
151 | content[]{
152 | ...,
153 | _type == 'productCardFeature'=>{
154 | products[]->{${productQuery}},
155 | }
156 | },
157 | products[]->{
158 | ${productQuery}
159 | }
160 | }
161 | `
162 |
163 |
--------------------------------------------------------------------------------