├── studio ├── plugins │ └── .gitkeep ├── config │ ├── @sanity │ │ ├── data-aspects.json │ │ ├── form-builder.json │ │ ├── default-layout.json │ │ └── default-login.json │ └── .checksums ├── .gitignore ├── static │ ├── .gitkeep │ └── favicon.ico ├── structure │ ├── previews │ │ ├── IframePreview.css │ │ └── IframePreview.js │ ├── subscriptions.tsx │ ├── collections.tsx │ ├── redirects.tsx │ ├── docs.tsx │ ├── pages.tsx │ ├── views │ │ ├── docPreview.js │ │ └── preview.js │ ├── products.tsx │ ├── config.tsx │ └── variants.tsx ├── .eslintrc.js ├── schemas │ ├── modules │ │ ├── social.ts │ │ ├── moduleContent.ts │ │ ├── internalLink.ts │ │ ├── externalLink.ts │ │ ├── pageItem.ts │ │ ├── pageModule.ts │ │ ├── productGrid.ts │ │ ├── nestedPages.ts │ │ ├── imageModule.ts │ │ ├── variantModule.ts │ │ ├── shopifyProductModule.ts │ │ ├── shopifyVariantModule.ts │ │ ├── productModule.ts │ │ ├── socialLink.ts │ │ ├── defaultVariant.ts │ │ ├── metaCard.ts │ │ └── standardText.tsx │ ├── types │ │ ├── variant.ts │ │ ├── product.ts │ │ ├── siteGlobal.ts │ │ ├── doc.ts │ │ ├── page.ts │ │ ├── collection.ts │ │ ├── menus.ts │ │ ├── post.ts │ │ ├── redirect.ts │ │ └── subscription.ts │ ├── tabs │ │ ├── variantContent.ts │ │ ├── pageContent.ts │ │ ├── globalContent.ts │ │ └── productContent.ts │ ├── siteSettings.ts │ ├── blockText.ts │ ├── blockContent.ts │ └── schemas.ts ├── sanity.json ├── deskStructure.tsx ├── styles │ └── variables.css └── package.json ├── web ├── .prettierignore ├── src │ ├── images │ │ └── favicon │ │ │ └── apple-icon.png │ ├── styles │ │ ├── midway │ │ │ ├── _button.scss │ │ │ ├── _footer.scss │ │ │ ├── _global.scss │ │ │ ├── _cart.css │ │ │ ├── _nested-pages.scss │ │ │ ├── _learn.scss │ │ │ ├── products │ │ │ │ └── _card.scss │ │ │ ├── _code.scss │ │ │ └── _typography.scss │ │ ├── lib │ │ │ ├── _containers.scss │ │ │ ├── _borders.scss │ │ │ ├── _drawer.scss │ │ │ └── _config.scss │ │ └── main.scss │ ├── pages │ │ ├── __index.tsx │ │ ├── cart.tsx │ │ ├── docs.tsx │ │ └── previews.tsx │ ├── api │ │ ├── sanity.js │ │ └── queries.js │ ├── utils │ │ ├── schema.tsx │ │ ├── error.tsx │ │ ├── updateCustomer.tsx │ │ ├── renderModules.tsx │ │ └── serializer.tsx │ ├── templates │ │ ├── 404.tsx │ │ ├── documentation.tsx │ │ ├── page.tsx │ │ ├── collection.tsx │ │ ├── account.tsx │ │ └── product.tsx │ ├── components │ │ ├── auth │ │ │ ├── invalid_token.tsx │ │ │ ├── authWrapper.tsx │ │ │ ├── portal.tsx │ │ │ ├── orders.tsx │ │ │ ├── forgotPassword.tsx │ │ │ ├── login.tsx │ │ │ └── reset.tsx │ │ ├── interfaces │ │ │ └── product.ts │ │ ├── global │ │ │ ├── standardText.tsx │ │ │ ├── productGrid.tsx │ │ │ └── nestedPages.tsx │ │ ├── disclaimer.tsx │ │ ├── link.tsx │ │ ├── cart.tsx │ │ ├── modules.tsx │ │ ├── product │ │ │ ├── card.tsx │ │ │ ├── waitlist.tsx │ │ │ ├── hero.tsx │ │ │ └── schema.tsx │ │ ├── header.tsx │ │ ├── image.tsx │ │ ├── footer.tsx │ │ ├── newsletter.tsx │ │ ├── svgs.tsx │ │ ├── cart │ │ │ └── lineItem.tsx │ │ ├── SEO.tsx │ │ ├── cartDrawer.tsx │ │ └── analytics.tsx │ ├── lambda │ │ ├── .babelrc │ │ ├── logout.ts │ │ ├── back-in-stock.ts │ │ ├── orders.ts │ │ ├── activate.ts │ │ ├── forgot-password.ts │ │ ├── login.ts │ │ ├── reset-password.ts │ │ └── register.ts │ ├── stories │ │ ├── header.css │ │ ├── Header.stories.tsx │ │ ├── Page.stories.tsx │ │ ├── button.css │ │ ├── Button.stories.tsx │ │ ├── Button.tsx │ │ ├── assets │ │ │ ├── direction.svg │ │ │ ├── flow.svg │ │ │ ├── code-brackets.svg │ │ │ ├── comments.svg │ │ │ ├── repo.svg │ │ │ ├── plugin.svg │ │ │ └── stackalt.svg │ │ ├── page.css │ │ ├── Header.tsx │ │ └── Page.tsx │ ├── types │ │ └── shopifyTypes.ts │ ├── layouts │ │ ├── password.tsx │ │ └── index.tsx │ └── build │ │ └── createPages.js ├── api │ └── shopify.tsx ├── gatsby-ssr.js ├── netlify.toml ├── .eslintrc.js ├── .gitignore ├── env.example ├── migrations │ └── backup-sanity.js ├── tslint.json ├── LICENSE ├── gatsby-browser.js ├── gatsby-node.js ├── gatsby-config.js └── package.json ├── .editorconfig ├── .gitignore ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── LICENSE └── shopify └── src └── theme.liquid /studio/plugins/.gitkeep: -------------------------------------------------------------------------------- 1 | User-specific packages can be placed here 2 | -------------------------------------------------------------------------------- /studio/config/@sanity/data-aspects.json: -------------------------------------------------------------------------------- 1 | { 2 | "listOptions": {} 3 | } 4 | -------------------------------------------------------------------------------- /studio/.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /node_modules 3 | /yarn.lock 4 | /yarn-error.log 5 | -------------------------------------------------------------------------------- /web/.prettierignore: -------------------------------------------------------------------------------- 1 | .cache 2 | package.json 3 | package-lock.json 4 | public 5 | -------------------------------------------------------------------------------- /studio/config/@sanity/form-builder.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": { 3 | "directUploads": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /studio/static/.gitkeep: -------------------------------------------------------------------------------- 1 | Files placed here will be served by the Sanity server under the `/static`-prefix 2 | -------------------------------------------------------------------------------- /studio/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrl-alt-del-world/midway/HEAD/studio/static/favicon.ico -------------------------------------------------------------------------------- /studio/config/@sanity/default-layout.json: -------------------------------------------------------------------------------- 1 | { 2 | "toolSwitcher": { 3 | "order": [], 4 | "hidden": [] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /web/src/images/favicon/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrl-alt-del-world/midway/HEAD/web/src/images/favicon/apple-icon.png -------------------------------------------------------------------------------- /web/src/styles/midway/_button.scss: -------------------------------------------------------------------------------- 1 | .button { 2 | &:hover { 3 | background-color: white; 4 | span { 5 | color: black; 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /web/src/styles/midway/_footer.scss: -------------------------------------------------------------------------------- 1 | .footer { 2 | &__colophon { 3 | max-width: 30px; 4 | } 5 | &__newsletter { 6 | max-width: 360px; 7 | } 8 | } -------------------------------------------------------------------------------- /studio/config/@sanity/default-login.json: -------------------------------------------------------------------------------- 1 | { 2 | "providers": { 3 | "mode": "append", 4 | "redirectOnSingle": false, 5 | "entries": [] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /web/src/styles/midway/_global.scss: -------------------------------------------------------------------------------- 1 | .site { 2 | margin-top: 200px; // UPDATEME: Important note this should be modified depending on style needs + disclaimer removal 3 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /web/api/shopify.tsx: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import shop from 'shopify-buy' 3 | 4 | export default shop.buildClient({ 5 | domain: "slater.store", 6 | storefrontAccessToken: "b37ccdd79ec9c25620bc9d60f3b4c066", 7 | }) 8 | -------------------------------------------------------------------------------- /web/src/pages/__index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | 4 | const IndexPage = () => { 5 | return ( 6 |
7 | tight tacos 8 |
9 | ) 10 | } 11 | 12 | export default IndexPage 13 | -------------------------------------------------------------------------------- /web/gatsby-ssr.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Implement Gatsby's SSR (Server Side Rendering) APIs in this file. 3 | * 4 | * See: https://www.gatsbyjs.org/docs/ssr-apis/ 5 | */ 6 | 7 | // You can delete this file if you're not using it 8 | -------------------------------------------------------------------------------- /web/netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "gatsby build && netlify-lambda build src/lambda" 3 | functions = "./functions" 4 | 5 | [context.production] 6 | environment = { TYPE = 'production' } 7 | 8 | [context.staging] 9 | environment = { TYPE = 'staging' } 10 | -------------------------------------------------------------------------------- /web/src/api/sanity.js: -------------------------------------------------------------------------------- 1 | const sanityClient = require('@sanity/client'); 2 | 3 | module.exports = sanityClient({ 4 | projectId: process.env.GATSBY_SANITY_PROJECT_ID, 5 | dataset: process.env.GATSBY_SANITY_DATASET, 6 | token: process.env.SANITY_API_TOKEN, 7 | useCdn: false 8 | }); -------------------------------------------------------------------------------- /web/src/utils/schema.tsx: -------------------------------------------------------------------------------- 1 | import PasswordValidator from 'password-validator' 2 | 3 | export const PasswordSchema = new PasswordValidator() 4 | PasswordSchema 5 | .is() 6 | .min(8) 7 | .is() 8 | .max(100) 9 | .has() 10 | .lowercase() 11 | .has() 12 | .uppercase() 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac files 2 | .DS_Store 3 | 4 | # Dependency directories 5 | /node_modules 6 | /studio/node_modules 7 | /studio/sanity.json 8 | /web/node_modules 9 | 10 | # Ignore hidden files as default 11 | *.env 12 | .prettierrc 13 | 14 | web/public 15 | # Local Netlify folder 16 | .netlify -------------------------------------------------------------------------------- /studio/structure/previews/IframePreview.css: -------------------------------------------------------------------------------- 1 | .componentWrapper { 2 | padding: 1em; 3 | } 4 | 5 | .iframeContainer iframe { 6 | border: 0; 7 | height: 100%; 8 | left: 0; 9 | position: absolute; 10 | top: 0; 11 | width: 100%; 12 | } 13 | 14 | .content { 15 | padding: 1em; 16 | } -------------------------------------------------------------------------------- /studio/.eslintrc.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | extends: ['standard', 'standard-react'], 5 | rules: { 6 | 'react/prop-types': 0 7 | }, 8 | settings: { 9 | react: { 10 | pragma: 'React', 11 | version: '16.2.0' 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /studio/schemas/modules/social.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'Social', 3 | name: 'social', 4 | type: 'object', 5 | fields: [ 6 | { 7 | title: 'Social Links', 8 | name: 'socialLinks', 9 | type: 'array', 10 | of: [{ type: 'socialLink' }] 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /web/src/utils/error.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const ErrorHandling = ({error}: {error: string}) => { 4 | return ( 5 |
6 | 7 | ⚠️ 8 | 9 | : {error} 10 |
11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /web/src/styles/midway/_cart.css: -------------------------------------------------------------------------------- 1 | .cart { 2 | &__drawer { 3 | display: none; 4 | top: 180px; 5 | max-width: 400px; 6 | &.is-open { 7 | display: block; 8 | } 9 | &-header { 10 | border-bottom: 1px solid black; 11 | } 12 | &-close { 13 | right: 1rem; 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /web/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [`react-app`, "eslint:recommended"], 3 | plugins: [`graphql`], 4 | rules: { 5 | "import/no-webpack-loader-syntax": [0], 6 | "strict": "off", 7 | "no-unused-vars": [1], 8 | "no-case-declarations": [1], 9 | "react-hooks/exhaustive-deps": [0] 10 | }, 11 | } -------------------------------------------------------------------------------- /web/src/templates/404.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const browser = typeof window !== "undefined" && window 4 | 5 | const NotFoundPage = () => { 6 | return ( 7 | browser && ( 8 |
9 | Not Found 10 |
11 | ) 12 | ) 13 | } 14 | 15 | export default NotFoundPage 16 | -------------------------------------------------------------------------------- /web/src/components/auth/invalid_token.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import Helmet from "react-helmet" 3 | 4 | export const InvalidToken = ({path}: {path:string}) => { 5 | return
6 | 7 |
8 |

Error: Invalid Activation Token.

9 |
10 | } -------------------------------------------------------------------------------- /web/src/styles/midway/_nested-pages.scss: -------------------------------------------------------------------------------- 1 | .doc { 2 | &__block { 3 | a { 4 | transition: all 0.3s ease-in-out; 5 | &:hover { 6 | background-color:rgba(4, 24, 147, 0.1); 7 | } 8 | } 9 | p { 10 | &:last-child { 11 | margin-bottom: 0; 12 | padding-bottom: 0; 13 | } 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /web/src/lambda/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-typescript" 4 | ], 5 | "plugins": [ 6 | "@babel/plugin-proposal-class-properties", 7 | "@babel/plugin-transform-object-assign", 8 | "@babel/plugin-proposal-object-rest-spread", 9 | "@babel/plugin-proposal-optional-chaining" 10 | ], 11 | "ignore": [ 12 | "node_modules" 13 | ] 14 | } -------------------------------------------------------------------------------- /studio/schemas/modules/moduleContent.ts: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | title: 'Module Content', 4 | name: 'moduleContent', 5 | type: 'array', 6 | of: [ 7 | { 8 | type: 'imageModule' 9 | }, 10 | { 11 | type: 'standardText' 12 | }, 13 | { 14 | type: 'productGrid' 15 | }, 16 | { 17 | type: 'nestedPages' 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /studio/schemas/types/variant.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'productVariant', 3 | title: 'Variant', 4 | type: 'document', 5 | __experimental_actions: ['update', 'publish', 'delete'], 6 | fields: [ 7 | { 8 | name: "content", 9 | type: "variantContent" 10 | } 11 | ], 12 | preview: { 13 | select: { 14 | title: 'content.main.title', 15 | media: 'mainImage' 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /studio/schemas/types/product.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'product', 3 | title: 'Product', 4 | type: 'document', 5 | __experimental_actions: ['update', 'publish', 'delete'], 6 | fields: [ 7 | { 8 | name: "content", 9 | type: "productContent" 10 | } 11 | ], 12 | preview: { 13 | select: { 14 | title: 'content.main.title', 15 | media: 'content.main.mainImage' 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /web/src/pages/cart.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import cx from 'classnames' 3 | 4 | import { Cart } from 'src/components/cart' 5 | 6 | const CartPage = ({ transitionStatus }: { transitionStatus: string }) => { 7 | return ( 8 |
9 |
10 | 11 |
12 |
13 | ) 14 | } 15 | 16 | export default CartPage 17 | -------------------------------------------------------------------------------- /web/src/styles/lib/_containers.scss: -------------------------------------------------------------------------------- 1 | 2 | .container--600 { 3 | max-width: 600px; 4 | } 5 | 6 | .container--800 { 7 | max-width: 800px; 8 | } 9 | 10 | .container--900 { 11 | max-width: 900px; 12 | } 13 | 14 | .container--1000 { 15 | max-width: 1000px; 16 | } 17 | 18 | .container--l { 19 | max-width: 1224px; 20 | } 21 | 22 | .container--xl { 23 | max-width: 1440px; 24 | } 25 | 26 | .container--xxl { 27 | max-width: calc(100vw - 4rem); 28 | } -------------------------------------------------------------------------------- /web/src/styles/midway/_learn.scss: -------------------------------------------------------------------------------- 1 | /* Custom Styles just for the Learn Page */ 2 | 3 | .midway { 4 | .pre { 5 | background-color: white; 6 | padding: 0 0.2rem; 7 | border: 1px solid gray; 8 | &.warning { 9 | background-color: lightgoldenrodyellow; 10 | } 11 | } 12 | .callout { 13 | background-color: lightgrey; 14 | } 15 | .bold { 16 | font-weight: 600; 17 | } 18 | &__logo { 19 | width: 30px; 20 | } 21 | } -------------------------------------------------------------------------------- /web/src/components/interfaces/product.ts: -------------------------------------------------------------------------------- 1 | export interface ProductInt { 2 | _id: string 3 | content: { 4 | main: { 5 | title: string 6 | mainImage: { 7 | asset: { 8 | _id: string 9 | _ref: string 10 | } 11 | } 12 | slug: { 13 | current: string 14 | } 15 | } 16 | shopify: { 17 | defaultPrice: string 18 | productId: number 19 | } 20 | meta: {} 21 | } 22 | } -------------------------------------------------------------------------------- /web/src/utils/updateCustomer.tsx: -------------------------------------------------------------------------------- 1 | import cookie from "js-cookie" 2 | 3 | export const UpdateCustomer = (res: { 4 | token: string 5 | customer: { 6 | firstName: string 7 | } 8 | }, email: string) => { 9 | console.log('update cussss', res, email) 10 | cookie.set("customer_token", res.token, { expires: 25 }) 11 | cookie.set("customer_firstName", res.customer.firstName, { 12 | expires: 25, 13 | }) 14 | cookie.set("customer_email", email, { expires: 25 }) 15 | } -------------------------------------------------------------------------------- /studio/schemas/modules/internalLink.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'Internal Link', 3 | name: 'internalLink', 4 | type: 'object', 5 | hidden: true, 6 | fields: [ 7 | { 8 | name: 'title', 9 | title: 'Link CTA', 10 | type: 'string' 11 | }, 12 | { 13 | name: 'link', 14 | title: 'Link', 15 | type: 'reference', 16 | to: [ 17 | { type: 'page' }, 18 | { type: 'product' } 19 | ] 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /web/src/utils/renderModules.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Modules } from "src/components/modules" 3 | 4 | export const RenderModules = (modules: []) => { 5 | if (modules) { 6 | return modules.map((module: { 7 | _key: string, 8 | _type: string 9 | }) => { 10 | return ( 11 | 13 | 15 | 16 | ) 17 | }) 18 | } 19 | } -------------------------------------------------------------------------------- /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/data-aspects": "d199e2c199b3e26cd28b68dc84d7fc01c9186bf5089580f2e2446994d36b3cb6", 6 | "@sanity/form-builder": "b38478227ba5e22c91981da4b53436df22e48ff25238a55a973ed620be5068aa" 7 | } 8 | -------------------------------------------------------------------------------- /studio/schemas/modules/externalLink.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'External Link', 3 | name: 'externalLink', 4 | type: 'object', 5 | hidden: true, 6 | fields: [ 7 | { 8 | name: 'title', 9 | title: 'Link CTA', 10 | type: 'string' 11 | }, 12 | { 13 | name: 'link', 14 | title: 'Link', 15 | type: 'string', 16 | description: 'There is no `link` validation on this so please type accurate urls with https://, mailto:, tel: etc.' 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # misc 5 | .DS_Store 6 | .env.local 7 | .env.development.local 8 | .env.test.local 9 | .env.production.local 10 | .env 11 | .cache 12 | 13 | 14 | # gatsby files 15 | .cache/ 16 | public 17 | 18 | # Yarn 19 | yarn-error.log 20 | .pnp/ 21 | .pnp.js 22 | yarn.lock 23 | 24 | # NPM 25 | package-lock.json 26 | 27 | # Yarn Integrity file 28 | .yarn-integrity 29 | 30 | # Local Netlify folder 31 | .netlify 32 | 33 | # Functions transpiled by Netlifa-Lambda 34 | functions/*.js 35 | -------------------------------------------------------------------------------- /web/src/styles/midway/products/_card.scss: -------------------------------------------------------------------------------- 1 | .product__card { 2 | &-title { 3 | padding: 5px; 4 | } 5 | .image__block { 6 | height: 0; 7 | padding-bottom: 125%; 8 | overflow: hidden; 9 | position: relative; 10 | .gatsby-image-wrapper { 11 | position: absolute !important; 12 | height: 100%; 13 | width: 100%; 14 | top: 0; 15 | left: 0; 16 | > div { 17 | height: 100%; 18 | position: absolute; 19 | padding-bottom: 0 !important; 20 | } 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /studio/schemas/tabs/variantContent.ts: -------------------------------------------------------------------------------- 1 | import Tabs from 'sanity-plugin-tabs' 2 | 3 | export default { 4 | name: "variantContent", 5 | type: "object", 6 | inputComponent: Tabs, 7 | fieldsets: [ 8 | { name: "main", title: "Main" }, 9 | { name: "shopify", title: "Shopify" } 10 | ], 11 | fields: [ 12 | { 13 | type: "variantModule", 14 | name: "main", 15 | fieldset: "main" 16 | }, 17 | { 18 | type: "shopifyVariantModule", 19 | name: "shopify", 20 | fieldset: "shopify" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /studio/structure/subscriptions.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import S from '@sanity/desk-tool/structure-builder'; 3 | import Emoji from 'a11y-react-emoji' 4 | 5 | const Icon = () => 6 | 7 | export const SubscriptionMenuItem = S.listItem() 8 | .title('Subscriptions') 9 | .icon(Icon) 10 | .child( 11 | S.documentTypeList('subscription') 12 | .title('Subscriptions') 13 | .menuItems(S.documentTypeList('subscription').getMenuItems()) 14 | .params({ type: 'subscription' }) 15 | ); 16 | -------------------------------------------------------------------------------- /studio/schemas/tabs/pageContent.ts: -------------------------------------------------------------------------------- 1 | import Tabs from 'sanity-plugin-tabs' 2 | 3 | export default { 4 | name: "pageContent", 5 | type: "object", 6 | title: "Page Content", 7 | inputComponent: Tabs, 8 | fieldsets: [ 9 | { name: "main", title: "Main" }, 10 | { name: "defaultMeta", title: "Meta" } 11 | ], 12 | fields: [ 13 | { 14 | type: "pageModule", 15 | name: "main", 16 | fieldset: "main" 17 | }, 18 | { 19 | type: "metaCard", 20 | name: "meta", 21 | fieldset: "defaultMeta" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /studio/schemas/modules/pageItem.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'Page Item', 3 | name: 'pageItem', 4 | type: 'object', 5 | hidden: true, 6 | fields: [ 7 | { 8 | name: 'title', 9 | title: 'Page Title', 10 | type: 'string' 11 | }, 12 | { 13 | name: 'description', 14 | title: 'Page Description', 15 | type: 'text', 16 | rows: 3 17 | }, 18 | { 19 | name: 'linkedPage', 20 | title: 'Linked Page', 21 | type: 'reference', 22 | to: [ 23 | { type: 'page' } 24 | ] 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /studio/structure/collections.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import S from '@sanity/desk-tool/structure-builder'; 3 | import Emoji from 'a11y-react-emoji' 4 | 5 | const Icon = () => 6 | 7 | export const CollectionMenuItem = S.listItem() 8 | .title('Collections') 9 | .icon(Icon) 10 | .child( 11 | S.documentTypeList('collection') 12 | .title('Collections') 13 | .menuItems(S.documentTypeList('collection').getMenuItems()) 14 | .filter('_type == $type') 15 | .params({ type: 'collection' }) 16 | ); 17 | -------------------------------------------------------------------------------- /web/src/components/global/standardText.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | import BlockContent from "@sanity/block-content-to-react" 4 | 5 | import { Serializer } from "src/utils/serializer" 6 | 7 | export interface StandardTextProps { 8 | data: { 9 | text: any[] 10 | } 11 | } 12 | 13 | const StandardText = ({ data }: StandardTextProps) => { 14 | const { text } = data 15 | return ( 16 |
17 | 18 |
19 | ) 20 | } 21 | 22 | export default StandardText -------------------------------------------------------------------------------- /studio/schemas/types/siteGlobal.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Emoji from 'a11y-react-emoji' 3 | 4 | import Tabs from 'sanity-plugin-tabs' 5 | 6 | export default { 7 | name: 'siteGlobal', 8 | _id: 'siteGlobal', 9 | title: 'Global', 10 | type: 'document', 11 | description: 'Handles general global settings', 12 | fields: [ 13 | { 14 | name: "content", 15 | type: "globalContent", 16 | } 17 | ], 18 | preview: { 19 | select: {}, 20 | prepare() { 21 | return Object.assign({}, { 22 | title: 'Global Settings' 23 | }) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /web/src/stories/header.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 4 | padding: 15px 20px; 5 | display: flex; 6 | align-items: center; 7 | justify-content: space-between; 8 | } 9 | 10 | svg { 11 | display: inline-block; 12 | vertical-align: top; 13 | } 14 | 15 | h1 { 16 | font-weight: 900; 17 | font-size: 20px; 18 | line-height: 1; 19 | margin: 6px 0 6px 10px; 20 | display: inline-block; 21 | vertical-align: top; 22 | } 23 | 24 | button + button { 25 | margin-left: 10px; 26 | } 27 | -------------------------------------------------------------------------------- /web/src/components/disclaimer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const Disclaimer = () => { 4 | return ( 5 |
6 |
7 |
8 |

This is a demo site built by Kevin Green. The products you find on this test site are from sites I've worked on. If you're interested in purchasing anything for real I link out to the real sites on the product landing pages!

9 |
10 |
11 |
12 | ) 13 | } -------------------------------------------------------------------------------- /studio/schemas/modules/pageModule.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'Page Content', 3 | name: 'pageModule', 4 | type: 'object', 5 | fields: [ 6 | { 7 | name: 'title', 8 | title: 'Title', 9 | type: 'string' 10 | }, 11 | { 12 | name: 'slug', 13 | title: 'Slug', 14 | type: 'slug', 15 | options: { 16 | source: 'content.main.title', 17 | maxLength: 96 18 | }, 19 | validation: Rule => Rule.required() 20 | }, 21 | { 22 | name: 'modules', 23 | title: 'Modules', 24 | type: 'moduleContent', 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /web/src/stories/Header.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // also exported from '@storybook/react' if you can deal with breaking changes in 6.1 3 | import { Story, Meta } from '@storybook/react/types-6-0'; 4 | 5 | import { Header, HeaderProps } from './Header'; 6 | 7 | export default { 8 | title: 'Example/Header', 9 | component: Header, 10 | } as Meta; 11 | 12 | const Template: Story = (args) =>
; 13 | 14 | export const LoggedIn = Template.bind({}); 15 | LoggedIn.args = { 16 | user: {}, 17 | }; 18 | 19 | export const LoggedOut = Template.bind({}); 20 | LoggedOut.args = {}; 21 | -------------------------------------------------------------------------------- /studio/schemas/modules/productGrid.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'Product Grid', 3 | name: 'productGrid', 4 | type: 'object', 5 | hidden: true, 6 | fields: [ 7 | { 8 | name: 'title', 9 | title: 'Title (Optional)', 10 | type: 'string' 11 | }, 12 | { 13 | name: 'shortDescription', 14 | title: 'Short Description (Optional)', 15 | type: 'string' 16 | }, 17 | { 18 | name: 'products', 19 | title: 'Products', 20 | type: 'array', 21 | of: [{ type: 'reference', to: { type: 'product' } }], 22 | validation: Rule => Rule.min(1).max(40), 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /web/src/styles/lib/_borders.scss: -------------------------------------------------------------------------------- 1 | $default-border-width: 1px; 2 | 3 | .ba { 4 | border: $default-border-width solid currentColor; 5 | } 6 | 7 | .bl { 8 | border-left: $default-border-width solid currentColor; 9 | } 10 | 11 | .bt { 12 | border-top: $default-border-width solid currentColor; 13 | 14 | } 15 | .bb { 16 | border-bottom: $default-border-width solid currentColor; 17 | } 18 | 19 | .br { 20 | border-right: $default-border-width solid currentColor; 21 | } 22 | 23 | .bn { 24 | border: none; 25 | } 26 | 27 | .bnt { 28 | border-top: none; 29 | } 30 | 31 | .bnl { 32 | border-left: none; 33 | } 34 | 35 | .bnr { 36 | border-right: none; 37 | } -------------------------------------------------------------------------------- /studio/structure/redirects.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import S from '@sanity/desk-tool/structure-builder'; 3 | import Emoji from 'a11y-react-emoji' 4 | 5 | const Icon = () => 6 | 7 | export const RedirectMenuItem = S.listItem() 8 | .title('Redirects') 9 | .icon(Icon) 10 | .child( 11 | S.documentTypeList('redirect') 12 | .title('Redirects') 13 | .menuItems(S.documentTypeList('redirect').getMenuItems()) 14 | .filter('_type == $type') 15 | .params({ type: 'redirect' }) 16 | .child(documentId => 17 | S.document() 18 | .documentId(documentId) 19 | ) 20 | ); 21 | -------------------------------------------------------------------------------- /web/src/components/link.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'gatsby' 3 | 4 | export const PageLink = (props: { 5 | className?: string 6 | to: string 7 | type?: string 8 | onClick?: () => void 9 | onMouseEnter?: () => void 10 | onMouseLeave?: () => void 11 | onMouseOver?: () => void 12 | children?: any 13 | }) => ( 14 | 23 | {props.children} 24 | 25 | ) 26 | -------------------------------------------------------------------------------- /studio/schemas/types/doc.ts: -------------------------------------------------------------------------------- 1 | import Tabs from 'sanity-plugin-tabs' 2 | 3 | export default { 4 | name: 'doc', 5 | title: 'Doc', 6 | type: 'document', 7 | liveEdit: false, 8 | // You probably want to uncomment the next line once you've made the pages documents in the Studio. This will remove the pages document type from the create-menus. 9 | // __experimental_actions: ['update', 'publish', /* 'create', 'delete' */], 10 | fields: [ 11 | { 12 | name: "content", 13 | type: "pageContent", 14 | } 15 | ], 16 | preview: { 17 | select: { 18 | title: 'content.main.title', 19 | subtitle: 'heroText', 20 | media: 'mainImage' 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /studio/schemas/types/page.ts: -------------------------------------------------------------------------------- 1 | import Tabs from 'sanity-plugin-tabs' 2 | 3 | export default { 4 | name: 'page', 5 | title: 'Page', 6 | type: 'document', 7 | liveEdit: false, 8 | // You probably want to uncomment the next line once you've made the pages documents in the Studio. This will remove the pages document type from the create-menus. 9 | // __experimental_actions: ['update', 'publish', /* 'create', 'delete' */], 10 | fields: [ 11 | { 12 | name: "content", 13 | type: "pageContent", 14 | } 15 | ], 16 | preview: { 17 | select: { 18 | title: 'content.main.title', 19 | subtitle: 'heroText', 20 | media: 'mainImage' 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /studio/schemas/tabs/globalContent.ts: -------------------------------------------------------------------------------- 1 | import Tabs from 'sanity-plugin-tabs' 2 | 3 | export default { 4 | name: "globalContent", 5 | type: "object", 6 | inputComponent: Tabs, 7 | fieldsets: [ 8 | { name: "defaultMeta", title: "Meta" }, 9 | { name: "social", title: "Social" } 10 | ], 11 | fields: [ 12 | { 13 | type: "metaCard", 14 | name: "meta", 15 | description: "Handles the default meta information for all content types", 16 | fieldset: "defaultMeta" 17 | }, 18 | { 19 | type: "social", 20 | name: "social", 21 | description: "Handles the default meta information for all content types", 22 | fieldset: "social" 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /studio/schemas/tabs/productContent.ts: -------------------------------------------------------------------------------- 1 | import Tabs from 'sanity-plugin-tabs' 2 | 3 | export default { 4 | name: "productContent", 5 | type: "object", 6 | inputComponent: Tabs, 7 | fieldsets: [ 8 | { name: "main", title: "Main" }, 9 | { name: "shopify", title: "Shopify" }, 10 | { name: "defaultMeta", title: "Meta" } 11 | ], 12 | fields: [ 13 | { 14 | type: "productModule", 15 | name: "main", 16 | fieldset: "main" 17 | }, 18 | { 19 | type: "shopifyProductModule", 20 | name: "shopify", 21 | fieldset: "shopify" 22 | }, 23 | { 24 | type: "metaCard", 25 | name: "meta", 26 | fieldset: "defaultMeta" 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /studio/schemas/types/collection.ts: -------------------------------------------------------------------------------- 1 | import Tabs from 'sanity-plugin-tabs' 2 | 3 | export default { 4 | name: 'collection', 5 | title: 'Collection', 6 | type: 'document', 7 | liveEdit: false, 8 | // You probably want to uncomment the next line once you've made the pages documents in the Studio. This will remove the pages document type from the create-menus. 9 | // __experimental_actions: ['update', 'publish', /* 'create', 'delete' */], 10 | fields: [ 11 | { 12 | name: "content", 13 | type: "pageContent", 14 | } 15 | ], 16 | preview: { 17 | select: { 18 | title: 'content.main.title', 19 | subtitle: 'heroText', 20 | media: 'mainImage' 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /web/src/stories/Page.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // also exported from '@storybook/react' if you can deal with breaking changes in 6.1 3 | import { Story, Meta } from '@storybook/react/types-6-0'; 4 | 5 | import { Page, PageProps } from './Page'; 6 | import * as HeaderStories from './Header.stories'; 7 | 8 | export default { 9 | title: 'Example/Page', 10 | component: Page, 11 | } as Meta; 12 | 13 | const Template: Story = (args) => ; 14 | 15 | export const LoggedIn = Template.bind({}); 16 | LoggedIn.args = { 17 | ...HeaderStories.LoggedIn.args, 18 | }; 19 | 20 | export const LoggedOut = Template.bind({}); 21 | LoggedOut.args = { 22 | ...HeaderStories.LoggedOut.args, 23 | }; 24 | -------------------------------------------------------------------------------- /studio/schemas/modules/nestedPages.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'Nested Pages', 3 | name: 'nestedPages', 4 | type: 'object', 5 | hidden: true, 6 | fields: [ 7 | { 8 | name: 'title', 9 | title: 'Title (Optional)', 10 | type: 'string' 11 | }, 12 | { 13 | name: 'page', 14 | title: 'Page Items', 15 | type: 'array', 16 | of: [{ type: 'pageItem' }] 17 | }, 18 | ], 19 | preview: { 20 | select: { 21 | title: 'title' 22 | }, 23 | prepare (selection) { 24 | const { title } = selection 25 | return Object.assign({}, selection, { 26 | title: 'Nested Pages', 27 | subtitle: title && `of ${title}` 28 | }) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /studio/structure/docs.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import S from '@sanity/desk-tool/structure-builder'; 3 | import Emoji from 'a11y-react-emoji' 4 | 5 | import { DocViews } from './views/docPreview' 6 | 7 | const Icon = () => 8 | 9 | export const DocMenuItem = S.listItem() 10 | .title('Docs') 11 | .icon(Icon) 12 | .child( 13 | S.documentTypeList('doc') 14 | .title('Docs') 15 | .menuItems(S.documentTypeList('doc').getMenuItems()) 16 | .filter('_type == $type') 17 | .params({ type: 'doc' }) 18 | .child(documentId => 19 | S.document() 20 | .views(DocViews({type: 'doc'})) 21 | .documentId(documentId) 22 | ) 23 | ); 24 | -------------------------------------------------------------------------------- /studio/structure/pages.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import S from '@sanity/desk-tool/structure-builder'; 3 | import Emoji from 'a11y-react-emoji' 4 | 5 | import { Views } from './views/preview' 6 | 7 | const Icon = () => 8 | 9 | export const PageMenuItem = S.listItem() 10 | .title('Pages') 11 | .icon(Icon) 12 | .child( 13 | S.documentTypeList('page') 14 | .title('Pages') 15 | .menuItems(S.documentTypeList('page').getMenuItems()) 16 | .filter('_type == $type') 17 | .params({ type: 'page' }) 18 | .child(documentId => 19 | S.document() 20 | .documentId(documentId) 21 | .views(Views({type: 'page'})) 22 | ) 23 | ); 24 | -------------------------------------------------------------------------------- /studio/structure/views/docPreview.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import S from '@sanity/desk-tool/structure-builder'; 3 | 4 | import EyeIcon from 'part:@sanity/base/eye-icon' 5 | import EditIcon from 'part:@sanity/base/edit-icon' 6 | import IframePreview from '../previews/IframePreview.js' 7 | 8 | const remoteURL = 'https://midway.ctrlaltdel.world/docs' 9 | const localURL = 'http://localhost:8000/docs' 10 | const previewURL = window.location.hostname === 'localhost' ? localURL : remoteURL 11 | 12 | export const DocViews = ({type}) => { 13 | return [ 14 | S.view.form().icon(EditIcon), 15 | S.view 16 | .component(IframePreview) 17 | .options({previewURL}) 18 | .icon(EyeIcon) 19 | .title('Documention') 20 | ] 21 | } -------------------------------------------------------------------------------- /studio/structure/views/preview.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import S from '@sanity/desk-tool/structure-builder'; 3 | 4 | import EyeIcon from 'part:@sanity/base/eye-icon' 5 | import EditIcon from 'part:@sanity/base/edit-icon' 6 | import IframePreview from '../previews/IframePreview.js' 7 | 8 | const remoteURL = 'https://midway.ctrlaltdel.world/previews' 9 | const localURL = 'http://localhost:8000/previews' 10 | const previewURL = window.location.hostname === 'localhost' ? localURL : remoteURL 11 | 12 | export const Views = ({type}) => { 13 | return [ 14 | S.view.form().icon(EditIcon), 15 | S.view 16 | .component(IframePreview) 17 | .options({previewURL}) 18 | .icon(EyeIcon) 19 | .title('Preview') 20 | ] 21 | } -------------------------------------------------------------------------------- /web/src/components/auth/authWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { navigate } from 'gatsby' 2 | import cookie from 'js-cookie' 3 | import React, { useEffect, useState } from "react" 4 | 5 | interface Props { 6 | component: React.ElementType 7 | path: string 8 | } 9 | 10 | const AuthWrapper = (props: Props) => { 11 | const { component: Component, path, ...rest } = props 12 | const [ready, setReady] = useState(false) 13 | 14 | useEffect(() => { 15 | if (!cookie.get('customer_token') || !cookie.get('customer_email')) navigate('/account/login') 16 | setReady(true) 17 | }, [0]); 18 | 19 | return ( 20 |
21 | {ready ? : } 22 |
23 | ) 24 | } 25 | 26 | export default AuthWrapper 27 | -------------------------------------------------------------------------------- /studio/sanity.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "api": { 4 | "projectId": "hinqmch5", 5 | "dataset": "production" 6 | }, 7 | "project": { 8 | "name": "Midway", 9 | "basePath": "/" 10 | }, 11 | "plugins": [ 12 | "@sanity/base", 13 | "@sanity/components", 14 | "@sanity/default-layout", 15 | "@sanity/default-login", 16 | "@sanity/desk-tool", 17 | "@sanity/color-input", 18 | "media", 19 | "tabs", 20 | "seo-tools", 21 | "@sanity/vision" 22 | ], 23 | "parts": [ 24 | { 25 | "name": "part:@sanity/base/schema", 26 | "path": "./schemas/schemas.ts" 27 | }, 28 | { 29 | "name": "part:@sanity/desk-tool/structure", 30 | "path": "./deskStructure.tsx" 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /studio/structure/products.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import S from '@sanity/desk-tool/structure-builder'; 3 | import Emoji from 'a11y-react-emoji' 4 | 5 | import { Views } from './views/preview' 6 | 7 | const Icon = () => 8 | 9 | export const ProductMenuItem = S.listItem() 10 | .title('Products') 11 | .icon(Icon) 12 | .child( 13 | S.documentTypeList('product') 14 | .title('Products') 15 | .menuItems(S.documentTypeList('product').getMenuItems()) 16 | .filter('_type == $type && subscription != true') 17 | .params({ type: 'product' }) 18 | .child(documentId => 19 | S.document() 20 | .documentId(documentId) 21 | .views(Views({type: 'page'})) 22 | ) 23 | ); 24 | -------------------------------------------------------------------------------- /web/env.example: -------------------------------------------------------------------------------- 1 | SANITY_DATASET="production" 2 | SANITY_PROJECT_ID="su2xvl4g" 3 | SANITY_API_TOKEN= 4 | 5 | GATSBY_SHOPIFY_GRAPHQL_URL="https:///api/2020-10/graphql" 6 | GATSBY_SHOPIFY_STOREFRONT_TOKEN= 7 | GATSBY_SITE_URL= 8 | GATSBY_SHOPIFY_STORE="site.myshopify.com" 9 | GATSBY_SANITY_PROJECT_ID="su2xvl4g" # This is a midway test instance 10 | GATSBY_SANITY_DATASET="production" 11 | GATSBY_SENTRY_DSN= 12 | GATSBY_GA_ID= # This is your key for Google Analytics 13 | 14 | SENTRY_AUTH_TOKEN= 15 | 16 | SHOPIFY_WEBHOOK_SECRET="" # Located at https://store.myshopify.com/admin/settings/notifications 17 | SHOPIFY_URL=site.myshopify.com 18 | SHOPIFY_GRAPHQL_URL="https:///api/2020-10/graphql" 19 | SHOPIFY_STOREFRONT_TOKEN= 20 | SHOPIFY_API_KEY= 21 | SHOPIFY_API_PASSWORD= 22 | -------------------------------------------------------------------------------- /web/src/stories/button.css: -------------------------------------------------------------------------------- 1 | .storybook-button { 2 | font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | font-weight: 700; 4 | border: 0; 5 | border-radius: 3em; 6 | cursor: pointer; 7 | display: inline-block; 8 | line-height: 1; 9 | } 10 | .storybook-button--primary { 11 | color: white; 12 | background-color: #1ea7fd; 13 | } 14 | .storybook-button--secondary { 15 | color: #333; 16 | background-color: transparent; 17 | box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset; 18 | } 19 | .storybook-button--small { 20 | font-size: 12px; 21 | padding: 10px 16px; 22 | } 23 | .storybook-button--medium { 24 | font-size: 14px; 25 | padding: 11px 20px; 26 | } 27 | .storybook-button--large { 28 | font-size: 16px; 29 | padding: 12px 24px; 30 | } 31 | -------------------------------------------------------------------------------- /web/migrations/backup-sanity.js: -------------------------------------------------------------------------------- 1 | // Setup environment variables 2 | require('dotenv').config({path: `.env.${process.env.NODE_ENV || 'development'}`}); 3 | const sanityClient = require('@sanity/client'); 4 | const exportDataset = require('@sanity/export'); 5 | 6 | const client = sanityClient({ 7 | projectId: process.env.GATSBY_SANITY_PROJECT_ID, 8 | dataset: process.env.GATSBY_SANITY_DATASET, 9 | token: process.env.SANITY_API_TOKEN, 10 | useCdn: false, 11 | }); 12 | const migration = async () => { 13 | await exportDataset({ 14 | client, 15 | dataset: process.env.GATSBY_SANITY_DATASET, 16 | outputPath: `./exports/${process.env.GATSBY_SANITY_DATASET}-${new Date().getTime()}.tar.gz`, 17 | assets: true, 18 | drafts: true, 19 | assetConcurrency: 12, 20 | }); 21 | console.log('success'); 22 | }; 23 | migration(); -------------------------------------------------------------------------------- /web/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "tslint-plugin-prettier" 4 | ], 5 | "extends": [ 6 | "tslint:latest", 7 | "tslint-react", 8 | "tslint-config-prettier" 9 | ], 10 | "rules": { 11 | "prettier": false, 12 | "jsx-no-multiline-js": false, 13 | "jsx-no-lambda": false, 14 | "import-name": false, 15 | "no-boolean-literal-compare": false, 16 | "interface-name" : [true, "never-prefix"], 17 | "object-literal-sort-keys": false, 18 | "no-object-literal-type-assertion": false, 19 | "curly": false, 20 | "no-submodule-imports": false, 21 | "no-implicit-dependencies": false, 22 | "no-return-await": false, 23 | "no-console": false, 24 | "ordered-imports": false, 25 | "only-arrow-functions": false, 26 | "no-var-requires": false 27 | } 28 | } -------------------------------------------------------------------------------- /studio/structure/config.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import S from '@sanity/desk-tool/structure-builder' 3 | import Emoji from 'a11y-react-emoji' 4 | 5 | const ConfigIcon = () => 6 | 7 | export const ConfigMenu = S.listItem() 8 | .title('Configuration') 9 | .icon(ConfigIcon) 10 | .child( 11 | S.list() 12 | .title('Settings') 13 | .items([ 14 | S.listItem() 15 | .title('Menus') 16 | .child( 17 | S.documentTypeList('menus') 18 | .title('Menus') 19 | .filter('_type == $type') 20 | .params({ type: 'menus' }) 21 | ), 22 | S.documentListItem() 23 | .title('Global') 24 | .id('siteGlobal') 25 | .schemaType('siteGlobal') 26 | ]) 27 | ) -------------------------------------------------------------------------------- /studio/schemas/modules/imageModule.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'Image Module', 3 | name: 'imageModule', 4 | type: 'object', 5 | hidden: true, 6 | fields: [ 7 | { 8 | name: 'image', 9 | title: 'Image', 10 | type: 'image', 11 | options: { 12 | hotspot: true 13 | }, 14 | validation: Rule => Rule.required() 15 | }, 16 | { 17 | name: 'caption', 18 | title: 'Image Caption', 19 | type: 'text' 20 | }, 21 | { 22 | name: 'layout', 23 | title: 'Layout', 24 | type: 'string', 25 | options: { 26 | list: [ 27 | {title: 'Full', value: 'full'}, 28 | {title: 'large', value: 'large'}, 29 | {title: 'medium', value: 'medium'} 30 | ], 31 | layout: 'dropdown' 32 | } 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /web/src/components/cart.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | 3 | import { useCartTotals, useCartItems } from 'src/context/siteContext' 4 | 5 | import { 6 | LineItem 7 | } from 'src/components/cart/lineItem' 8 | 9 | export const Cart = () => { 10 | const lineItems = useCartItems() 11 | const { total } = useCartTotals() 12 | return ( 13 |
14 | {lineItems.length > 0 ? ( 15 |
16 | {lineItems.map((item: { id: string, quantity: number }) => ( 17 | 18 | 19 | 20 | ))} 21 |
22 | ) : ( 23 |
24 | Cart is Empty 25 |
26 | )} 27 |
28 | ) 29 | } -------------------------------------------------------------------------------- /web/src/templates/documentation.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { RenderModules } from 'src/utils/renderModules' 4 | 5 | export interface DocumentationProps { 6 | pageContext: { 7 | main: { 8 | modules: [], 9 | slug: { 10 | current: string 11 | }, 12 | title: string 13 | }, 14 | meta: {} 15 | } 16 | path?: string 17 | preview?: boolean 18 | } 19 | 20 | const Documentation = ({ 21 | path, 22 | pageContext, 23 | preview = false 24 | }: DocumentationProps) => { 25 | const { 26 | main: { 27 | modules 28 | }, 29 | meta 30 | } = pageContext 31 | 32 | return ( 33 |
34 |
35 | {RenderModules(modules)} 36 |
37 |
38 | ) 39 | } 40 | 41 | export default Documentation -------------------------------------------------------------------------------- /web/src/components/global/productGrid.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | import { ProductCard } from 'src/components/product/card' 4 | import { ProductInt } from 'src/interfaces/product' 5 | 6 | export interface ProductGridProps { 7 | data: { 8 | title?: string 9 | shortDescription?: string 10 | products?: [ProductInt] 11 | } 12 | } 13 | 14 | const ProductGrid = ({ data }: ProductGridProps) => { 15 | const { title, products } = data 16 | return ( 17 |
18 |

{title}

19 |
20 | {products && products.map((singleProduct) => ( 21 | 22 | ))} 23 |
24 |
25 |
26 | ) 27 | } 28 | 29 | export default ProductGrid -------------------------------------------------------------------------------- /studio/schemas/types/menus.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Emoji from 'a11y-react-emoji' 3 | 4 | export default { 5 | name: 'menus', 6 | _id: 'menus', 7 | title: 'Menus', 8 | type: 'document', 9 | description: 'This handles all the global settings throughout the site, promo bars, phone numbers etc, ', 10 | fields: [ 11 | 12 | { 13 | name: 'title', 14 | title: 'Title', 15 | type: 'string' 16 | }, 17 | { 18 | name: 'slug', 19 | title: 'Slug', 20 | type: 'slug', 21 | options: { 22 | source: 'title', 23 | maxLength: 96 24 | }, 25 | validation: Rule => Rule.required() 26 | }, 27 | 28 | { 29 | name: 'items', 30 | title: 'Nav Items', 31 | type: 'array', 32 | of: [ 33 | { type: 'internalLink' }, 34 | { type: 'externalLink' } 35 | ] 36 | }, 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /studio/schemas/siteSettings.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'siteSettings', 3 | title: 'Site Settings', 4 | type: 'document', 5 | // You probably want to uncomment the next line once you've made a siteSettings document in the Studio. This will remove the settings document type from the create-menus. 6 | // __experimental_actions: ['update', 'publish', /* 'create', 'delete' */], 7 | fields: [ 8 | { 9 | name: 'title', 10 | title: 'Title', 11 | type: 'string' 12 | }, 13 | { 14 | name: 'description', 15 | title: 'Description', 16 | type: 'text' 17 | }, 18 | { 19 | name: 'keywords', 20 | title: 'Keywords', 21 | type: 'array', 22 | of: [{ type: 'string' }], 23 | options: { 24 | layout: 'tags' 25 | } 26 | }, 27 | { 28 | name: 'author', 29 | title: 'Author', 30 | type: 'string' 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /web/src/components/modules.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import loadable from '@loadable/component' 3 | 4 | const StandardText = loadable(() => import('src/components/global/standardText')) 5 | const ProductGrid = loadable(() => import('src/components/global/productGrid')) 6 | const NestedPages = loadable(() => import('src/components/global/nestedPages')) 7 | 8 | export const Modules = ({ reactModule }: { reactModule: any}) => getModule(reactModule) 9 | 10 | const getModule = (module: any) => { 11 | const type = module._type 12 | const modules = { 13 | 'standardText': StandardText, 14 | 'productGrid': ProductGrid, 15 | 'nestedPages': NestedPages, 16 | 'default': () => {type} 17 | } 18 | /* tslint:disable:no-string-literal */ 19 | const Module = modules[type] || modules["default"]; 20 | /* tslint:enable:no-string-literal */ 21 | return ; 22 | } -------------------------------------------------------------------------------- /studio/deskStructure.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import S from '@sanity/desk-tool/structure-builder' 3 | 4 | import { ConfigMenu } from './structure/config' 5 | import { ProductMenuItem } from './structure/products' 6 | import { ProductVariantParent } from './structure/variants' 7 | import { CollectionMenuItem } from './structure/collections' 8 | import { PageMenuItem } from './structure/pages' 9 | import { DocMenuItem } from './structure/docs' 10 | import { RedirectMenuItem } from './structure/redirects' 11 | import { SubscriptionMenuItem } from './structure/subscriptions' 12 | 13 | // 14 | // === Structure === 15 | // 16 | 17 | export default () => 18 | S.list() 19 | .title('Content') 20 | .items([ 21 | ConfigMenu, 22 | PageMenuItem, 23 | CollectionMenuItem, 24 | ProductMenuItem, 25 | ProductVariantParent, 26 | SubscriptionMenuItem, 27 | RedirectMenuItem, 28 | DocMenuItem 29 | ]) -------------------------------------------------------------------------------- /studio/structure/variants.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import S from '@sanity/desk-tool/structure-builder'; 3 | import Emoji from 'a11y-react-emoji' 4 | 5 | const VariantIcon = () => 6 | 7 | export const ProductVariantParent = S.listItem() 8 | .title('Product Variants') 9 | .icon(VariantIcon) 10 | .child( 11 | S.documentTypeList('product') 12 | .title('By Product') 13 | .menuItems(S.documentTypeList('product').getMenuItems()) 14 | .filter('_type == $type && !defined(parents) && subscription != true') 15 | .params({ type: 'product' }) 16 | .child(productId => 17 | S.documentList() 18 | .title('Variants') 19 | .menuItems(S.documentTypeList('productVariant').getMenuItems()) 20 | .filter('_type == $type && content.shopify.productId == $productId') 21 | .params({ type: 'productVariant', productId: Number(productId) }) 22 | ) 23 | ); 24 | -------------------------------------------------------------------------------- /studio/styles/variables.css: -------------------------------------------------------------------------------- 1 | /* ./styles/variables.css */ 2 | 3 | @import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap'); 4 | 5 | :root { 6 | /* Brand colors */ 7 | --body-bg: #191D21; 8 | --body-text: #fff; 9 | --component-bg: #242A2F; 10 | --component-text-color: #fff; 11 | --main-navigation-color: #041893; 12 | 13 | --brand-primary: #041893; 14 | --brand-primary--inverted: #ffffff; 15 | --brand-secondary: #ffa800; 16 | --brand-secondary--inverted: #041893; 17 | 18 | /* Typography */ 19 | --font-family-sans-serif: 'Space Mono'; 20 | --font-size-base: 15px; 21 | --text-color: var(--brand-primary--inverted); 22 | 23 | --header-height: 3.3rem; 24 | 25 | 26 | --default-button-color: var(--brand-primary); 27 | 28 | --default-button-color: white; 29 | --default-button-color--inverted: var(--white); 30 | --default-button-color--hover: white; 31 | --default-button-color--active: white; 32 | } -------------------------------------------------------------------------------- /web/src/stories/Button.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // also exported from '@storybook/react' if you can deal with breaking changes in 6.1 3 | import { Story, Meta } from '@storybook/react/types-6-0'; 4 | 5 | import { Button, ButtonProps } from './Button'; 6 | 7 | export default { 8 | title: 'Example/Button', 9 | component: Button, 10 | argTypes: { 11 | backgroundColor: { control: 'color' }, 12 | }, 13 | } as Meta; 14 | 15 | const Template: Story = (args) => 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /web/src/templates/collection.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { RenderModules } from 'src/utils/renderModules' 4 | import { SEO } from 'src/components/SEO' 5 | 6 | export interface CollectionProps { 7 | pageContext: { 8 | modules: [] 9 | slug: string 10 | main: { 11 | title: string 12 | }, 13 | meta: {} 14 | site: {} 15 | title: string 16 | } 17 | path?: string 18 | preview?: boolean 19 | } 20 | 21 | const Collection = ({ 22 | path, 23 | pageContext, 24 | preview = false 25 | }: CollectionProps) => { 26 | const { 27 | modules, 28 | slug, 29 | title, 30 | site, 31 | meta 32 | } = pageContext 33 | 34 | const url = slug === 'home' ? '' : path 35 | return ( 36 |
37 | {preview && ( 38 |
This is a Preview
39 | )} 40 | 41 |
42 | {RenderModules(modules)} 43 |
44 |
45 | ) 46 | } 47 | 48 | export default Collection -------------------------------------------------------------------------------- /web/src/components/header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { PageLink } from 'src/components/link' 4 | import { Disclaimer } from 'src/components/disclaimer' 5 | 6 | import { useCartCount, useToggleCart, useStore } from 'src/context/siteContext' 7 | 8 | export const Header = () => { 9 | const {customerName} = useStore() 10 | const count = useCartCount() 11 | const toggleCart = useToggleCart() 12 | 13 | return ( 14 |
15 | 16 |
17 |
18 | Index 19 | Docs 20 | Shop All 21 |
22 |
23 | {customerName ? `Hi, ${customerName}` : 'Account'} 24 | 27 |
28 |
29 |
30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /shopify/src/theme.liquid: -------------------------------------------------------------------------------- 1 | {% comment %} 2 | 3 | This file is basically not used. It serves to redirect the headless site 4 | 5 | Adding plugin or app scripts and snippets here will likely have no effect on the main site 6 | itself. Reach out a developer for help installing scripts and code snippets. 7 | 8 | {% endcomment %} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /web/src/lambda/back-in-stock.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayEvent } from 'aws-lambda' 2 | import axios from 'axios' 3 | 4 | import { 5 | statusReturn 6 | } from './requestConfig' 7 | 8 | let data: { 9 | email: string 10 | variant: number 11 | platform: string 12 | accountId: string 13 | } 14 | 15 | export const handler = async (event: APIGatewayEvent): Promise => { 16 | if (event.httpMethod !== 'POST' || !event.body) return statusReturn(400, {}) 17 | 18 | try { 19 | data = JSON.parse(event.body) 20 | } catch (error) { 21 | console.log('JSON parsing error:', error); 22 | return statusReturn(400, { error: 'Bad Request Body' }) 23 | } 24 | 25 | const { 26 | accountId, 27 | email, 28 | platform, 29 | variant 30 | } = data 31 | 32 | const stringData = `a=${accountId}&email=${encodeURIComponent(email)}&variant=${variant}&platform=${platform}` 33 | try { 34 | let subscription = await axios({ 35 | url: 'https://a.klaviyo.com/api/v1/catalog/subscribe', 36 | method: 'POST', 37 | data: stringData 38 | }) 39 | 40 | return statusReturn(200, subscription.data) 41 | } catch (err) { 42 | console.log(err) 43 | return statusReturn(500, { error: err.message }) 44 | } 45 | } -------------------------------------------------------------------------------- /web/src/lambda/orders.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayEvent } from 'aws-lambda' 2 | import axios from 'axios' 3 | 4 | import { 5 | statusReturn, 6 | preparePayload, 7 | shopifyConfig, 8 | SHOPIFY_GRAPHQL_URL, 9 | CUSTOMER_QUERY 10 | } from './requestConfig' 11 | 12 | export const handler = async (event: APIGatewayEvent): Promise => { 13 | if (event.httpMethod !== 'POST' || !event.body) return statusReturn(400, '') 14 | let data: { 15 | token: string 16 | } 17 | 18 | try { 19 | data = JSON.parse(event.body) 20 | } catch (error) { 21 | console.log('JSON parsing error:', error); 22 | return statusReturn(400, { error: 'Bad request body' }) 23 | } 24 | 25 | const payload = preparePayload(CUSTOMER_QUERY, { 26 | customerAccessToken: data.token 27 | }) 28 | 29 | try { 30 | let customer = await axios({ 31 | url: SHOPIFY_GRAPHQL_URL, 32 | method: 'POST', 33 | headers: shopifyConfig, 34 | data: JSON.stringify(payload) 35 | }) 36 | customer = customer.data.data.customer 37 | 38 | return statusReturn(200, { 39 | token: data.token, 40 | customer 41 | }) 42 | } catch (err) { 43 | console.log(err) 44 | return statusReturn(500, { error: err.message }) 45 | } 46 | } -------------------------------------------------------------------------------- /web/src/stories/assets/direction.svg: -------------------------------------------------------------------------------- 1 | illustration/direction -------------------------------------------------------------------------------- /web/src/components/global/nestedPages.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { PageLink } from 'src/components/link' 3 | 4 | export interface NestedPagesProps { 5 | data: { 6 | title: string 7 | page: Array<{ 8 | _key: string 9 | description?: string 10 | title: string 11 | linkedPage: { 12 | content: { 13 | main: { 14 | slug: { 15 | current: string 16 | } 17 | } 18 | } 19 | } 20 | }> 21 | } 22 | } 23 | 24 | const NestedPages = ({ data }: NestedPagesProps) => { 25 | const { page } = data 26 | return ( 27 |
28 |
{data.title}
29 |
30 | {page.map(({ title, _key, description, linkedPage }) => ( 31 |
32 | 33 |

{title} ➔

34 |

{description}

35 |
36 |
37 | ))} 38 |
39 |
40 | ) 41 | } 42 | 43 | export default NestedPages -------------------------------------------------------------------------------- /web/src/components/image.tsx: -------------------------------------------------------------------------------- 1 | import cx from "classnames" 2 | import Img from "gatsby-image" 3 | // @ts-ignore 4 | import { getFluidGatsbyImage } from "gatsby-source-sanity" 5 | import React, { useState, useEffect } from "react" 6 | 7 | const sanityConfig = { 8 | projectId: process.env.GATSBY_SANITY_PROJECT_ID, 9 | dataset: process.env.GATSBY_SANITY_DATASET 10 | } 11 | 12 | export const Image = ({ imageId, className, width, alt, src }: { 13 | imageId?: string 14 | width?: number 15 | alt?: string 16 | className?: string 17 | src?: string 18 | }) => { 19 | 20 | const [loaded, setLoaded] = useState(false) 21 | let fluidProps 22 | 23 | if (imageId && !/gif/.test(imageId)) { 24 | fluidProps = getFluidGatsbyImage(imageId, { maxWidth: width || 2400 }, sanityConfig) 25 | } 26 | 27 | return ( 28 |
29 | {fluidProps ? ( 30 | {alt} 31 | ) : ( 32 | {alt} { 39 | setLoaded(true) 40 | }} /> 41 | )} 42 |
43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /web/src/stories/page.css: -------------------------------------------------------------------------------- 1 | section { 2 | font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | font-size: 14px; 4 | line-height: 24px; 5 | padding: 48px 20px; 6 | margin: 0 auto; 7 | max-width: 600px; 8 | color: #333; 9 | } 10 | 11 | h2 { 12 | font-weight: 900; 13 | font-size: 32px; 14 | line-height: 1; 15 | margin: 0 0 4px; 16 | display: inline-block; 17 | vertical-align: top; 18 | } 19 | 20 | p { 21 | margin: 1em 0; 22 | } 23 | 24 | a { 25 | text-decoration: none; 26 | color: #1ea7fd; 27 | } 28 | 29 | ul { 30 | padding-left: 30px; 31 | margin: 1em 0; 32 | } 33 | 34 | li { 35 | margin-bottom: 8px; 36 | } 37 | 38 | .tip { 39 | display: inline-block; 40 | border-radius: 1em; 41 | font-size: 11px; 42 | line-height: 12px; 43 | font-weight: 700; 44 | background: #e7fdd8; 45 | color: #66bf3c; 46 | padding: 4px 12px; 47 | margin-right: 10px; 48 | vertical-align: top; 49 | } 50 | 51 | .tip-wrapper { 52 | font-size: 13px; 53 | line-height: 20px; 54 | margin-top: 40px; 55 | margin-bottom: 40px; 56 | } 57 | 58 | .tip-wrapper svg { 59 | display: inline-block; 60 | height: 12px; 61 | width: 12px; 62 | margin-right: 4px; 63 | vertical-align: top; 64 | margin-top: 3px; 65 | } 66 | 67 | .tip-wrapper svg path { 68 | fill: #1ea7fd; 69 | } 70 | -------------------------------------------------------------------------------- /web/src/components/footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { Github } from 'src/components/svgs' 4 | import { Newsletter } from 'src/components/newsletter' 5 | import { Icon } from 'src/components/icon' 6 | 7 | export const Footer = ({ social }: { social?: Array<{ icon: string, url: string }>}) => { 8 | return ( 9 |
10 |
11 |
12 |
13 |
14 |
Newsletter Signup
15 |

Fake newsletter, but talks to real Klaviyo test instance

16 | 17 |
18 |
19 |
20 |
21 | {social?.map(socialLink => ( 22 | 23 | 24 | 25 | ))} 26 |
27 |
28 |
29 |
30 |
31 | ) 32 | } -------------------------------------------------------------------------------- /web/src/lambda/activate.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayEvent } from 'aws-lambda' 2 | import axios from 'axios' 3 | 4 | import { 5 | statusReturn, 6 | preparePayload, 7 | shopifyConfig, 8 | SHOPIFY_GRAPHQL_URL, 9 | CUSTOMER_ACTIVATE_QUERY 10 | } from './requestConfig' 11 | 12 | let data 13 | 14 | export const handler = async (event: APIGatewayEvent): Promise => { 15 | if (event.httpMethod !== 'POST' || !event.body) return statusReturn(400, '') 16 | 17 | try { 18 | data = JSON.parse(event.body) 19 | } catch (error) { 20 | console.log('JSON parsing error:', error); 21 | return statusReturn(400, { error: 'Bady Request Body' }) 22 | } 23 | 24 | const payload = preparePayload(CUSTOMER_ACTIVATE_QUERY, { 25 | id: data.id, 26 | input: data.input 27 | }) 28 | 29 | try { 30 | const customer = await axios({ 31 | url: SHOPIFY_GRAPHQL_URL, 32 | method: 'POST', 33 | headers: shopifyConfig, 34 | data: JSON.stringify(payload) 35 | }) 36 | if (customer.data.data.customerActivate.userErrors.length > 0) { 37 | throw customer.data.data.customerActivate.userErrors 38 | } else { 39 | const cusRes = customer.data.data.customerActivate 40 | return statusReturn(200, { data: cusRes }) 41 | } 42 | } catch (err) { 43 | return statusReturn(500, { error: err[0].message }) 44 | } 45 | } -------------------------------------------------------------------------------- /web/src/stories/assets/flow.svg: -------------------------------------------------------------------------------- 1 | illustration/flow -------------------------------------------------------------------------------- /web/src/components/newsletter.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { subscribe } from 'klaviyo-subscribe' 3 | 4 | export const Newsletter = ({ 5 | listId = 'EMPTY', // UPDATEME: important for handling newsletters in klaviyo 6 | customFields = {}, 7 | buttonText = 'Subscribe', 8 | message = `We've recieved your information!` 9 | }: { 10 | listId?: string 11 | customFields?: {} 12 | buttonText?: string 13 | message?: string 14 | }) => { 15 | const [success, setSuccess] = useState(false) 16 | const handleSubmit = (e: React.FormEvent) => { 17 | e.preventDefault() 18 | const form = e.currentTarget 19 | const { email } = form.elements 20 | subscribe(listId, email.value, customFields) 21 | .then(() => { 22 | form.reset() 23 | setSuccess(true) 24 | }) 25 | } 26 | return ( 27 | 28 | {!success ? ( 29 |
handleSubmit(e)} className='x df jcb aist'> 30 | 31 | 34 |
35 | ): ( 36 |
37 |

{message}

38 |
39 | )} 40 |
41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /studio/schemas/blockText.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is the schema definition for the rich text fields used for 3 | * for this blog studio. When you import it in schemas.js it can be 4 | * reused in other parts of the studio with: 5 | * { 6 | * name: 'someName', 7 | * title: 'Some title', 8 | * type: 'blockContent' 9 | * } 10 | */ 11 | export default { 12 | title: 'Block Text', 13 | name: 'blockText', 14 | type: 'array', 15 | of: [ 16 | { 17 | title: 'Block', 18 | type: 'block', 19 | // Styles let you set what your user can mark up blocks with. These 20 | // corrensponds with HTML tags, but you can set any title or value 21 | // you want and decide how you want to deal with it where you want to 22 | // use your content. 23 | styles: [{ title: 'Normal', value: 'normal' }], 24 | lists: [], 25 | // Marks let you mark up inline text in the block editor. 26 | marks: { 27 | // Decorators usually describe a single property – e.g. a typographic 28 | // preference or highlighting by editors. 29 | decorators: [ 30 | { title: 'Strong', value: 'strong' }, 31 | { title: 'Emphasis', value: 'em' }, 32 | { title: 'Code', value: 'code' } 33 | ], 34 | // Annotations can be any object structure – e.g. a link or a footnote. 35 | annotations: [] 36 | } 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /web/src/components/auth/portal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { navigate } from 'gatsby' 3 | import cookie from 'js-cookie' 4 | import fetch from 'unfetch' 5 | 6 | import { setCustomerInState, useStore } from 'src/context/siteContext' 7 | import { Orders } from 'src/components/auth/orders' 8 | 9 | const logout = (e: React.MouseEvent, updateCustomer: any) => { 10 | e.preventDefault() 11 | const customerToken = cookie.get('customer_token') 12 | fetch(`/.netlify/functions/logout`, { 13 | method: 'POST', 14 | body: JSON.stringify({ 15 | accessToken: customerToken 16 | }) 17 | }) 18 | .then(() => { 19 | cookie.remove('customer_token') 20 | cookie.remove('customer_email') 21 | cookie.remove('customer_firstName') 22 | setTimeout(() => { 23 | updateCustomer() 24 | }, 300) 25 | setTimeout(() => { 26 | navigate('/') 27 | }, 500) 28 | }) 29 | } 30 | 31 | export const Portal = () => { 32 | const updateCustomerInState = setCustomerInState() 33 | const { customerToken } = useStore() 34 | return ( 35 |
36 |

Account Portal

37 | logout(e, updateCustomerInState)}>Logout 38 |
39 | {customerToken && ( 40 | 41 | )} 42 |
43 |
44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /web/gatsby-browser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Implement Gatsby's Browser APIs in this file. 3 | * 4 | * See: https://www.gatsbyjs.org/docs/browser-apis/ 5 | */ 6 | 7 | // You can delete this file if you're not using it 8 | 9 | 10 | import React from "react" 11 | import { StoreContextProvider } from "src/context/siteContext" 12 | import * as Sentry from '@sentry/browser' 13 | 14 | import { Initialize } from 'src/components/analytics' 15 | 16 | const app = {} 17 | 18 | app.analytics = Initialize({ 19 | // Setup analytics 20 | googleAnalyticsPropertyId: process.env.GATSBY_GA_ID, 21 | googleLinkerDomains: [ 22 | 'midway-sanity.myshopify.com', 23 | 'midway.ctrlaltdel.world', 24 | ], 25 | }) 26 | 27 | export const onRouteUpdate = ({ 28 | location, previousLocation 29 | }) => { 30 | app.analytics.pageview(location) 31 | } 32 | 33 | export const shouldUpdateScroll = ({ 34 | routerProps: { location }, 35 | getSavedScrollPosition 36 | }) => { 37 | if (location.action === "PUSH") { 38 | window.setTimeout(() => window.scrollTo(0, 0), 600); 39 | } else { 40 | const savedPosition = getSavedScrollPosition(location); 41 | window.setTimeout(() => window.scrollTo(...(savedPosition || [0, 0])), 600); 42 | } 43 | return false; 44 | }; 45 | 46 | // Optional Config Sentry 47 | Sentry.init({dsn: process.env.GATSBY_SENTRY_DSN}); 48 | 49 | export const wrapRootElement = ({ element }) => ( 50 | {element} 51 | ) -------------------------------------------------------------------------------- /web/src/stories/assets/code-brackets.svg: -------------------------------------------------------------------------------- 1 | illustration/code-brackets -------------------------------------------------------------------------------- /web/src/components/product/waitlist.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { decode } from 'shopify-gid' 3 | 4 | export const Waitlist = ({ accountId, message, buttonText, variantId }: { 5 | accountId: string 6 | message: string 7 | buttonText: string 8 | variantId: string 9 | }) => { 10 | const [success, setSuccess] = useState(false) 11 | const handleSubmit = (e: React.FormEvent) => { 12 | e.preventDefault() 13 | const form = e.currentTarget 14 | const { email } = form.elements 15 | const productIdDecoded = decode(variantId).id 16 | fetch('/.netlify/functions/back-in-stock', { 17 | method: 'POST', 18 | body: JSON.stringify({ 19 | accountId, 20 | email: email.value, 21 | variant: productIdDecoded, 22 | platform: 'shopify' 23 | }) 24 | }) 25 | .then(() => { 26 | setSuccess(true) 27 | }) 28 | 29 | } 30 | return ( 31 | 32 | {!success ? ( 33 |
handleSubmit(e)} className='x f jcb aist'> 34 | 35 | 38 |
39 | ): ( 40 |
41 |

{message}

42 |
43 | )} 44 |
45 | ) 46 | } -------------------------------------------------------------------------------- /web/src/lambda/forgot-password.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayEvent } from 'aws-lambda' 2 | import axios from 'axios' 3 | 4 | import { 5 | statusReturn, 6 | preparePayload, 7 | shopifyConfig, 8 | SHOPIFY_GRAPHQL_URL, 9 | CUSTOMER_RECOVERY_QUERY 10 | } from './requestConfig' 11 | 12 | let data: { 13 | email?: string 14 | }; 15 | 16 | export const handler = async (event: APIGatewayEvent): Promise => { 17 | if (event.httpMethod !== "POST" || !event.body) return statusReturn(400, '') 18 | 19 | try { 20 | data = JSON.parse(event.body) 21 | } catch (error) { 22 | console.log('JSON parsing error:', error); 23 | return statusReturn(400, { error: 'Bad Request Body' }) 24 | } 25 | 26 | const payload = preparePayload(CUSTOMER_RECOVERY_QUERY, { 27 | email: data.email 28 | }) 29 | 30 | try { 31 | const customer = await axios({ 32 | url: SHOPIFY_GRAPHQL_URL, 33 | method: 'POST', 34 | headers: shopifyConfig, 35 | data: JSON.stringify(payload) 36 | }) 37 | const { 38 | data, 39 | errors 40 | } = customer.data 41 | const { customerRecover } = data 42 | if (customerRecover && customerRecover.userErrors.length > 0) { 43 | throw customerRecover.userErrors 44 | } else if (errors && errors.length > 0) { 45 | throw errors 46 | } else { 47 | return statusReturn(200, { customerRecover }) 48 | } 49 | } catch (err) { 50 | return statusReturn(500, { error: err[0].message }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /web/src/stories/assets/comments.svg: -------------------------------------------------------------------------------- 1 | illustration/comments -------------------------------------------------------------------------------- /web/src/templates/account.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { Router } from '@reach/router' 3 | 4 | import AuthWrapper from 'src/components/auth/authWrapper' 5 | // @ts-ignore 6 | import Activate from 'src/components/auth/activate' 7 | 8 | import { ForgotPassword } from 'src/components/auth/forgotPassword' 9 | import { Register } from 'src/components/auth/register' 10 | import { Login } from 'src/components/auth/login' 11 | import { Reset } from 'src/components/auth/reset' 12 | import { InvalidToken } from 'src/components/auth/invalid_token' 13 | import { Portal } from 'src/components/auth/portal' 14 | 15 | const Account = ({ 16 | pageContext 17 | }: { 18 | pageContext: {} 19 | }) => { 20 | return ( 21 |
22 | 23 | 24 | 25 | 26 | 27 | 31 | 32 | 33 | 34 | 35 | 36 |
37 | ) 38 | } 39 | 40 | function PublicRoute(props: { children: React.ReactNode; path: string }) { 41 | return
{props.children}
42 | } 43 | 44 | export default Account 45 | -------------------------------------------------------------------------------- /web/src/stories/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Button } from './Button'; 4 | import './header.css'; 5 | 6 | export interface HeaderProps { 7 | user?: {}; 8 | onLogin: () => void; 9 | onLogout: () => void; 10 | onCreateAccount: () => void; 11 | } 12 | 13 | export const Header: React.FC = ({ user, onLogin, onLogout, onCreateAccount }) => ( 14 |
15 |
16 |
17 | 18 | 19 | 23 | 27 | 31 | 32 | 33 |

Acme

34 |
35 |
36 | {user ? ( 37 |
45 |
46 |
47 | ); 48 | -------------------------------------------------------------------------------- /web/src/styles/lib/_drawer.scss: -------------------------------------------------------------------------------- 1 | .cart { 2 | &__drawer { 3 | height: 100vh; 4 | max-height: -webkit-fill-available; 5 | max-height: stretch; 6 | border-left: 1px solid currentColor; 7 | transition: all 0.3s ease-in-out; 8 | transform: translateX(100%); 9 | display: flex; 10 | justify-content: space; 11 | flex-direction: column; 12 | justify-content: space-between; 13 | width: 100%; 14 | align-items: stretch; 15 | &-buttons { 16 | &.visible { 17 | border-top: 2px solid rgba(28, 31, 42, 0.1); 18 | } 19 | } 20 | &-inner { 21 | flex: 1; 22 | overflow-y: scroll; 23 | } 24 | @include breakpoint(800) { 25 | left: auto; 26 | transform: translateX(375px); 27 | max-width: 375px; 28 | right: 0; 29 | } 30 | &-progress { 31 | min-height: 6px; 32 | > span { 33 | max-width: 100%; 34 | } 35 | } 36 | &-bg { 37 | opacity: 0; 38 | user-select: none; 39 | visibility: hidden; 40 | &.is-open { 41 | user-select: visible; 42 | visibility: visible; 43 | opacity: 0.4; 44 | } 45 | } 46 | &.is-open { 47 | transform: translateX(0px); 48 | } 49 | &-header { 50 | min-height: 60px; 51 | } 52 | &-close { 53 | margin-left: 10px; 54 | svg { 55 | width: 10px; 56 | height: auto; 57 | } 58 | } 59 | &-buttons { 60 | max-height: 120px; 61 | flex: 1; 62 | &.visible { 63 | max-height: 164px; 64 | } 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /web/src/stories/assets/repo.svg: -------------------------------------------------------------------------------- 1 | illustration/repo -------------------------------------------------------------------------------- /studio/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": false, 3 | "name": "example-studio", 4 | "version": "1.0.0", 5 | "author": "Kevin Green", 6 | "scripts": { 7 | "deploy": "npm run graphql-deploy && npm run sanity-deploy", 8 | "format": "prettier-eslint --write \"**/*.js\" \"!node_modules/**\"", 9 | "graphql-deploy": "sanity graphql deploy --playground", 10 | "sanity-deploy": "sanity deploy", 11 | "lint": "eslint .", 12 | "start": "sanity start", 13 | "build": "sanity build", 14 | "now-build": "npm run build", 15 | "now-dev": "npm run build", 16 | "test": "sanity check" 17 | }, 18 | "dependencies": { 19 | "@sanity/base": "^2.0.7", 20 | "@sanity/cli": "^2.0.5", 21 | "@sanity/color-input": "^2.0.5", 22 | "@sanity/components": "^2.0.7", 23 | "@sanity/core": "^2.0.5", 24 | "@sanity/default-layout": "^2.0.7", 25 | "@sanity/default-login": "^2.0.5", 26 | "@sanity/desk-tool": "^2.0.8", 27 | "@sanity/vision": "^2.0.5", 28 | "a11y-react-emoji": "^1.1.2", 29 | "node-emoji": "^1.10.0", 30 | "prop-types": "^15.7.2", 31 | "react": "^16.12.0", 32 | "react-dom": "^16.12.0", 33 | "react-icons": "^3.8.0", 34 | "sanity-plugin-media": "^0.2.10", 35 | "sanity-plugin-seo-tools": "^1.0.6", 36 | "sanity-plugin-tabs": "^2.0.0" 37 | }, 38 | "devDependencies": { 39 | "eslint": "^6.8.0", 40 | "eslint-config-standard": "^14.1.0", 41 | "eslint-config-standard-react": "^9.2.0", 42 | "eslint-plugin-import": "^2.20.0", 43 | "eslint-plugin-node": "^11.0.0", 44 | "eslint-plugin-promise": "^4.2.1", 45 | "eslint-plugin-react": "^7.18.0", 46 | "eslint-plugin-standard": "^4.0.1", 47 | "prettier-eslint-cli": "^5.0.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /studio/schemas/modules/shopifyProductModule.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'Shopify Product Content', 3 | name: 'shopifyProductModule', 4 | type: 'object', 5 | fieldsets: [ 6 | { 7 | name: 'default', 8 | title: 'Default Variant', 9 | options: { 10 | collapsible: true, 11 | collapsed: true 12 | } 13 | } 14 | ], 15 | fields: [ 16 | { 17 | name: 'title', 18 | title: 'Title', 19 | type: 'string' 20 | }, 21 | { 22 | name: 'id', 23 | title: 'ID', 24 | type: 'string', 25 | description: 'This comes from Shopify and cannot be changed', 26 | readOnly: true, 27 | hidden: true 28 | }, 29 | { 30 | name: 'deleted', 31 | title: 'Deleted', 32 | type: 'boolean', 33 | description: 'This can be a flag set if the item is deleted from Shopify' 34 | }, 35 | { 36 | name: 'image', 37 | type: 'image', 38 | title: 'Shopify Image', 39 | readOnly: true, 40 | }, 41 | { 42 | name: 'productId', 43 | title: 'Product ID', 44 | type: 'number', 45 | description: 'This comes from Shopify and cannot be changed', 46 | readOnly: true, 47 | hidden: true 48 | }, 49 | { 50 | name: 'defaultPrice', 51 | title: 'Default Price', 52 | type: 'string', 53 | description: 'This comes from Shopify and cannot be changed', 54 | readOnly: true 55 | }, 56 | { 57 | name: 'variants', 58 | title: 'Variants', 59 | type: 'array', 60 | of: [{ type: 'reference', to: { type: 'productVariant' } }] 61 | }, 62 | { 63 | name: 'defaultVariant', 64 | type: 'defaultVariant' 65 | } 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /web/src/styles/midway/_typography.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;700&display=swap'); 2 | 3 | body, html { 4 | font-family: 'Space Grotesk', sans-serif; 5 | font-weight: 400; 6 | letter-spacing: 0.03px; 7 | } 8 | 9 | 10 | p { 11 | line-height: 1.5; 12 | padding: 10px 0; 13 | margin: 12px 0; 14 | a { 15 | text-decoration: underline; 16 | } 17 | } 18 | 19 | h1, h2, h3, h4 { 20 | font-weight: 700; 21 | } 22 | 23 | h3 { 24 | @include breakpoint(800) { 25 | margin: 1.4rem 0; 26 | } 27 | } 28 | 29 | a, .a { 30 | background-color: transparent; 31 | /* color: var(--blu; */ 32 | text-decoration: none; 33 | } 34 | 35 | a, button { 36 | position: relative; 37 | line-height: 1; 38 | } 39 | 40 | select { 41 | border-radius: 0; 42 | } 43 | 44 | ul { 45 | li { 46 | list-style-type: disc; 47 | margin-left: 20px; 48 | padding-bottom: 6px; 49 | } 50 | } 51 | 52 | ol { 53 | li { 54 | list-style-type: decimal; 55 | margin-left: 20px; 56 | padding-bottom: 6px; 57 | } 58 | } 59 | 60 | .s12 { 61 | font-size: 12px; 62 | } 63 | 64 | input:focus { 65 | border: 1px dashed currentColor; 66 | } 67 | 68 | .strikethrough { 69 | text-decoration: line-through; 70 | } 71 | 72 | .underline { 73 | text-decoration: underline; 74 | } 75 | 76 | .caps { 77 | text-transform: uppercase; 78 | } 79 | 80 | .ls1 { 81 | letter-spacing: 1px; 82 | } 83 | 84 | .skip { 85 | left: 2rem; 86 | top: 2px; 87 | z-index: 99999; 88 | background-color: white; 89 | border: 2px solid black; 90 | padding: 0.4rem; 91 | border-top: 0; 92 | transition: all 0.3s ease-in-out; 93 | transform: translateY(-40px); 94 | &:focus { 95 | transform: translateY(0px); 96 | } 97 | } -------------------------------------------------------------------------------- /studio/schemas/modules/shopifyVariantModule.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'Shopify Variant Content', 3 | name: 'shopifyVariantModule', 4 | type: 'object', 5 | fieldsets: [ 6 | { 7 | name: 'default', 8 | title: 'Default Variant', 9 | options: { 10 | collapsible: true, 11 | collapsed: true 12 | } 13 | } 14 | ], 15 | fields: [ 16 | { 17 | name: 'title', 18 | title: 'Title', 19 | type: 'string' 20 | }, 21 | { 22 | name: 'variantTitle', 23 | title: 'Variant Title', 24 | type: 'string' 25 | }, 26 | { 27 | name: 'id', 28 | title: 'ID', 29 | type: 'string', 30 | description: 'This comes from Shopify and cannot be changed', 31 | readOnly: true, 32 | hidden: true 33 | }, 34 | { 35 | name: 'productId', 36 | title: 'Product ID', 37 | type: 'number', 38 | description: 'This comes from Shopify and cannot be changed', 39 | readOnly: true, 40 | hidden: true 41 | }, 42 | { 43 | name: 'variantId', 44 | title: 'Variant ID', 45 | type: 'number', 46 | description: 'This comes from Shopify and cannot be changed', 47 | readOnly: true, 48 | hidden: true 49 | }, 50 | { 51 | name: 'price', 52 | title: 'Price', 53 | type: 'string', 54 | description: 'This comes from Shopify and cannot be changed', 55 | readOnly: true 56 | }, 57 | { 58 | name: 'sku', 59 | title: 'SKU', 60 | type: 'string', 61 | description: 'This comes from Shopify and cannot be changed', 62 | readOnly: true 63 | }, 64 | { 65 | name: 'productDescription', 66 | title: 'Product Description', 67 | type: 'blockContent' 68 | }, 69 | ] 70 | } 71 | -------------------------------------------------------------------------------- /web/src/components/product/hero.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import BlockContent from "@sanity/block-content-to-react" 4 | 5 | import { Serializer } from "src/utils/serializer" 6 | import { Image } from 'src/components/image' 7 | import { ProductForm } from './form' 8 | 9 | export const ProductHero = ({ product, main: { title, productDescription, linkedSite, linkedSiteName, mainImage }}: { 10 | main: { 11 | title?: string 12 | subTitle?: string 13 | slug: {} 14 | productDescription?: [] 15 | mainImage: { 16 | asset: { 17 | _id: string 18 | } 19 | } 20 | linkedSite: string 21 | linkedSiteName: string 22 | cerealImage: {} 23 | } 24 | product: { 25 | defaultPrice: string 26 | productId: number 27 | } 28 | }) => { 29 | return ( 30 |
31 |
32 |
33 |
34 | {title} 35 |
36 |
37 |
38 |

{title}

39 | {productDescription && ()} 40 | {linkedSite && linkedSiteName && ( 41 |
42 | Shop the real product on the {linkedSiteName} Website. 43 |
44 | )} 45 | 46 |
47 |
48 |
49 |
50 |
51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /studio/schemas/modules/productModule.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'Product Content', 3 | name: 'productModule', 4 | type: 'object', 5 | fieldsets: [ 6 | { 7 | name: 'modules', 8 | title: 'Product Modules', 9 | options: { 10 | collapsible: true, 11 | collapsed: true 12 | } 13 | }, 14 | { 15 | name: 'main', 16 | title: 'Product Main Content', 17 | options: { 18 | collapsible: true, 19 | collapsed: true 20 | } 21 | } 22 | ], 23 | fields: [ 24 | { 25 | name: 'title', 26 | title: 'Title', 27 | type: 'string' 28 | }, 29 | { 30 | name: 'slug', 31 | title: 'Slug', 32 | type: 'slug', 33 | readOnly: true, 34 | description: 'This has to stay in sync with Shopify', 35 | options: { 36 | source: 'content.main.title', 37 | maxLength: 96 38 | }, 39 | validation: Rule => Rule.required() 40 | }, 41 | { 42 | name: 'linkedSite', 43 | title: 'Linked Site Url', 44 | description: 'This is a fake product so link to the real site!', 45 | type: 'url' 46 | }, 47 | { 48 | name: 'linkedSiteName', 49 | title: 'Linked Site Name', 50 | description: 'Linked Site Title', 51 | type: 'string' 52 | }, 53 | { 54 | name: 'mainImage', 55 | title: 'Main image', 56 | type: 'image', 57 | options: { 58 | hotspot: true 59 | }, 60 | fieldset: 'main', 61 | validation: Rule => Rule.required() 62 | }, 63 | { 64 | name: 'productDescription', 65 | title: 'Product Description', 66 | type: 'blockContent', 67 | fieldset: 'main' 68 | }, 69 | { 70 | name: 'modules', 71 | title: 'Modules', 72 | type: 'moduleContent', 73 | fieldset: 'modules' 74 | } 75 | ] 76 | } 77 | -------------------------------------------------------------------------------- /web/src/templates/product.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import cx from 'classnames' 3 | 4 | import { RenderModules } from 'src/utils/renderModules' 5 | 6 | import { ProductHero } from 'src/components/product/hero' 7 | import { ProductSchema } from 'src/components/product/schema' 8 | 9 | import { useSetPage } from 'src/context/siteContext' 10 | import { SEO } from 'src/components/SEO' 11 | 12 | export interface ProductProps { 13 | pageContext: { 14 | modules: any[], 15 | slug: { 16 | current: string 17 | }, 18 | main: {} 19 | title: string 20 | meta: {} 21 | shopify: {} 22 | site: { 23 | defaultMeta: {} 24 | } 25 | } 26 | preview?: boolean 27 | } 28 | 29 | const Product = ({ 30 | pageContext, 31 | preview = false 32 | }: ProductProps) => { 33 | const setPage = useSetPage() 34 | const { main } = pageContext 35 | const { 36 | modules, 37 | slug, 38 | site, 39 | title, 40 | meta, 41 | shopify 42 | } = pageContext 43 | useEffect(() => { 44 | setPage(slug) 45 | }, [0]) 46 | 47 | const url = `products/${slug}` 48 | return ( 49 |
50 | {preview && ( 51 |
This is a Preview
52 | )} 53 |
56 | ${JSON.stringify(ProductSchema(main, shopify))} 57 | 58 | ` 59 | }} /> 60 | 61 |
62 | 63 | {RenderModules(modules)} 64 |
65 |
66 | ) 67 | } 68 | 69 | export default Product -------------------------------------------------------------------------------- /studio/schemas/modules/socialLink.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FaApple, 3 | FaFacebookF, 4 | FaInstagram, 5 | FaSoundcloud, 6 | FaSpotify, 7 | FaTwitter, 8 | FaYoutube, 9 | FaGithub, 10 | FaLinkedinIn 11 | } from 'react-icons/fa' 12 | 13 | const getIcon = icon => { 14 | switch (icon) { 15 | case 'Apple': 16 | return FaApple 17 | case 'facebook': 18 | return FaFacebookF 19 | case 'instagram': 20 | return FaInstagram 21 | case 'Soundcloud': 22 | return FaSoundcloud 23 | case 'Spotify': 24 | return FaSpotify 25 | case 'twitter': 26 | return FaTwitter 27 | case 'youtube': 28 | return FaYoutube 29 | case 'linkedin': 30 | return FaLinkedinIn 31 | case 'github': 32 | return FaGithub 33 | default: 34 | return false 35 | } 36 | } 37 | 38 | export default { 39 | title: 'Social Link', 40 | name: 'socialLink', 41 | type: 'object', 42 | options: { 43 | columns: 2, 44 | collapsible: false 45 | }, 46 | fields: [ 47 | { 48 | title: 'Icon', 49 | name: 'icon', 50 | type: 'string', 51 | options: { 52 | list: [ 53 | { title: 'Apple', value: 'apple' }, 54 | { title: 'Facebook', value: 'facebook' }, 55 | { title: 'Instagram', value: 'instagram' }, 56 | { title: 'Twitter', value: 'twitter' }, 57 | { title: 'Linkedin', value: 'linkedin' }, 58 | { title: 'Github', value: 'github' }, 59 | { title: 'YouTube', value: 'youtube' } 60 | ] 61 | } 62 | }, 63 | { 64 | title: 'URL', 65 | name: 'url', 66 | type: 'url' 67 | } 68 | ], 69 | preview: { 70 | select: { 71 | icon: 'icon', 72 | url: 'url' 73 | }, 74 | prepare({ icon, url }) { 75 | return { 76 | title: icon, 77 | subtitle: url ? url : '(url not set)', 78 | media: getIcon(icon) 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /web/gatsby-node.js: -------------------------------------------------------------------------------- 1 | // Setup environment variables 2 | require('dotenv').config({ path: `.env.${process.env.NODE_ENV || 'development'}` }); 3 | 4 | const { filter } = require('rxjs/operators'); 5 | const client = require('./src/api/sanity'); 6 | 7 | const { 8 | getAllPageData, 9 | createAllPages, 10 | } = require('./src/build/createPages'); 11 | 12 | const CURRENT_COMMIT = require('child_process') 13 | .execSync('git rev-parse HEAD') 14 | .toString() 15 | .trim(); 16 | 17 | exports.createPages = ({ actions }) => new Promise((resolve, reject) => { 18 | getAllPageData() 19 | .then(allResponses => { 20 | createAllPages( 21 | allResponses, 22 | actions, 23 | resolve, 24 | reject 25 | ) 26 | }); 27 | }); 28 | 29 | exports.onCreateWebpackConfig = ({ 30 | plugins, 31 | actions, 32 | }) => { 33 | actions.setWebpackConfig({ 34 | plugins: [ 35 | plugins.define({ 36 | 'process.env.GITCOMMIT': JSON.stringify(CURRENT_COMMIT), 37 | }), 38 | ], 39 | }); 40 | }; 41 | 42 | exports.onCreatePage = async ({ page, actions }) => { 43 | const { deletePage } = actions; 44 | 45 | // Delete dev 404 page for accounts to work in dev 46 | if (page.internalComponentName === 'ComponentDev404Page') { 47 | deletePage(page); 48 | } 49 | }; 50 | 51 | exports.sourceNodes = async ({ 52 | actions, 53 | createContentDigest, 54 | createNodeId, 55 | }) => { 56 | client 57 | .listen('*[!(_id in path("_.**"))]') 58 | .pipe(filter(event => !event.documentId.startsWith('drafts.'))) 59 | .subscribe(() => { 60 | const update = { date: new Date() }; 61 | 62 | actions.createNode({ 63 | id: createNodeId(1), 64 | internal: { 65 | type: 'update', 66 | content: JSON.stringify(update), 67 | contentDigest: createContentDigest(update), 68 | }, 69 | }); 70 | 71 | console.log('[gatsby-node]: CMS update triggered'); 72 | }) 73 | }; -------------------------------------------------------------------------------- /studio/schemas/modules/defaultVariant.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'Product variant', 3 | name: 'defaultVariant', 4 | type: 'object', 5 | description: `This information is sync'd from Shopify and should not be modified here but is mostly just a reference.`, 6 | fieldsets: [ 7 | { 8 | name: 'information', 9 | title: 'Variant Information', 10 | options: { 11 | collapsible: true, 12 | collapsed: true 13 | } 14 | } 15 | ], 16 | fields: [ 17 | { 18 | title: 'Title', 19 | name: 'title', 20 | readOnly: true, 21 | type: 'string', 22 | fieldset: 'information' 23 | }, 24 | { 25 | title: 'Weight in grams', 26 | name: 'grams', 27 | readOnly: true, 28 | type: 'number', 29 | fieldset: 'information' 30 | }, 31 | { 32 | title: 'Price', 33 | name: 'price', 34 | readOnly: true, 35 | type: 'string', 36 | fieldset: 'information' 37 | }, 38 | { 39 | title: 'Variant Id', 40 | name: 'variantId', 41 | readOnly: true, 42 | type: 'number', 43 | fieldset: 'information' 44 | }, 45 | { 46 | title: 'SKU', 47 | name: 'sku', 48 | readOnly: true, 49 | type: 'string', 50 | fieldset: 'information' 51 | }, 52 | { 53 | title: 'Taxable', 54 | name: 'taxable', 55 | readOnly: true, 56 | type: 'boolean', 57 | fieldset: 'information' 58 | }, { 59 | title: 'Inventory Policy', 60 | name: 'inventoryPolicy', 61 | readOnly: true, 62 | type: 'string', 63 | fieldset: 'information' 64 | }, 65 | { 66 | title: 'Inventory Quantity', 67 | name: 'inventoryQuantity', 68 | readOnly: true, 69 | type: 'number', 70 | fieldset: 'information' 71 | }, 72 | { 73 | title: 'Bar code', 74 | name: 'barcode', 75 | readOnly: true, 76 | type: 'string', 77 | fieldset: 'information' 78 | } 79 | ] 80 | } 81 | -------------------------------------------------------------------------------- /web/src/lambda/login.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayEvent } from 'aws-lambda' 2 | import axios from 'axios' 3 | 4 | import { 5 | statusReturn, 6 | preparePayload, 7 | shopifyConfig, 8 | SHOPIFY_GRAPHQL_URL, 9 | CUSTOMER_QUERY, 10 | CUSTOMER_TOKEN_QUERY 11 | } from './requestConfig' 12 | 13 | let data: { 14 | email?: string 15 | password?: string 16 | } 17 | 18 | let accessToken 19 | 20 | export const handler = async (event: APIGatewayEvent): Promise => { 21 | if (event.httpMethod !== 'POST' || !event.body) return statusReturn(400, {}) 22 | 23 | try { 24 | data = JSON.parse(event.body) 25 | } catch (error) { 26 | console.log('JSON parsing error:', error); 27 | return statusReturn(400, { error: 'Bad Request Body' }) 28 | } 29 | 30 | const payload = preparePayload(CUSTOMER_TOKEN_QUERY, { 31 | input: { 32 | email: data.email, 33 | password: data.password 34 | } 35 | }) 36 | try { 37 | const token = await axios({ 38 | url: SHOPIFY_GRAPHQL_URL, 39 | method: 'POST', 40 | headers: shopifyConfig, 41 | data: JSON.stringify(payload) 42 | }) 43 | if (token.data.data.customerAccessTokenCreate.userErrors.length > 0) { 44 | throw token.data.data.customerAccessTokenCreate.userErrors 45 | } else { 46 | accessToken = token.data.data.customerAccessTokenCreate.customerAccessToken.accessToken 47 | } 48 | } catch (err) { 49 | return statusReturn(200, { error: 'Problem with email or password' }) 50 | } 51 | 52 | const payloadCustomer = preparePayload(CUSTOMER_QUERY, { 53 | customerAccessToken: accessToken 54 | }) 55 | 56 | try { 57 | let customer = await axios({ 58 | url: SHOPIFY_GRAPHQL_URL, 59 | method: 'POST', 60 | headers: shopifyConfig, 61 | data: JSON.stringify(payloadCustomer) 62 | }) 63 | customer = customer.data.data.customer 64 | return statusReturn(200, { 65 | token: accessToken, 66 | customer 67 | }) 68 | } catch (err) { 69 | return statusReturn(500, { error: err.message }) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /studio/schemas/modules/metaCard.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'Meta Information', 3 | name: 'metaCard', 4 | type: 'object', 5 | fieldsets: [ 6 | { 7 | name: 'opengraph', 8 | title: 'Open Graph Protocol', 9 | options: { 10 | collapsible: true, 11 | collapsed: true 12 | } 13 | }, 14 | { 15 | name: 'twitter', 16 | title: 'Twitter Protocol', 17 | options: { 18 | collapsible: true, 19 | collapsed: true 20 | } 21 | } 22 | ], 23 | fields: [ 24 | { 25 | name: 'metaKeywords', 26 | title: 'Meta Keywords', 27 | type: 'string' 28 | }, 29 | { 30 | name: 'metaTitle', 31 | title: 'Meta Title (overrides default title)', 32 | type: 'string' 33 | }, 34 | { 35 | name: 'metaDescription', 36 | title: 'Meta Description', 37 | type: 'string' 38 | }, 39 | { 40 | name: 'openImage', 41 | title: 'Open Graph Image', 42 | type: 'image', 43 | description: 'Ideal size for open graph images is 1200 x 600', 44 | options: { 45 | hotspot: true 46 | }, 47 | fieldset: 'opengraph' 48 | }, 49 | { 50 | name: 'openTitle', 51 | title: 'Open Graph Title', 52 | type: 'string', 53 | fieldset: 'opengraph' 54 | }, 55 | { 56 | name: 'openGraphDescription', 57 | title: 'Open Graph Description', 58 | type: 'text', 59 | fieldset: 'opengraph' 60 | }, 61 | { 62 | name: 'twitterImage', 63 | title: 'Twitter Image', 64 | type: 'image', 65 | description: 'Ideal size for twitter images is 800 x 418', 66 | fieldset: 'twitter', 67 | options: { 68 | hotspot: true 69 | } 70 | }, 71 | { 72 | name: 'twitterTitle', 73 | title: 'Twitter Card Title', 74 | type: 'string', 75 | fieldset: 'twitter' 76 | }, 77 | { 78 | name: 'twitterDescription', 79 | title: 'Twitter Description', 80 | type: 'text', 81 | fieldset: 'twitter' 82 | } 83 | ] 84 | } 85 | -------------------------------------------------------------------------------- /web/src/stories/assets/plugin.svg: -------------------------------------------------------------------------------- 1 | illustration/plugin -------------------------------------------------------------------------------- /web/src/pages/docs.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useCallback} from "react" 2 | import { Router } from "@reach/router" 3 | 4 | import { useLoads } from 'react-loads' 5 | 6 | import Documentation from "src/templates/documentation" 7 | 8 | import { sanityClient } from 'src/api/sanity' 9 | 10 | import { 11 | pageQuery 12 | } from "src/api/queries" 13 | 14 | 15 | const PreviewPage = ({ document }: { document: string }) => { 16 | const [doc, setDoc] = useState(null as any) 17 | 18 | 19 | // @ts-ignore 20 | const queryDraft = `*[_id == "${document}"] { 21 | ..., 22 | }` 23 | 24 | // @ts-ignore 25 | const queryPreviewDocs= `*[_id == "${document}"] { 26 | ${pageQuery} 27 | }` 28 | 29 | const handlePreviewFetch = useCallback( 30 | () => 31 | sanityClient 32 | .fetch(queryDraft) 33 | .then((response: any) => { 34 | switch (response[0]._type) { 35 | case 'doc': 36 | sanityClient.fetch(queryPreviewDocs).then(res => { 37 | setDoc(res[0]) 38 | }) 39 | break 40 | default: 41 | break 42 | } 43 | }), 44 | [] 45 | ) 46 | 47 | const { error, isResolved, isPending, isReloading, load } = useLoads( 48 | 'handlePreviewFetch', 49 | handlePreviewFetch as any, 50 | { 51 | defer: true, 52 | } 53 | ) 54 | 55 | useEffect(() => { 56 | load() 57 | }, [0]) 58 | 59 | const renderPreview = () => { 60 | if (doc) { 61 | switch (doc._type) { 62 | case 'doc': return 63 | default: break 64 | } 65 | } 66 | } 67 | return ( 68 | <> 69 | {(isPending || 70 | isReloading) && ( 71 |
Loading
72 | )} 73 | {isResolved && !isPending && renderPreview()} 74 | 75 | ) 76 | } 77 | 78 | 79 | const Docs = () => { 80 | return ( 81 |
82 | 83 | 84 | 85 |
86 | ) 87 | } 88 | 89 | export default Docs 90 | -------------------------------------------------------------------------------- /web/src/styles/main.scss: -------------------------------------------------------------------------------- 1 | // Configuration 2 | @import '~magic-tricks/sass/config'; 3 | 4 | // Configuration Override 5 | @import './lib/config'; 6 | @import './lib/containers'; 7 | @import './lib/borders'; 8 | @import './lib/drawer'; 9 | // @import './lib/fonts'; 10 | // @import './lib/global'; 11 | // @import './lib/typography'; 12 | 13 | // 14 | // 15 | // // 16 | // === Put Configuration Overrides Here === // 17 | // // 18 | 19 | // Mixins 20 | @import '~magic-tricks/sass/mixins'; 21 | 22 | // CSS Reset 23 | @import '~magic-tricks/sass/reset'; 24 | 25 | // Utilities 26 | @import '~magic-tricks/sass/utils/alignment'; 27 | @import '~magic-tricks/sass/utils/background'; 28 | @import '~magic-tricks/sass/utils/border'; 29 | @import '~magic-tricks/sass/utils/color'; 30 | @import '~magic-tricks/sass/utils/cursor'; 31 | @import '~magic-tricks/sass/utils/display'; 32 | @import '~magic-tricks/sass/utils/float'; 33 | @import '~magic-tricks/sass/utils/font-weight'; 34 | @import '~magic-tricks/sass/utils/margin'; 35 | @import '~magic-tricks/sass/utils/opacity'; 36 | @import '~magic-tricks/sass/utils/overflow'; 37 | @import '~magic-tricks/sass/utils/pointer-events'; 38 | @import '~magic-tricks/sass/utils/position'; 39 | @import '~magic-tricks/sass/utils/text-align'; 40 | @import '~magic-tricks/sass/utils/transform'; 41 | @import '~magic-tricks/sass/utils/transitions'; 42 | @import '~magic-tricks/sass/utils/whitespace'; 43 | @import '~magic-tricks/sass/utils/flex'; 44 | @import '~magic-tricks/sass/utils/z-index'; 45 | 46 | // Utility Components 47 | @import '~magic-tricks/sass/components/grid-container'; 48 | @import '~magic-tricks/sass/components/inline-grid'; 49 | @import '~magic-tricks/sass/components/spacer'; 50 | @import '~magic-tricks/sass/components/visibility'; 51 | @import '~magic-tricks/sass/components/image'; 52 | 53 | // MIDWAY 54 | @import './midway/typography'; 55 | @import './midway/learn'; 56 | @import './midway/global'; 57 | @import './midway/code'; 58 | @import './midway/button'; 59 | @import './midway/nested-pages'; 60 | @import './midway/footer'; 61 | 62 | @import './midway/products/card'; -------------------------------------------------------------------------------- /web/src/lambda/reset-password.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayEvent } from 'aws-lambda' 2 | import axios from 'axios' 3 | 4 | import { 5 | statusReturn, 6 | preparePayload, 7 | shopifyConfig, 8 | SHOPIFY_GRAPHQL_URL, 9 | CUSTOMER_TOKEN_QUERY, 10 | CUSTOMER_RESET_QUERY 11 | } from './requestConfig' 12 | 13 | let customer 14 | 15 | export const handler = async (event: APIGatewayEvent): Promise => { 16 | 17 | // TEST for POST request 18 | if (event.httpMethod !== 'POST' || !event.body) { 19 | return statusReturn(400, '') 20 | } 21 | 22 | let data 23 | 24 | try { 25 | data = JSON.parse(event.body) 26 | } catch (error) { 27 | console.log('JSON parsing error:', error); 28 | return statusReturn(400, { error: 'Bad request body' }) 29 | } 30 | const payload = preparePayload(CUSTOMER_RESET_QUERY, { 31 | id: data.id, 32 | input: data.input 33 | }) 34 | 35 | try { 36 | customer = await axios({ 37 | url: SHOPIFY_GRAPHQL_URL, 38 | method: 'POST', 39 | headers: shopifyConfig, 40 | data: JSON.stringify(payload) 41 | }) 42 | 43 | if (customer.data.data.customerReset.userErrors.length > 0) { 44 | throw customer.data.data.customerReset.userErrors 45 | } else { 46 | customer = customer.data.data.customerReset.customer 47 | } 48 | } catch (err) { 49 | return statusReturn(500, { error: err[0].message }) 50 | } 51 | 52 | const loginPayload = preparePayload(CUSTOMER_TOKEN_QUERY, { 53 | input: { 54 | email: customer.email, 55 | password: data.input.password 56 | } 57 | }) 58 | 59 | try { 60 | let token = await axios({ 61 | url: SHOPIFY_GRAPHQL_URL, 62 | method: 'POST', 63 | headers: shopifyConfig, 64 | data: JSON.stringify(loginPayload) 65 | }) 66 | if (token.data.data.customerAccessTokenCreate.userErrors.length > 0) { 67 | throw token.data.data.customerAccessTokenCreate.userErrors 68 | } else { 69 | token = token.data.data.customerAccessTokenCreate.customerAccessToken.accessToken 70 | return statusReturn(200, { 71 | token, 72 | customer 73 | }) 74 | } 75 | } catch (err) { 76 | return statusReturn(500, { error: err.message }) 77 | } 78 | } -------------------------------------------------------------------------------- /web/src/api/queries.js: -------------------------------------------------------------------------------- 1 | const groq = require('groq') 2 | 3 | const slugQuery = groq` 4 | 'slug': content.main.slug.current 5 | ` 6 | 7 | const asset = groq`{ 8 | _type, 9 | _key, 10 | alt, 11 | caption, 12 | '_id': image.asset->_id, 13 | 'dimensions': image.asset->metadata.dimensions, 14 | 'url': image.asset->url, 15 | }` 16 | 17 | const SEOQuery = groq` 18 | _type, 19 | metaKeywords, 20 | metaDescription, 21 | metaTitle, 22 | openGraphDescription, 23 | 'openImage': openImage.asset->url, 24 | openTitle, 25 | twitterTitle, 26 | twitterDescription, 27 | 'twitterImage': twitterImage.asset->url 28 | ` 29 | 30 | const moduleQuery = groq` 31 | _type == 'nestedPages' => { 32 | ..., 33 | page[] { 34 | ..., 35 | linkedPage-> 36 | } 37 | }, 38 | _type == 'productGrid' => { 39 | ..., 40 | products[]-> { 41 | ..., 42 | } 43 | } 44 | ` 45 | 46 | const pageQuery = groq` 47 | ${slugQuery}, 48 | 'title': content.main.title, 49 | ..., 50 | 'meta': content.meta { 51 | ${SEOQuery} 52 | }, 53 | 'modules': content.main.modules[] { 54 | ..., 55 | ${moduleQuery} 56 | }, 57 | ` 58 | 59 | const productQuery = groq` 60 | ${slugQuery}, 61 | 'title': content.main.title, 62 | ..., 63 | 'meta': content.meta { 64 | ${SEOQuery} 65 | }, 66 | 'modules': content.main.modules[] { 67 | ..., 68 | ${moduleQuery} 69 | }, 70 | 'shopify': content.shopify, 71 | 'main': content.main { 72 | ..., 73 | mainImage { 74 | asset-> { 75 | _id 76 | } 77 | } 78 | } 79 | ` 80 | module.exports.global = groq`*[_type == "siteGlobal"][0] { 81 | ..., 82 | 'defaultMeta': content.meta { 83 | ${SEOQuery} 84 | }, 85 | 'social': content.social.socialLinks 86 | }` 87 | 88 | module.exports.collections = groq`*[_type == "collection"] { 89 | ${pageQuery} 90 | }` 91 | module.exports.pages = groq`*[_type == "page"] { 92 | ${pageQuery} 93 | }` 94 | 95 | module.exports.products = groq`*[_type == "product"]{ 96 | ${productQuery} 97 | }` 98 | 99 | 100 | module.exports.pageQuery = pageQuery 101 | module.exports.productQuery = productQuery -------------------------------------------------------------------------------- /studio/schemas/types/subscription.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default { 4 | name: 'subscription', 5 | title: 'Subscriptions', 6 | type: 'document', 7 | __experimental_actions: ['update', 'publish', 'delete'], 8 | fields: [ 9 | { 10 | name: 'title', 11 | title: 'Title', 12 | type: 'string', 13 | readOnly: true, 14 | }, 15 | { 16 | name: 'discountAmount', 17 | title: 'Discount Amount', 18 | type: 'number', 19 | readOnly: true, 20 | }, 21 | { 22 | name: 'discountType', 23 | title: 'Discount Type', 24 | type: 'string', 25 | list: ['percentage', 'fixed_amount'], 26 | readOnly: true, 27 | }, 28 | { 29 | name: 'chargeIntervalFrequency', 30 | title: 'Charge Interval Frequency', 31 | type: 'number', 32 | readOnly: true, 33 | }, 34 | { 35 | name: 'cutoffDayOfMonth', 36 | title: 'Cutoff Day of Month', 37 | type: 'number', 38 | readOnly: true, 39 | }, 40 | { 41 | name: 'cutoffDayOfWeek', 42 | title: 'Cutoff Day of Week', 43 | type: 'number', 44 | readOnly: true, 45 | }, 46 | { 47 | name: 'expireAfterSpecificNumberOfCharges', 48 | title: 'Expire after specific number of charges', 49 | type: 'number', 50 | readOnly: true, 51 | }, 52 | { 53 | name: 'modifiableProperties', 54 | title: 'Modifiable Properties', 55 | type: 'array', 56 | of: [{type: 'string'}], 57 | readOnly: true, 58 | }, 59 | { 60 | name: 'numberChargesUntilExpiration', 61 | title: 'Number of charges until expiration', 62 | type: 'number', 63 | readOnly: true, 64 | }, 65 | { 66 | name: 'orderDayOfMonth', 67 | title: 'Order day of month', 68 | type: 'number', 69 | readOnly: true, 70 | }, 71 | { 72 | name: 'orderDayOfWeek', 73 | title: 'Order day of week', 74 | type: 'number', 75 | readOnly: true, 76 | }, 77 | { 78 | name: 'orderIntervalFrequencyOptions', 79 | title: 'Order interval frequency options', 80 | type: 'array', 81 | of: [{type: 'number'}], 82 | readOnly: true, 83 | }, 84 | { 85 | name: 'orderIntervalUnit', 86 | title: 'Order interval unit', 87 | type: 'string', 88 | list: ['day', 'week', 'month'], 89 | readOnly: true, 90 | }, 91 | { 92 | name: 'storefrontPurchaseOptions', 93 | title: 'Storefront purchase options', 94 | type: 'string', 95 | readOnly: true, 96 | }, 97 | ], 98 | }; -------------------------------------------------------------------------------- /studio/schemas/modules/standardText.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default { 4 | title: 'Standard Text', 5 | name: 'standardText', 6 | type: 'object', 7 | hidden: true, 8 | fields: [ 9 | { 10 | name: 'text', 11 | title: 'Text', 12 | type: 'array', 13 | of: [ 14 | { 15 | title: 'Block', 16 | type: 'block', 17 | styles: [ 18 | {title: 'Normal', value: 'normal'}, 19 | {title: 'H1', value: 'h1'}, 20 | {title: 'H2', value: 'h2'}, 21 | {title: 'H2 - Question', value: 'h2-question'}, 22 | {title: 'H3', value: 'h3'}, 23 | {title: 'H4', value: 'h4'}, 24 | {title: 'H5', value: 'h5'}, 25 | {title: 'H6', value: 'h6'}, 26 | {title: 'Quote', value: 'blockquote'} 27 | ], 28 | // Marks let you mark up inline text in the block editor. 29 | marks: { 30 | // Annotations can be any object structure – e.g. a link or a footnote. 31 | decorators: [ 32 | { value: 'strong', title: 'Strong' }, 33 | { value: 'italic', title: 'Italic' }, 34 | { value: 'underline', title: 'Underline' }, 35 | { value: 'code', title: 'Code' }, 36 | { 37 | title: 'Inline Snippet', 38 | value: 'tick', 39 | blockEditor: { 40 | icon: () => 'T', 41 | render: (props) => ( 42 | {props.children} 43 | ) 44 | } 45 | } 46 | ], 47 | annotations: [ 48 | { 49 | title: 'URL', 50 | name: 'link', 51 | type: 'object', 52 | fields: [ 53 | { 54 | title: 'URL', 55 | name: 'href', 56 | type: 'url' 57 | } 58 | ] 59 | } 60 | ] 61 | }, 62 | }, 63 | ] 64 | } 65 | ], 66 | preview: { 67 | select: { 68 | title: '' 69 | }, 70 | prepare (selection) { 71 | return Object.assign({}, selection, { 72 | title: 'Standard Text' 73 | }) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /web/src/components/svgs.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | interface SvgProps { 4 | className?: string 5 | } 6 | 7 | export const Github = ({ className }: SvgProps ) => ( 8 | 9 | ) 10 | 11 | export const Minus = ({ className }: SvgProps ) => ( 12 | 13 | 14 | 15 | ) 16 | 17 | export const Plus = ({ className }: SvgProps ) => ( 18 | 19 | 20 | 21 | ) 22 | 23 | export const Close = ({ className }: SvgProps ) => ( 24 | 25 | 26 | 27 | ) -------------------------------------------------------------------------------- /web/src/stories/assets/stackalt.svg: -------------------------------------------------------------------------------- 1 | illustration/stackalt -------------------------------------------------------------------------------- /web/src/components/product/schema.tsx: -------------------------------------------------------------------------------- 1 | const siteRoute = "https://midway-starter.netlify.com" 2 | 3 | function toPlainText(blocks = []) { 4 | return blocks 5 | .map((block: { 6 | _type: string 7 | children: any 8 | }) => { 9 | if (block._type !== 'block' || !block.children) { 10 | return '' 11 | } 12 | return block.children.map((child: { text: any }) => child.text).join('') 13 | }) 14 | .join('\n\n') 15 | } 16 | 17 | export const ProductSchema = (main: { 18 | title: string 19 | productDescription?: [] 20 | mainImage?: { 21 | asset: { 22 | url: string 23 | } 24 | } 25 | slug: { 26 | current: string 27 | } 28 | }, shopify: { 29 | defaultVariant: { 30 | sku: string 31 | price: string 32 | } 33 | }) => { 34 | const schema = { 35 | "@context": "https://schema.org/", 36 | "@type": "Product", 37 | "name": main.title, 38 | "image": main.mainImage && main.mainImage.asset.url, 39 | "description": main.productDescription && toPlainText(main.productDescription), 40 | "sku": shopify.defaultVariant.sku, 41 | "mpn": shopify.defaultVariant.sku, 42 | "price": shopify.defaultVariant.price, 43 | "brand": { 44 | "@type": "Thing", 45 | "name": "Midway" 46 | }, 47 | "offers": { 48 | "@type": "Offer", 49 | "url": `${siteRoute}/products/${main.slug.current}`, 50 | "priceCurrency": "USD", 51 | "price": shopify.defaultVariant.price, 52 | "itemCondition": "https://schema.org/UsedCondition", 53 | "availability": "https://schema.org/InStock", 54 | "seller": { 55 | "@type": "Organization", 56 | "name": "Midway" 57 | } 58 | }, 59 | // FIXME: If you have reviews modify this area 60 | // "aggregrateRating": { 61 | // "@type": "AggregateRating", 62 | // "ratingValue": '4.5, 63 | // "itemReviewed": { 64 | // "@type": "Product", 65 | // "name": title, 66 | // "brand": "Midway" 67 | // } 68 | // }, 69 | // FIXME: If you have reviews modify this area 70 | // "review": { 71 | // "@type": "Review", 72 | // "reviewRating": { 73 | // "@type": "Rating", 74 | // "ratingValue": reviews && reviews.reviews[0] && reviews.reviews[0][0].node.score 75 | // }, 76 | // "author": { 77 | // "@type": "Person", 78 | // "name": reviews && reviews.reviews[0] && reviews.reviews[0][0].node.name 79 | // }, 80 | // "reviewBody": reviews && reviews.reviews[0] && reviews.reviews[0][0].node.content 81 | // } 82 | } 83 | return schema 84 | } -------------------------------------------------------------------------------- /studio/structure/previews/IframePreview.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-multi-comp, react/no-did-mount-set-state */ 2 | import React from 'react' 3 | import PropTypes from 'prop-types' 4 | import styles from './IframePreview.css' 5 | 6 | const assembleProjectUrl = ({displayed, draft, options}) => { 7 | const {content: {main: {slug}}} = displayed 8 | const {previewURL} = options 9 | if (!slug || !previewURL) { 10 | console.warn('Missing slug or previewURL', {slug, previewURL}) 11 | return '' 12 | } 13 | return `${previewURL}/${draft ? draft._id : displayed._id}` 14 | } 15 | 16 | class IframePreview extends React.Component { 17 | constructor(props) { 18 | super(props) 19 | this.state = { 20 | url: null, 21 | changing: false 22 | } 23 | } 24 | static propTypes = { 25 | document: PropTypes.object // eslint-disable-line react/forbid-prop-types 26 | } 27 | 28 | static defaultProps = { 29 | document: null 30 | } 31 | 32 | componentDidMount() { 33 | const {options} = this.props 34 | const {displayed, draft} = this.props.document 35 | console.log('build the url', assembleProjectUrl({displayed, draft, options})) 36 | this.setState({ 37 | url: assembleProjectUrl({displayed, draft, options}) 38 | }) 39 | } 40 | 41 | componentDidUpdate(prevProps) { 42 | const {options} = this.props 43 | const {displayed, draft} = this.props.document 44 | if (prevProps.document.draft !== null && this.props.document.draft) { 45 | if (this.props.document.draft._updatedAt !== prevProps.document.draft._updatedAt) { 46 | this.setState({ 47 | url: assembleProjectUrl({displayed, draft, options}), 48 | changing: true 49 | }) 50 | setTimeout(() => { 51 | this.setState({ 52 | changing: false 53 | }) 54 | }, 200) 55 | } 56 | } 57 | } 58 | 59 | render () { 60 | const {displayed} = this.props.document 61 | if (!displayed) { 62 | return (
63 |

There is no document to preview

64 |
) 65 | } 66 | 67 | 68 | if (!this.state.url) { 69 | return (
70 |

Hmm. Having problems constructing the web front-end URL.

71 |
) 72 | } 73 | 74 | return ( 75 |
76 |
77 | {!this.state.changing && ( 78 | 105 | `})} 106 | {/* Facebook */} 107 | {facebookPixelId && ( 108 | )} 119 | {facebookPixelId &&( 120 | )} 128 | 129 | ); -------------------------------------------------------------------------------- /web/src/components/auth/reset.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from 'react' 2 | import Helmet from 'react-helmet' 3 | import fetch from 'unfetch' 4 | import { encode } from 'shopify-gid' 5 | import { useLoads } from 'react-loads' 6 | import { navigate } from 'gatsby' 7 | import Timeout from 'await-timeout' 8 | 9 | import { ErrorHandling } from 'src/utils/error' 10 | import { PasswordSchema } from 'src/utils/schema' 11 | import { UpdateCustomer } from "src/utils/updateCustomer" 12 | 13 | export const Reset = (props: { 14 | path: string 15 | id?: string 16 | token?: string 17 | }) => { 18 | const [passwordField1, setPasswordField1] = useState("") 19 | const [passwordField2, setPasswordField2] = useState("") 20 | const [submit, setSubmitting] = useState(false) 21 | const [formSuccess, setFormSucces] = useState(false) 22 | const form = React.createRef() as React.RefObject 23 | 24 | const handleReset = useCallback( 25 | async (password) => { 26 | 27 | if (!PasswordSchema.validate(passwordField1)) { 28 | throw new Error( 29 | "Your password should be between 8 and 100 characters, and have at least one lowercase and one uppercase letter." 30 | ) 31 | } 32 | 33 | if (passwordField1 !== passwordField2) { 34 | await Timeout.set(400) 35 | throw new Error("Passwords do not match.") 36 | } 37 | fetch(`/.netlify/functions/reset-password`, { 38 | method: 'POST', 39 | body: JSON.stringify({ 40 | id: encode('Customer', props.id), 41 | input: { 42 | resetToken: props.token, 43 | password 44 | } 45 | }) 46 | }) 47 | .then(res => res.json()) 48 | .then(res => { 49 | if (res.error) { 50 | throw new Error(res.error) 51 | setSubmitting(false) 52 | } else { 53 | setFormSucces(true) 54 | // UpdateCustomer(res, res.customer.email) 55 | // re-hydrate the cart so it contains the email 56 | // checkout.hydrate() 57 | setTimeout(() => { 58 | navigate('/account/login') 59 | }, 400) 60 | } 61 | }) 62 | }, 63 | [passwordField1, passwordField2] 64 | ) 65 | 66 | const { error, isRejected, isPending, isReloading, load } = useLoads( 67 | "handleReset", 68 | handleReset as any, 69 | { 70 | defer: true 71 | } 72 | ) 73 | 74 | const handleSubmit = (e: React.FormEvent) => { 75 | e.preventDefault() 76 | setSubmitting(true) 77 | const { password } = form!.current!.elements 78 | load(password.value) 79 | } 80 | return ( 81 |
82 | 83 |
84 |
85 |
86 |
handleSubmit(e)} ref={form}> 87 |
88 |
Reset Your Password
89 |

Let's get you logged back in.

90 |
91 | {(isPending || 92 | isReloading) && ( 93 | Loading 94 | )} 95 | 96 | {isRejected && } 97 | 98 |
99 | {formSuccess && ( 100 |
Got it! Email coming your way now.
101 | )} 102 |
103 |
Password
104 | setPasswordField1(e.target.value)} required={true} className='accounts__input px1 py1 s16 x' placeholder='Password' /> 105 |
106 |
107 |
Confirm Password
108 | setPasswordField2(e.target.value)} required={true} className='accounts__input mb1 px1 py1 s16 x' placeholder='Confirm Password' /> 109 |
110 |
111 | 112 |
113 |
114 | 115 |
116 |
117 |
118 | ) 119 | } 120 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "midway", 3 | "private": true, 4 | "description": "Boilerplate for Gatsby + Sanity + Shopify", 5 | "version": "0.1.0", 6 | "author": "You ", 7 | "dependencies": { 8 | "@asbjorn/eslint-plugin-groq": "^1.0.0", 9 | "@babel/plugin-transform-object-assign": "^7.10.4", 10 | "@babel/template": "^7.10.4", 11 | "@loadable/component": "^5.14.1", 12 | "@picostate/react": "^3.0.1", 13 | "@sanity/block-content-to-react": "^2.0.7", 14 | "@sanity/client": "^0.147.3", 15 | "@sentry/browser": "^5.15.4", 16 | "@sentry/cli": "^1.52.1", 17 | "await-timeout": "^1.1.1", 18 | "aws-lambda": "^1.0.5", 19 | "classnames": "^2.2.6", 20 | "crypto": "^1.0.1", 21 | "docz": "^2.3.0-alpha.13", 22 | "dotenv": "^8.2.0", 23 | "encoding": "^0.1.13", 24 | "gatsby": "^2.20.29", 25 | "gatsby-image": "^2.2.34", 26 | "gatsby-link": "^2.3.5", 27 | "gatsby-plugin-create-client-paths": "^2.1.22", 28 | "gatsby-plugin-layout": "^1.2.1", 29 | "gatsby-plugin-loadable-components-ssr": "^2.1.0", 30 | "gatsby-plugin-manifest": "^2.2.31", 31 | "gatsby-plugin-netlify": "^2.3.15", 32 | "gatsby-plugin-offline": "^3.0.27", 33 | "gatsby-plugin-postcss": "^2.1.20", 34 | "gatsby-plugin-react-helmet": "^3.1.16", 35 | "gatsby-plugin-robots-txt": "^1.5.2", 36 | "gatsby-plugin-root-import": "^2.0.5", 37 | "gatsby-plugin-sass": "^2.3.13", 38 | "gatsby-plugin-sharp": "^2.3.5", 39 | "gatsby-plugin-tslint": "^0.0.2", 40 | "gatsby-plugin-typescript": "^2.1.27", 41 | "gatsby-react-router-scroll": "^2.2.3", 42 | "gatsby-source-filesystem": "^2.1.40", 43 | "gatsby-source-sanity": "^5.0.5", 44 | "gatsby-transformer-sharp": "^2.3.7", 45 | "groq": "^2.2.6", 46 | "highlight.js": "^9.18.1", 47 | "http-proxy-middleware": "^0.21.0", 48 | "js-cookie": "^2.2.1", 49 | "jsondiffpatch": "^0.4.1", 50 | "klaviyo-subscribe": "^1.0.0", 51 | "magic-tricks": "^0.3.1", 52 | "markdown-it": "^10.0.0", 53 | "netlify-lambda": "^1.6.3", 54 | "node-fetch": "^2.6.1", 55 | "node-sass": "^4.13.1", 56 | "password-validator": "^5.0.3", 57 | "picostate": "^4.0.0", 58 | "postcss": "6.0.23", 59 | "postcss-cli": "^8.0.0", 60 | "postcss-custom-properties": "^10.0.0", 61 | "prismjs": "^1.19.0", 62 | "prop-types": "^15.7.2", 63 | "raw-body": "^2.4.1", 64 | "react": "^16.12.0", 65 | "react-dom": "^16.12.0", 66 | "react-focus-lock": "^2.2.1", 67 | "react-helmet": "^5.2.1", 68 | "react-loads": "^9.0.4", 69 | "react-responsive": "^8.0.1", 70 | "react-transition-group": "^4.3.0", 71 | "reset-css": "^5.0.1", 72 | "shopify-buy": "^2.9.0", 73 | "shopify-gid": "^1.0.1", 74 | "shopify-storefront-api-typings": "^1.1.1", 75 | "spacetime": "^6.4.2", 76 | "svbstrate": "^4.1.1", 77 | "tighpo": "^1.0.1", 78 | "unfetch": "^4.1.0" 79 | }, 80 | "devDependencies": { 81 | "@babel/core": "^7.11.6", 82 | "@types/await-timeout": "^0.3.1", 83 | "@types/classnames": "^2.2.9", 84 | "@types/js-cookie": "^2.2.5", 85 | "@types/node": "^13.11.1", 86 | "@types/react-helmet": "^5.0.15", 87 | "@types/react-transition-group": "^4.2.4", 88 | "babel-loader": "^8.1.0", 89 | "babel-preset-react-app": "^9.1.2", 90 | "prettier": "^1.19.1", 91 | "react-is": "^16.13.1", 92 | "ts-jest": "^24.1.0", 93 | "tslint": "^5.20.0", 94 | "tslint-config-prettier": "^1.18.0", 95 | "tslint-loader": "^3.5.4", 96 | "tslint-plugin-prettier": "^2.0.1", 97 | "tslint-react": "^4.1.0", 98 | "typescript": "^3.7.2" 99 | }, 100 | "keywords": [ 101 | "gatsby" 102 | ], 103 | "license": "MIT", 104 | "scripts": { 105 | "build": "gatsby build", 106 | "build:lambda": "netlify-lambda build src/lambda", 107 | "develop": "gatsby develop", 108 | "dev": "export NETLIFY_DEV=true; netlify dev:exec netlify-lambda serve src/lambda --port 34567 --timeout 20 & netlify dev:exec gatsby develop -H 0.0.0.0", 109 | "format": "prettier --write \"**/*.{js,jsx,json,md}\"", 110 | "__NETLIFY_________": "", 111 | "netlify:dev": "npm run dev", 112 | "__postinstall": "netlify-lambda install", 113 | "__SENTRY__________": "", 114 | "sentry:release": "sentry-cli releases --org ctrl-alt-del-world new -p midway $COMMIT_REF", 115 | "sentry:commits": "sentry-cli releases --org ctrl-alt-del-world set-commits $COMMIT_REF --commit \"ctrl-alt-del-world/midway@$COMMIT_REF\"", 116 | "start": "npm run develop", 117 | "ngrok": "ngrok http 8000", 118 | "ngrok:reserved": "ngrok http 8000 --subdomain midway-shop", 119 | "serve": "gatsby serve", 120 | "clean": "gatsby clean", 121 | "test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\" && exit 1" 122 | }, 123 | "repository": { 124 | "type": "git", 125 | "url": "https://github.com/gatsbyjs/gatsby-starter-default" 126 | }, 127 | "bugs": { 128 | "url": "https://github.com/gatsbyjs/gatsby/issues" 129 | } 130 | } 131 | --------------------------------------------------------------------------------