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 |
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 |
79 | )}
80 |
81 |
82 | )
83 | }
84 | }
85 |
86 | export default IframePreview
--------------------------------------------------------------------------------
/web/src/pages/previews.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 Page from "src/templates/page"
7 | import Product from "src/templates/product"
8 |
9 | import sanityClient from 'src/api/sanity'
10 |
11 | import {
12 | pageQuery,
13 | productQuery
14 | } from "src/api/queries"
15 |
16 |
17 | const PreviewPage = ({ document }: { document: string }) => {
18 | const [doc, setDoc] = useState(null as any)
19 |
20 |
21 | // @ts-ignore
22 | const queryDraft = `*[_id == "${document}"] {
23 | ...,
24 | }`
25 |
26 | // @ts-ignore
27 | const queryPreviewPage = `*[_id == "${document}"] {
28 | ${pageQuery}
29 | }`
30 |
31 | // @ts-ignore
32 | const queryPreviewProduct = `*[_id == "${document}"] {
33 | ${productQuery}
34 | }`
35 |
36 | const handlePreviewFetch = useCallback(
37 | () =>
38 | sanityClient
39 | .fetch(queryDraft)
40 | .then((response: any) => {
41 | switch (response[0]._type) {
42 | case 'page':
43 | sanityClient.fetch(queryPreviewPage).then(res => {
44 | setDoc(res[0])
45 | })
46 | break
47 | case 'product':
48 | sanityClient.fetch(queryPreviewProduct).then(res => {
49 | setDoc(res[0])
50 | })
51 | break
52 | default:
53 | break
54 | }
55 | }),
56 | []
57 | )
58 |
59 | const { error, isResolved, isPending, isReloading, load } = useLoads(
60 | 'handlePreviewFetch',
61 | handlePreviewFetch as any,
62 | {
63 | defer: true,
64 | }
65 | )
66 |
67 | useEffect(() => {
68 | load()
69 | }, [0])
70 |
71 | const renderPreview = () => {
72 | if (doc) {
73 | switch (doc._type) {
74 | case 'page': return
75 | case 'product': return
76 | default: break
77 | }
78 | }
79 | }
80 | return (
81 | <>
82 | {(isPending ||
83 | isReloading) && (
84 | Loading
85 | )}
86 | {isResolved && !isPending && renderPreview()}
87 | >
88 | )
89 | }
90 |
91 |
92 | const Previews = () => {
93 | return (
94 |
99 | )
100 | }
101 |
102 | export default Previews
103 |
--------------------------------------------------------------------------------
/web/src/stories/Page.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Header } from './Header';
4 | import './page.css';
5 |
6 | export interface PageProps {
7 | user?: {};
8 | onLogin: () => void;
9 | onLogout: () => void;
10 | onCreateAccount: () => void;
11 | }
12 |
13 | export const Page: React.FC = ({ user, onLogin, onLogout, onCreateAccount }) => (
14 |
15 |
16 |
17 |
18 | Pages in Storybook
19 |
20 | We recommend building UIs with a{' '}
21 |
22 | component-driven
23 | {' '}
24 | process starting with atomic components and ending with pages.
25 |
26 |
27 | Render pages with mock data. This makes it easy to build and review page states without
28 | needing to navigate to them in your app. Here are some handy patterns for managing page data
29 | in Storybook:
30 |
31 |
32 |
33 | Use a higher-level connected component. Storybook helps you compose such data from the
34 | "args" of child component stories
35 |
36 |
37 | Assemble data in the page component from your services. You can mock these services out
38 | using Storybook.
39 |
40 |
41 |
42 | Get a guided tutorial on component-driven development at{' '}
43 |
44 | Learn Storybook
45 |
46 | . Read more in the{' '}
47 |
48 | docs
49 |
50 | .
51 |
52 |
53 |
Tip Adjust the width of the canvas with the{' '}
54 |
55 |
56 |
61 |
62 |
63 | Viewports addon in the toolbar
64 |
65 |
66 |
67 | );
68 |
--------------------------------------------------------------------------------
/studio/schemas/blockContent.ts:
--------------------------------------------------------------------------------
1 | import { FaExternalLinkAlt } from 'react-icons/fa'
2 |
3 | /**
4 | * This is the schema definition for the rich text fields used for
5 | * for this blog studio. When you import it in schemas.js it can be
6 | * reused in other parts of the studio with:
7 | * {
8 | * name: 'someName',
9 | * title: 'Some title',
10 | * type: 'blockContent'
11 | * }
12 | */
13 | export default {
14 | title: 'Block Content',
15 | name: 'blockContent',
16 | type: 'array',
17 | of: [
18 | {
19 | title: 'Block',
20 | type: 'block',
21 | // Styles let you set what your user can mark up blocks with. These
22 | // corrensponds with HTML tags, but you can set any title or value
23 | // you want and decide how you want to deal with it where you want to
24 | // use your content.
25 | styles: [
26 | { title: 'Normal', value: 'normal' },
27 | { title: 'H1', value: 'h1' },
28 | { title: 'H2', value: 'h2' },
29 | { title: 'H3', value: 'h3' },
30 | { title: 'H4', value: 'h4' },
31 | { title: 'Quote', value: 'blockquote' }
32 | ],
33 | lists: [{ title: 'Bullet', value: 'bullet' }],
34 | // Marks let you mark up inline text in the block editor.
35 | marks: {
36 | // Decorators usually describe a single property – e.g. a typographic
37 | // preference or highlighting by editors.
38 | decorators: [{ title: 'Strong', value: 'strong' }, { title: 'Emphasis', value: 'em' }],
39 | // Annotations can be any object structure – e.g. a link or a footnote.
40 | annotations: [
41 | {
42 | title: 'External Link',
43 | name: 'link',
44 | type: 'object',
45 | blockEditor: {
46 | icon: FaExternalLinkAlt
47 | },
48 | fields: [
49 | {
50 | title: 'URL',
51 | name: 'href',
52 | type: 'url',
53 | validation: Rule =>
54 | Rule.uri({
55 | allowRelative: true,
56 | scheme: ['https', 'http', 'mailto', 'tel']
57 | })
58 | },
59 | {
60 | title: 'Open in new tab',
61 | name: 'blank',
62 | description: 'Read https://css-tricks.com/use-target_blank/',
63 | type: 'boolean'
64 | }
65 | ]
66 | }
67 | ]
68 | }
69 | },
70 | // You can add additional types here. Note that you can't use
71 | // primitive types such as 'string' and 'number' in the same array
72 | // as a block type.
73 |
74 | ]
75 | }
76 |
--------------------------------------------------------------------------------
/web/src/lambda/register.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_CREATE_QUERY
11 | } from './requestConfig'
12 |
13 | export const handler = async (event: APIGatewayEvent): Promise => {
14 | // TEST for POST request
15 | if (event.httpMethod !== 'POST' || !event.body) {
16 | return statusReturn(400, '')
17 | }
18 |
19 | let data
20 |
21 | try {
22 | data = JSON.parse(event.body)
23 | } catch (error) {
24 | console.log('JSON parsing error:', error);
25 | return statusReturn(400, { error: 'Bad request body' })
26 | }
27 |
28 | console.log(`[λ: new account]`, { email: data.email, password: data.password, firstName: data.firstName, lastName: data.lastName })
29 | const payload = preparePayload(CUSTOMER_CREATE_QUERY, {
30 | input: {
31 | email: data.email,
32 | password: data.password,
33 | firstName: data.firstName,
34 | lastName: data.lastName
35 | }
36 | })
37 |
38 | try {
39 | let customer = await axios({
40 | url: SHOPIFY_GRAPHQL_URL,
41 | method: 'POST',
42 | headers: shopifyConfig,
43 | data: payload
44 | })
45 |
46 | const { customerCreate } = customer.data.data
47 |
48 | if (customer.data.errors) throw customer.data.errors[0]
49 | if (customerCreate.userErrors.length > 0) throw customerCreate.userErrors[0]
50 |
51 | // If that was successful lets log our new user in
52 | const loginPayload = preparePayload(CUSTOMER_TOKEN_QUERY, {
53 | input: {
54 | email: data.email,
55 | password: data.password
56 | }
57 | })
58 |
59 |
60 | try {
61 | let token = await axios({
62 | url: SHOPIFY_GRAPHQL_URL,
63 | method: 'POST',
64 | headers: shopifyConfig,
65 | data: loginPayload
66 | })
67 | const {
68 | customerAccessTokenCreate
69 | } = token.data.data
70 | if (customerAccessTokenCreate.userErrors.length > 0) {
71 | throw customerAccessTokenCreate.userErrors
72 | } else {
73 | token = customerAccessTokenCreate.customerAccessToken.accessToken
74 | // Manipulate the response and send some customer info back down that we can use later
75 | return statusReturn(200, {
76 | token,
77 | customer: {
78 | firstName: data.firstName,
79 | lastName: data.lastName
80 | }
81 | })
82 | }
83 | } catch (err) {
84 | return statusReturn(500, { error: err[0].message })
85 | }
86 | } catch (err) {
87 | return statusReturn(500, { error: err.message })
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/web/src/layouts/password.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, FormHTMLAttributes } from 'react'
2 | import cookie from 'js-cookie'
3 | import cx from 'classnames'
4 |
5 | const passwords = [
6 | 'spaghetti'
7 | ]
8 |
9 | const browser = typeof window !== 'undefined' && window;
10 |
11 | export const PasswordWrapper = ({ children }: {
12 | children: any
13 | }) => {
14 | // Comment this out to turn the password back on!
15 | // const [password, setPassword] = useState(cookie.get('password'))
16 | const [password, setPassword] = useState('spaghetti')
17 | const [error, setError] = useState(false)
18 | const form = React.createRef() as React.RefObject
19 |
20 | const handleSubmit = (e: React.FormEvent) => {
21 | e.preventDefault()
22 | if (form.current) {
23 | const { elements } = form.current
24 | const found = passwords.filter(el => el === elements.password.value)
25 |
26 | if (found.length === 1) {
27 | cookie.set('password', 'true', { expires: 30 })
28 | location.reload()
29 | } else {
30 | setError(true)
31 | }
32 | }
33 | }
34 | if (password) {
35 | return (
36 |
37 | {children}
38 |
39 | )
40 | } else {
41 | return (
42 |
43 | {browser && (
44 |
68 | )}
69 |
70 | )
71 | }
72 | }
--------------------------------------------------------------------------------
/web/src/components/auth/orders.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useCallback, useState } from 'react'
2 | import cookie from 'js-cookie'
3 | import { useLoads } from 'react-loads'
4 | import spacetime from 'spacetime'
5 |
6 | import { useStore } from 'src/context/siteContext'
7 |
8 | export const Orders = () => {
9 | const {customerToken} = useStore()
10 | const [orders, setOrders] = useState([])
11 | const handleOrders = useCallback(
12 | (token) =>
13 | fetch(`/.netlify/functions/orders`, {
14 | method: 'POST',
15 | body: JSON.stringify({
16 | token
17 | }),
18 | })
19 | .then(res => res.json())
20 | .then(res => {
21 | if (res.error) {
22 | throw new Error(res.error.message)
23 | } else {
24 | console.log(res)
25 | if (res.customer.orders.edges) {
26 | setOrders(res.customer.orders.edges)
27 | }
28 | return null
29 | }
30 | }),
31 | []
32 | )
33 |
34 | const { error, isRejected, isPending, isReloading, load } = useLoads(
35 | 'handleOrders',
36 | handleOrders as any,
37 | {
38 | defer: true,
39 | }
40 | )
41 |
42 | useEffect(() => {
43 | const token = customerToken || cookie.get('customer_token')
44 | if (token) {
45 | load(token)
46 | }
47 | }, [])
48 |
49 | return (
50 |
51 |
Orders
52 |
53 | {(isPending ||
54 | isReloading) && (
55 |
Loading
56 | )}
57 |
58 | {isRejected && (
59 |
60 |
61 | ⚠️
62 |
63 | : {error.message}
64 |
65 | )}
66 |
67 | {orders.length > 0 ? (
68 |
69 |
70 | {orders.map(order => (
71 |
72 |
73 |
{order.node.orderNumber}
74 | Processed: {spacetime(order.node.processedAt).unixFmt('dd.MM.yyyy')}
75 |
76 |
80 |
81 | ))}
82 |
83 |
84 | ) : (
85 |
86 |
Sorry you don't have any orders yet!
87 |
88 | )}
89 |
90 |
91 | )
92 | }
--------------------------------------------------------------------------------
/web/src/components/cart/lineItem.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 |
3 | import {
4 | Close,
5 | Minus,
6 | Plus
7 | } from 'src/components/svgs'
8 |
9 | import {
10 | useUpdateItemsFromCart,
11 | useRemoveItemFromCart,
12 | client
13 | } from 'src/context/siteContext'
14 |
15 | export const LineItem = ({ id, title, quantity, variant: { price, compareAtPrice, image }, customAttributes }: {
16 | id: string
17 | title: string
18 | quantity: number
19 | variant: {
20 | price: string
21 | image: string
22 | compareAtPrice: string
23 | }
24 | customAttributes: Array<{
25 | value: string
26 | }>
27 | }) => {
28 | const updateItemsFromCart = useUpdateItemsFromCart()
29 |
30 | const [stateQuantity, setQuantity] = useState(quantity)
31 | const removeFromCart = useRemoveItemFromCart()
32 |
33 | const updateQuantity = (qty: number) => {
34 | updateItemsFromCart({id, quantity: qty})
35 | setQuantity(qty)
36 | }
37 |
38 | const itemImage = client.image.helpers.imageForSize(
39 | image,
40 | { maxWidth: 300, maxHeight: 300 }
41 | )
42 |
43 | return (
44 | <>
45 |
46 |
47 |
48 |
49 |
50 |
51 |
{title}
52 |
53 |
54 |
55 |
stateQuantity === 1 ? null : updateQuantity(stateQuantity - 1)}>
56 |
{stateQuantity}
57 |
updateQuantity(stateQuantity + 1)}>
58 |
59 |
60 |
61 |
62 |
63 | {compareAtPrice && (
64 |
65 | ${parseFloat(compareAtPrice) * stateQuantity}
66 |
67 | )}
68 |
69 | ${parseFloat(price) * stateQuantity}
70 |
71 |
72 |
73 |
74 |
75 |
removeFromCart(id)}>
76 |
77 |
78 |
79 |
80 | >
81 | )
82 | }
--------------------------------------------------------------------------------
/web/src/components/auth/forgotPassword.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback } from "react"
2 | import Helmet from "react-helmet"
3 | import fetch from "unfetch"
4 | import { Link } from "gatsby"
5 | import { useLoads } from 'react-loads'
6 |
7 | import { ErrorHandling } from 'src/utils/error'
8 |
9 | export const ForgotPassword = ({ path }: { path: string }) => {
10 | const [formSuccess, setFormSuccess] = useState(false)
11 | const form = React.createRef() as React.RefObject
12 |
13 | const handleForgot = useCallback(
14 | (email) =>
15 | fetch(`/.netlify/functions/forgot-password`, {
16 | method: "POST",
17 | body: JSON.stringify({
18 | email
19 | }),
20 | })
21 | .then(res => res.json())
22 | .then(res => {
23 | if (res.error) {
24 | throw new Error(res.error)
25 | } else {
26 | setFormSuccess(true)
27 | }
28 | }),
29 | []
30 | )
31 |
32 | const { error, isRejected, isPending, isReloading, load } = useLoads(
33 | "handleForgot",
34 | handleForgot as any,
35 | {
36 | defer: true,
37 | }
38 | )
39 |
40 | const handleSubmit = (e: React.FormEvent) => {
41 | e.preventDefault()
42 | if (form) {
43 | const { email } = form!.current!.elements
44 | load(email.value)
45 | }
46 | }
47 |
48 | return (
49 |
93 | )
94 | }
95 |
--------------------------------------------------------------------------------
/web/src/components/SEO.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import Helmet from "react-helmet"
3 |
4 | const siteRoute = "https://midway-starter.netlify.com"
5 |
6 | interface MetaInformation {
7 | metaTitle?: string
8 | metaDescription?: string
9 | openImage?: string
10 | twitterImage?: string
11 | twitterTitle?: string
12 | openTitle?: string
13 | openGraphDescription?: string
14 | twitterDescription?: string
15 | }
16 |
17 | export const SEO = ({
18 | defaultMeta,
19 | defaultTitle,
20 | pagePath,
21 | metaInfo,
22 | }: {
23 | pagePath: string
24 | metaInfo: MetaInformation,
25 | defaultMeta: MetaInformation,
26 | defaultTitle: string
27 | }) => {
28 | const title = metaInfo
29 | ? metaInfo.metaTitle
30 | ? metaInfo.metaTitle
31 | : defaultTitle
32 | : defaultTitle ? defaultTitle : defaultMeta && defaultMeta.metaTitle
33 | const metaDescription = metaInfo
34 | ? metaInfo.metaDescription
35 | ? metaInfo.metaDescription
36 | : defaultMeta.metaDescription
37 | : defaultMeta.metaDescription
38 | const metaKeywords = defaultMeta.metaKeywords
39 | const ogImage = metaInfo
40 | ? metaInfo.openImage
41 | ? metaInfo.openImage
42 | : defaultMeta.openImage
43 | : defaultMeta.openImage
44 | const twitterImage = metaInfo
45 | ? metaInfo.twitterImage
46 | ? metaInfo.twitterImage
47 | : defaultMeta.twitterImage
48 | : defaultMeta.twitterImage
49 | const openTitle = metaInfo
50 | ? metaInfo.openTitle
51 | ? metaInfo.openTitle
52 | : title
53 | : title
54 | const openGraphDescription = metaInfo
55 | ? metaInfo.openGraphDescription
56 | ? metaInfo.openGraphDescription
57 | : metaDescription
58 | : metaDescription
59 | const twitterTitle = metaInfo
60 | ? metaInfo.twitterTitle
61 | ? metaInfo.twitterTitle
62 | : title
63 | : title
64 | const twitterDescription = metaInfo
65 | ? metaInfo.twitterDescription
66 | ? metaInfo.twitterDescription
67 | : metaDescription
68 | : metaDescription
69 | return (
70 |
71 |
72 |
73 |
74 |
75 |
76 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
93 |
94 | )
95 | }
96 |
--------------------------------------------------------------------------------
/web/gatsby-config.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config()
2 | const path = require('path')
3 | const proxy = require('http-proxy-middleware')
4 |
5 | module.exports = {
6 | // Handles local dev for the netlify functions
7 | developMiddleware: app => {
8 | app.use(
9 | '/.netlify/functions/',
10 | proxy({
11 | target: 'http://localhost:34567',
12 | pathRewrite: {
13 | '/.netlify/functions/': ''
14 | }
15 | })
16 | )
17 | },
18 | siteMetadata: {
19 | title: `Midway`,
20 | description: `Gatsby + Sanity + Shopify Repo`,
21 | siteUrl: `https://midway.ctrlaltdel.world`,
22 | author: `iamkevingreen`,
23 | password: true
24 | },
25 | plugins: [
26 | `gatsby-plugin-react-helmet`,
27 | {
28 | resolve: `gatsby-source-filesystem`,
29 | options: {
30 | name: `images`,
31 | path: `${__dirname}/src/images`,
32 | },
33 | },
34 | `gatsby-transformer-sharp`,
35 | `gatsby-plugin-sharp`,
36 | `gatsby-plugin-sass`,
37 | {
38 | resolve: `gatsby-plugin-robots-txt`,
39 | options: {
40 | host: 'https://midway.ctrlaltdel.world',
41 | sitemap: 'https://midway.ctrlaltdel.world/sitemap.xml',
42 | policy: [{ userAgent: '*', allow: '/', disallow: ['/checkout', '/account', '/docs', '/previews'] }]
43 | }
44 | },
45 | {
46 | resolve: 'gatsby-plugin-loadable-components-ssr',
47 | options: {
48 | useHydrate: true
49 | }
50 | },
51 | {
52 | resolve: 'gatsby-plugin-netlify',
53 | options: {
54 | mergeSecurityHeaders: false,
55 | headers: {
56 | '/preview': [
57 | // 'X-Frame-Options: DENY', allow iframe usage
58 | 'X-XSS-Protection: 1; mode=block',
59 | 'X-Content-Type-Options: nosniff',
60 | 'Referrer-Policy: same-origin',
61 | ],
62 | }
63 | }
64 | },
65 | {
66 | resolve: `gatsby-plugin-manifest`,
67 | options: {
68 | name: `midway`,
69 | short_name: `midway`,
70 | start_url: `/`,
71 | background_color: `#663399`,
72 | theme_color: `#663399`,
73 | display: `minimal-ui`,
74 | icon: `src/images/favicon/apple-icon.png`, // This path is relative to the root of the site.
75 | },
76 | },
77 | {
78 | resolve: `gatsby-plugin-layout`,
79 | options: {
80 | layout: require.resolve(`./src/layouts/index.tsx`)
81 | }
82 | },
83 | {
84 | resolve: `gatsby-plugin-create-client-paths`,
85 | options: { prefixes: [`/account/*`, `/docs/*`, `/previews/*`] },
86 | },
87 | {
88 | resolve: 'gatsby-plugin-root-import',
89 | options: {
90 | src: path.join(__dirname, 'src'),
91 | pages: path.join(__dirname, 'src/pages'),
92 | context: path.join(__dirname, 'src/context'),
93 | static: path.join(__dirname, 'static'),
94 | pages: path.join(__dirname, 'src/pages'),
95 | utils: path.join(__dirname, 'src/utils')
96 | }
97 | },
98 | `gatsby-plugin-typescript`,
99 | `gatsby-plugin-tslint`,
100 |
101 | // this (optional) plugin enables Progressive Web App + Offline functionality
102 | // To learn more, visit: https://gatsby.dev/offline
103 | // `gatsby-plugin-offline`,
104 | ],
105 | }
106 |
--------------------------------------------------------------------------------
/studio/schemas/schemas.ts:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | // First, we must import the schema creator
3 | import createSchema from 'part:@sanity/base/schema-creator'
4 |
5 | // Then import schema types from any plugins that might expose them
6 | import schemaTypes from 'all:part:@sanity/base/schema-type'
7 |
8 | // We import object and document schemas
9 | import blockContent from './blockContent'
10 | import blockText from './blockText'
11 | import siteSettings from './siteSettings'
12 |
13 | // Content Types
14 | import product from './types/product'
15 | import page from './types/page'
16 | import collection from './types/collection'
17 | import siteGlobal from './types/siteGlobal'
18 | import menus from './types/menus'
19 | import redirect from './types/redirect'
20 | import post from './types/post'
21 | import variant from './types/variant'
22 | import doc from './types/doc'
23 | import subscription from './types/subscription'
24 |
25 | // Modules
26 | import externalLink from './modules/externalLink'
27 | import internalLink from './modules/internalLink'
28 | import metaCard from './modules/metaCard'
29 | import social from './modules/social'
30 | import socialLink from './modules/socialLink'
31 | import nestedPages from './modules/nestedPages'
32 | import pageItem from './modules/pageItem'
33 | import pageModule from './modules/pageModule'
34 | import imageModule from './modules/imageModule'
35 | import productGrid from './modules/productGrid'
36 | import standardText from './modules/standardText'
37 | import moduleContent from './modules/moduleContent'
38 |
39 | // Product Modules
40 | import productModule from './modules/productModule'
41 | import shopifyProductModule from './modules/shopifyProductModule'
42 | import shopifyVariantModule from './modules/shopifyVariantModule'
43 | import variantModule from './modules/variantModule'
44 | import defaultVariant from './modules/defaultVariant'
45 |
46 |
47 | // GraphQL Tab Modules
48 | import globalContent from './tabs/globalContent'
49 | import pageContent from './tabs/pageContent'
50 | import variantContent from './tabs/variantContent'
51 | import productContent from './tabs/productContent'
52 |
53 |
54 | // Then we give our schema to the builder and provide the result to Sanity
55 | export default createSchema({
56 | // We name our schema
57 | name: 'default',
58 | // Then proceed to concatenate our our document type
59 | // to the ones provided by any plugins that are installed
60 | types: schemaTypes.concat([
61 | // The following are document types which will appear
62 | // in the studio.
63 | siteSettings,
64 | siteGlobal,
65 | page,
66 | post,
67 | doc,
68 | redirect,
69 | menus,
70 | collection,
71 | product,
72 | variant,
73 | subscription, // This can be disabled/hidden if not using recharge
74 | // Modules
75 | externalLink,
76 | internalLink,
77 | productGrid,
78 | pageModule,
79 | nestedPages,
80 | pageItem,
81 | social,
82 | socialLink,
83 | standardText,
84 | imageModule,
85 | moduleContent,
86 | metaCard,
87 | blockContent,
88 | blockText,
89 | // Product Specific Modules
90 | productModule,
91 | shopifyProductModule,
92 | shopifyVariantModule,
93 | variantModule,
94 | defaultVariant,
95 | // GrapqhQL Tab Things
96 | globalContent,
97 | pageContent,
98 | productContent,
99 | variantContent
100 |
101 | // When added to this list, object types can be used as
102 | // { type: 'typename' } in other document schemas
103 | ])
104 | })
105 |
--------------------------------------------------------------------------------
/web/src/styles/lib/_config.scss:
--------------------------------------------------------------------------------
1 | //
2 | // === Colors ===
3 | //
4 |
5 | $all-colors: (
6 | white : #FFFFFF,
7 | off-white : #FBFBFB,
8 | black : #000000,
9 | gray-border : #BFBFBF,
10 | blue : #041893
11 | );
12 |
13 | //
14 | // === Directions ===
15 | //
16 | $sides: (
17 | t : top,
18 | b : bottom,
19 | l : left,
20 | r : right,
21 | j : justify,
22 | );
23 | $alignments: (
24 | l : left,
25 | r : right,
26 | c : center,
27 | );
28 |
29 |
30 | //
31 | // === Breakpoints ===
32 | //
33 |
34 | $breakpoints : (
35 | 400 : 400px,
36 | 600 : 600px,
37 | md : 800px,
38 | 800 : 800px,
39 | 1000 : 1000px,
40 | 1200 : 1200px,
41 | 1400 : 1400px,
42 | );
43 |
44 | @mixin breakpoint($size) {
45 | $breakpoint-found: map-has-key($breakpoints, $size);
46 | @if ($breakpoint-found == true) {
47 | $breakpoint: map-get($breakpoints, $size);
48 | @media (min-width: #{$breakpoint}) {
49 | @content;
50 | }
51 | } @else {
52 | @warn "Breakpoint size " + #{$size} + " doesn't exist.";
53 | }
54 | }
55 |
56 | //
57 | // === Components ===
58 | //
59 | // Spacer
60 | $spacer-max: 10;
61 | $spacer-size: 10px;
62 | // Inline Grid
63 | $prime-grid-column-size: 12;
64 | $inline-grid-column-size: 12;
65 | $inline-grid-breakpoint: md;
66 | $inline-grid-gutter-mobile: 10px;
67 | $inline-grid-gutter-desktop: 10px;
68 | $inline-grid-gutter-extra: (
69 | none : 0px,
70 | );
71 | // Grid Container
72 | $grid-padding-mobile: 10px;
73 | $grid-padding-desktop: 20px;
74 | $grid-max-width: 1920px;
75 | // CSS Grid
76 | $grid-column-sizes: (
77 | xs: 12,
78 | lg: 12,
79 | );
80 | $grid-gutter-sizes: (
81 | xs: 10px,
82 | lg: 10px,
83 | );
84 | $grid-column-max: max(map-values($grid-column-sizes)...);
85 | $grid-gutter-extra: (
86 | none : 0px,
87 | );
88 | @function gutter($gutter-name) {
89 | $gutter-found: map-has-key($inline-grid-gutter-extra, $gutter-name);
90 | @if ($gutter-found == true) {
91 | @return map-get($inline-grid-gutter-extra, $gutter-name);
92 | } @else {
93 | @warn "Gutter " + #{$gutter-name} + " doesn't exist.";
94 | }
95 | }
96 |
97 | // //
98 | // // === Spacer ===
99 | // //
100 |
101 | // $spacer-max: 12;
102 | $spacer-size: 10px;
103 |
104 | //
105 | // === Typography ===
106 | //
107 |
108 | $font-size-mobile: 14px;
109 | $line-height-mobile: 22px;
110 |
111 | $font-size-desktop: 14px;
112 | $line-height-desktop: 22px;
113 |
114 | $font-family-montserrat: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, sans-serif;
115 | $font-family-domaine: 'Domaine Display', 'Bodoni', Bodoni, serif;
116 |
117 | $body-letter-spacing: 0;
118 |
119 | //
120 | // === Constants ===
121 | //
122 |
123 | $cubic-bezier: cubic-bezier(.12,.67,.53,1);
124 |
125 | $header-height: 80px;
126 |
127 | /*
128 | // === Config Getter Functions ===
129 | */
130 |
131 | @function color($color-name) {
132 | $color-found: map-has-key($all-colors, $color-name);
133 | @if ($color-found == true) {
134 | @return map-get($all-colors, $color-name);
135 | } @else {
136 | @warn "Color " + #{$color-name} + " doesn't exist.";
137 | }
138 | }
139 |
140 | @mixin breakpoint($size) {
141 | $breakpoint-found: map-has-key($breakpoints, $size);
142 | @if ($breakpoint-found == true) {
143 | $breakpoint: map-get($breakpoints, $size);
144 | @media (min-width: #{$breakpoint}) {
145 | @content;
146 | }
147 | } @else {
148 | @warn "Breakpoint size " + #{$size} + " doesn't exist."
149 | }
150 | }
151 |
152 | @function gutter($gutter-name) {
153 | $gutter-found: map-has-key($grid-gutter-extra, $gutter-name);
154 | @if ($gutter-found == true) {
155 | @return map-get($grid-gutter-extra, $gutter-name);
156 | } @else {
157 | @warn "Gutter " + #{$gutter-name} + " doesn't exist.";
158 | }
159 | }
160 |
161 |
--------------------------------------------------------------------------------
/web/src/layouts/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import Helmet from 'react-helmet'
3 | import tighpo from 'tighpo'
4 |
5 | import { Header } from 'src/components/header'
6 | import { Footer } from 'src/components/footer'
7 | import { SwitchTransition, Transition } from 'react-transition-group'
8 | import { CartDrawer } from 'src/components/cartDrawer'
9 | import { PasswordWrapper } from './password'
10 |
11 | import Analytics from 'src/components/analytics'
12 |
13 |
14 | const TRANSITION_DURATION = 400;
15 | const TRANSITION_STYLES = {
16 | default: {
17 | transition: `opacity ${TRANSITION_DURATION}ms ease-in-out`,
18 | },
19 | entering: {
20 | opacity: 0,
21 | },
22 | entered: {
23 | opacity: 1,
24 | },
25 | exiting: {
26 | opacity: 0,
27 | },
28 | exited: {
29 | opacity: 0,
30 | },
31 | };
32 |
33 | import 'src/styles/main.scss'
34 |
35 | const Layout = ({ children, location, pageContext }: { children: any, location: { pathname: string }, pageContext: { site?: {}, layout: string }}) => {
36 |
37 | const { site } = pageContext
38 |
39 | // Render documentation for CMS minus header/footer experience
40 | if (pageContext.layout === 'docs') {
41 | return (
42 |
43 | {children}
44 |
45 | )
46 | }
47 |
48 | if (pageContext.layout === 'accounts') {
49 | return (
50 |
51 |
52 |
53 | {children}
54 |
55 |
56 | )
57 | }
58 |
59 | useEffect(() => {
60 | tighpo('spaghetti', function () {
61 | const style = document.createElement('style')
62 | document.body.appendChild(style)
63 | style.sheet.insertRule('html, body { cursor: url(https://spaghet.now.sh), auto !important; }')
64 | })
65 | }, [0])
66 |
67 | return (
68 |
69 |
70 |
71 |
72 |
74 |
75 |
76 |
81 | Skip to main content
82 |
83 |
84 |
85 | {/*
86 |
87 | Smooth transition credits to Ian Williams: https://github.com/dictions
88 |
89 | */}
90 | {!/account/.test(location.pathname) ? (
91 |
92 |
98 | {status => (
99 |
106 | {children}
107 |
108 |
109 | )}
110 |
111 |
112 | ) : (
113 |
114 | {children}
115 |
116 |
117 | )}
118 |
119 |
120 |
121 | )
122 | }
123 |
124 | export default Layout
--------------------------------------------------------------------------------
/web/src/components/cartDrawer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Link } from 'gatsby'
3 | import cx from 'classnames'
4 |
5 | import { Cart } from 'src/components/cart'
6 | import { useStore, useToggleCart, useCartTotals, useCartItems, useCheckout } from 'src/context/siteContext'
7 |
8 | import {
9 | Close
10 | } from 'src/components/svgs'
11 |
12 | /*
13 | / Cart Drawer: This module is a bit more robust then other parts of the theme,
14 | / this is because carts are relatively difficult to execute, scale to mobile etc,
15 | / feel free to keep more of this styling if you use this component, the structure
16 | / a bit confusing to make sure all elements are visible/scrollable on mobile
17 | */
18 |
19 | export const CartDrawer = () => {
20 | const lineItems = useCartItems()
21 | const { cartIsOpen } = useStore()
22 | const { total } = useCartTotals()
23 | const toggleCart = useToggleCart()
24 | const openCheckout = useCheckout()
25 | const trap = cartIsOpen ? (
26 |
27 |
28 |
29 | toggleCart()}>
30 |
31 |
32 |
Cart
33 |
34 |
35 |
36 |
37 |
38 |
39 | {lineItems.length > 0 && (
40 | <>
41 | {parseInt(total, 0) <= 40 ? (
42 |
You're ${(40 - (parseFloat(total))).toFixed(2)} away from free shipping!
43 | ) : (
44 |
Your order qualifies for Free Shipping!
45 | )}
46 | >
47 | )}
48 |
49 |
50 |
51 |
52 | = 40 && 'free', lineItems.length >= 1 && 'visible')}>
53 |
54 |
55 | {lineItems.length > 0 && (
56 |
57 |
58 | Subtotal
59 | ${total}
60 |
61 |
62 | )}
63 |
64 | {lineItems.length < 1 ? (
65 |
toggleCart()} to='/'>Continue Shopping
66 | ): (
67 |
68 | Checkout
69 |
70 | )}
71 |
72 |
73 |
74 | ) : (
75 | false
76 | )
77 | return (
78 | <>
79 |
83 | {trap}
84 |
85 | {/* Handles the overlay to click-close the cart */}
86 | toggleCart()} className={cx('cart__drawer-bg bg--white z9 left top x y pf', cartIsOpen && 'is-open')} />
87 | >
88 | )
89 | }
--------------------------------------------------------------------------------
/web/src/build/createPages.js:
--------------------------------------------------------------------------------
1 | const queries = require('../api/queries');
2 | const sanity = require('../api/sanity');
3 |
4 | //
5 | // === Get Data ===
6 | //
7 |
8 | module.exports.getAllPageData = () => {
9 | // Fetch all data needs
10 | const productsQuery = sanity.fetch(queries.products)
11 | const pagesQuery = sanity.fetch(queries.pages);
12 | const collectionQuery = sanity.fetch(queries.collections);
13 | const globalQuery = sanity.fetch(queries.global);
14 |
15 | // Wait for all data needs
16 | return Promise.all([
17 | productsQuery,
18 | pagesQuery,
19 | collectionQuery,
20 | globalQuery
21 | ]);
22 | };
23 |
24 | //
25 | // === Create All Pages ===
26 | //
27 |
28 | module.exports.createAllPages = (
29 | promiseResults,
30 | actions,
31 | resolve,
32 | reject
33 | ) => {
34 | const [
35 | products,
36 | pages,
37 | collections,
38 | global,
39 | ] = promiseResults;
40 |
41 | //
42 | // === Create Contexts ===
43 | //
44 | const sharedContext = {
45 | // menus, Addd this pattern
46 | // siteGlobal
47 | site: global
48 | };
49 |
50 | //
51 | // === Create pages ===
52 | //
53 |
54 | try {
55 |
56 | // Collections
57 | collections && collections.forEach(collection => {
58 | actions.createPage({
59 | path: `/collections/${collection.slug}`,
60 | component: require.resolve('../templates/collection.tsx'),
61 | context: {
62 | ...sharedContext,
63 | ...collection
64 | }
65 | })
66 | })
67 |
68 |
69 |
70 | // Pages
71 | pages && pages.forEach(page => {
72 | actions.createPage({
73 | path: `${page.slug === 'home' ? '/' : `/${page.slug}` }`,
74 | component: require.resolve('../templates/page.tsx'),
75 | context: {
76 | ...sharedContext,
77 | ...page,
78 | }
79 | });
80 | });
81 |
82 | // Products
83 | products && products.forEach(product => {
84 | actions.createPage({
85 | path: `/products/${product.slug}`,
86 | component: require.resolve('../templates/product.tsx'),
87 | context: {
88 | ...sharedContext,
89 | ...product,
90 | }
91 | });
92 | });
93 |
94 | // Accounts
95 | actions.createPage({
96 | path: '/account',
97 | matchPath: '/account',
98 | component: require.resolve('../templates/account.tsx'),
99 | context: {
100 | layout: 'account',
101 | ...sharedContext
102 | },
103 | })
104 |
105 | actions.createPage({
106 | path: '/account',
107 | matchPath: '/account/*',
108 | component: require.resolve('../templates/account.tsx'),
109 | context: {
110 | layout: 'account',
111 | ...sharedContext
112 | },
113 | })
114 |
115 | actions.createRedirect({
116 | fromPath: '/account*',
117 | toPath: '/account',
118 | statusCode: 200,
119 | })
120 |
121 |
122 | actions.createPage({
123 | path: '/404/',
124 | component: require.resolve('../templates/404.tsx'),
125 | context: {
126 | ...sharedContext
127 | },
128 | })
129 |
130 | //
131 | // === Redirects ===
132 | //
133 | // redirects.forEach(redirect => {
134 | // actions.createRedirect({
135 | // fromPath: redirect.fromPath,
136 | // toPath: redirect.toPath,
137 | // isPermanent: redirect.statusCode === 301, // use as fallback. this is part of Gatsby's API
138 | // statusCode: redirect.statusCode || 302 // Netlify specific. Will override `isPermanent`
139 | // });
140 | // });
141 | actions.createRedirect({
142 | fromPath: '/:accountId/orders/:orderId/authenticate key=:key',
143 | toPath:
144 | 'https://shop.allkinds.com/:accountId/orders/:orderId/authenticate?key=:key',
145 | isPermanent: true,
146 | statusCode: 301,
147 | })
148 | } catch(error) {
149 | reject(error);
150 | return;
151 | }
152 |
153 | resolve();
154 | };
--------------------------------------------------------------------------------
/web/src/components/auth/login.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect } from 'react'
2 | import fetch from 'unfetch'
3 | import { Link, navigate } from 'gatsby'
4 | import Helmet from 'react-helmet'
5 | import { useLoads } from 'react-loads'
6 | import cx from 'classnames'
7 |
8 |
9 | import { ErrorHandling } from 'src/utils/error'
10 |
11 | import { UpdateCustomer } from 'src/utils/updateCustomer'
12 | import { setCustomerInState } from 'src/context/siteContext'
13 |
14 | export const Login = ({ path }: { path: string }) => {
15 | const updateCustomerInState = setCustomerInState()
16 | const form = React.createRef() as React.RefObject
17 |
18 | const handleLogin = useCallback(
19 | (email, password) =>
20 | fetch(`/.netlify/functions/login`, {
21 | method: 'POST',
22 | body: JSON.stringify({
23 | email,
24 | password,
25 | }),
26 | })
27 | .then(res => res.json())
28 | .then(res => {
29 | if (res.error) {
30 | throw new Error(res.error)
31 | } else {
32 | UpdateCustomer(res, email)
33 | setTimeout(() => {
34 | updateCustomerInState()
35 | navigate('/account')
36 | }, 400)
37 | return null
38 | }
39 | }),
40 | []
41 | )
42 |
43 | const { error, isRejected, isPending, isReloading, load } = useLoads(
44 | 'handleLogin',
45 | handleLogin as any,
46 | {
47 | defer: true,
48 | }
49 | )
50 |
51 | const handleSubmit = (e: React.FormEvent) => {
52 | e.preventDefault()
53 |
54 | const { email, password } = form.current.elements
55 |
56 | load(email.value, password.value)
57 | }
58 |
59 | return (
60 |
118 | )
119 | }
120 |
--------------------------------------------------------------------------------
/web/src/components/analytics.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Helmet from 'react-helmet'
3 |
4 | //
5 | // === Initialize global Analytics ===
6 | //
7 |
8 | export const Initialize = ({
9 | facebookPixelId,
10 | googleAnalyticsPropertyId,
11 | googleLinkerDomains,
12 | }: {
13 | facebookPixelId?: string
14 | googleAnalyticsPropertyId?: string
15 | googleLinkerDomains?: any[]
16 | }) => {
17 | // Initialize Gtag
18 | const dataLayer = global.dataLayer || [];
19 | global.gtag && global.gtag('js', new Date());
20 |
21 | return {
22 | pageview(location) {
23 | global.gtag && global.gtag('config', googleAnalyticsPropertyId, {
24 | page_path: location.pathname,
25 | linker: {
26 | domains: googleLinkerDomains,
27 | }
28 | });
29 | global.fbq && global.fbq('track', 'PageView');
30 | },
31 | viewProduct(item = {}) {
32 | global.gtag && global.gtag('event', 'view_item', { items: [item] });
33 |
34 | global.fbq && global.fbq('track', 'ViewContent', {
35 | content_name: item.name,
36 | content_ids: [item.id],
37 | content_type: 'product',
38 | value: item.price,
39 | currency: 'USD'
40 | });
41 | },
42 | addToCart(item = {}) {
43 | global.gtag && global.gtag('event', 'add_to_cart', { items: [item] });
44 |
45 | global.fbq && global.fbq('track', 'AddToCart', {
46 | content_name: item.name,
47 | content_ids: [item.id],
48 | content_type: 'product',
49 | value: item.price,
50 | currency: 'USD'
51 | });
52 | },
53 | removeFromCart(item = {}) {
54 | global.gtag && global.gtag('event', 'remove_from_cart', { items: [item] });
55 | },
56 | clickCheckout() {
57 | global.gtag && global.gtag('event', 'Click Checkout',);
58 | global.fbq && global.fbq('track', 'InitiateCheckout');
59 | }
60 | };
61 | };
62 |
63 | //
64 | // === Group all script tags here` ===
65 | //
66 |
67 | export default ({
68 | googleAnalyticsPropertyId,
69 | gtmPropertyId,
70 | facebookPixelId
71 | }: {
72 | gtmPropertyId?: string
73 | googleAnalyticsPropertyId?: string
74 | facebookPixelId?: string
75 | }) => (
76 |
77 | {/* GA */}
78 | {googleAnalyticsPropertyId && (
79 | )}
83 | {googleAnalyticsPropertyId && ()}
88 | {/* GTM */}
89 | {gtmPropertyId && (
90 | )}
97 | {gtmPropertyId && (
98 | {`
99 |
105 | `} )}
106 | {/* Facebook */}
107 | {facebookPixelId && (
108 | )}
119 | {facebookPixelId &&(
120 | {`
121 |
127 | `} )}
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 |
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 |
--------------------------------------------------------------------------------