├── .eslintrc.js
├── .github
├── ISSUE_TEMPLATE
│ ├── 1.Bug_Report.md
│ └── 2.Feature_Request.md
├── release-draft-template.yml
└── workflows
│ ├── publish.yml
│ ├── release-drafter.yml
│ └── test.yml
├── .gitignore
├── .npmignore
├── .nvmrc
├── .prettierrc
├── .storybook
├── config.js
├── createMetadata.js
├── main.js
└── stories
│ ├── Accordion.stories.js
│ ├── ActionButton.stories.js
│ ├── AutoScrollToNewChildren.stories.js
│ ├── BackToTop.stories.js
│ ├── Breadcrumbs.stories.js
│ ├── Carousel.stories.js
│ ├── CartButton.stories.js
│ ├── CheckboxFilterGroup.stories.js
│ ├── Drawer.stories.js
│ ├── ExpandableSection.stories.js
│ ├── Image.stories.js
│ ├── Lazy.stories.js
│ ├── LoadMask.stories.js
│ ├── MediaCarousel.stories.js
│ ├── Menu.stories.js
│ ├── MenuButton.stories.js
│ ├── NavTabs.stories.js
│ ├── Offline.stories.js
│ ├── ProductOptionSelector.stories.js
│ ├── QuantitySelector.stories.js
│ ├── Rating.stories.js
│ ├── ResponsiveTiles.stories.js
│ ├── TabPanel.stories.js
│ └── ToolbarButton.stories.js
├── .swcrc
├── LICENSE.md
├── README.md
├── RSF BRANDGUIDE JPG.pdf
├── RSF Logo trans.png
├── RSF power.png
├── babel.config.js
├── docs
├── buildDocs.js
└── moduleParser.js
├── jest.config.js
├── package-lock.json
├── package.json
├── src
├── Accordion.js
├── ActionButton.js
├── AppBar.js
├── AutoScrollToNewChildren.js
├── BackToTop.js
├── Box.js
├── Breadcrumbs.js
├── CartButton.js
├── CmsSlot.js
├── ErrorBoundary.js
├── ExpandableSection.js
├── Fill.js
├── ForwardThumbnail.js
├── Highlight.js
├── Image.js
├── Label.js
├── Lazy.js
├── LazyHydrate.js
├── LoadMask.js
├── NextScript.js
├── NoScript.js
├── Offline.js
├── PWA.js
├── PWAContext.js
├── QuantitySelector.js
├── Rating.js
├── ResponsiveTiles.js
├── Row.js
├── Spacer.js
├── TabPanel.js
├── ToolbarButton.js
├── api
│ ├── addVersion.js
│ └── getAPIURL.js
├── carousel
│ ├── Carousel.js
│ ├── CarouselArrows.js
│ ├── CarouselDots.js
│ ├── CarouselThumbnails.js
│ ├── Lightbox.js
│ ├── MagnifyHint.js
│ ├── Media.js
│ └── MediaCarousel.js
├── drawer
│ ├── Drawer.js
│ └── DrawerCloseButton.js
├── fetch.js
├── hooks
│ ├── useAppStore.js
│ ├── useCartTotal.js
│ ├── useIntersectionObserver.js
│ ├── useJssStyles.js
│ ├── useLazyState.js
│ ├── useLocalStorage.js
│ ├── useNavigationEvent.js
│ ├── useStateFromProp.js
│ └── useTraceUpdate.js
├── link
│ ├── Link.js
│ ├── LinkContext.js
│ └── LinkContextProvider.js
├── menu
│ ├── Menu.js
│ ├── MenuBack.js
│ ├── MenuBody.js
│ ├── MenuBranch.js
│ ├── MenuButton.js
│ ├── MenuCard.js
│ ├── MenuContext.js
│ ├── MenuExpanderIcon.js
│ ├── MenuFooter.js
│ ├── MenuHeader.js
│ ├── MenuIcon.js
│ ├── MenuItem.js
│ ├── MenuItemContent.js
│ ├── MenuLeaf.js
│ └── SEOLinks.js
├── middlewares
│ └── withAmpFormParser.js
├── mock-connector
│ ├── .gitignore
│ ├── account.js
│ ├── addToCart.js
│ ├── cart.js
│ ├── home.js
│ ├── index.js
│ ├── product.js
│ ├── productMedia.js
│ ├── productSuggestions.js
│ ├── removeCartItem.js
│ ├── search.js
│ ├── searchSuggestions.js
│ ├── session.js
│ ├── subcategory.js
│ ├── updateCartItem.js
│ └── utils
│ │ ├── cartStore.js
│ │ ├── colors.js
│ │ ├── createAppData.js
│ │ ├── createFacets.js
│ │ ├── createMedia.js
│ │ ├── createMenu.js
│ │ ├── createProduct.js
│ │ ├── createProducts.js
│ │ ├── createSortOptions.js
│ │ └── createTabs.js
├── nav
│ ├── NavTab.js
│ └── NavTabs.js
├── option
│ ├── ProductOption.js
│ ├── ProductOptionSelector.js
│ ├── SwatchProductOption.js
│ └── TextProductOption.js
├── plp
│ ├── ButtonFilterGroup.js
│ ├── CheckboxFilterGroup.js
│ ├── FacetGroup.js
│ ├── Filter.js
│ ├── FilterButton.js
│ ├── FilterFooter.js
│ ├── FilterHeader.js
│ ├── SearchResultsContext.js
│ ├── SearchResultsProvider.js
│ ├── ShowMore.js
│ ├── Sort.js
│ ├── SortButton.js
│ └── useSearchResultsStore.js
├── plugins
│ ├── withReactStorefront.js
│ └── withServiceWorker.js
├── profile.js
├── props
│ ├── createLazyProps.js
│ ├── fetchFromAPI.js
│ ├── fetchServerSideProps.js
│ └── fulfillAPIRequest.js
├── router
│ ├── replaceState.js
│ ├── storeInitialPropsInHistory.js
│ └── useSimpleNavigation.js
├── search
│ ├── SearchButton.js
│ ├── SearchContext.js
│ ├── SearchDrawer.js
│ ├── SearchField.js
│ ├── SearchForm.js
│ ├── SearchHeader.js
│ ├── SearchPopover.js
│ ├── SearchProvider.js
│ ├── SearchSubmitButton.js
│ ├── SearchSuggestionGroup.js
│ ├── SearchSuggestionItem.js
│ └── SearchSuggestions.js
├── server
│ ├── getRoutes.js
│ └── listRoutes.js
├── serviceWorker.js
├── session
│ ├── SessionContext.js
│ └── SessionProvider.js
├── sw
│ └── configureServiceWorker.js
├── theme
│ └── createTheme.js
└── utils
│ ├── fetchLatest.js
│ ├── format.js
│ ├── getBase64ForImage.js
│ ├── isBrowser.js
│ ├── isSameOrigin.js
│ ├── lazyLoadImages.js
│ ├── merge.js
│ ├── minifyStyles.js
│ ├── mod.js
│ ├── useDebounce.js
│ ├── withCaching.js
│ └── withDefaultHandler.js
└── test
├── Accordion.test.js
├── ActionButton.test.js
├── AppBar.test.js
├── AutoScrollToNewChildren.test.js
├── BackToTop.test.js
├── Box.test.js
├── BreadCrumbs.test.js
├── CartButton.test.js
├── CmsSlot.test.js
├── ErrorBoundary.test.js
├── ExpandableSection.test.js
├── Fill.test.js
├── ForwardThumbnail.test.js
├── Highlight.test.js
├── Image.test.js
├── Label.test.js
├── Lazy.test.js
├── LazyHydrate.test.js
├── LoadMask.test.js
├── NextScript.test.js
├── NoScript.test.js
├── Offline.test.js
├── PWA.test.js
├── QuantitySelector.test.js
├── Rating.test.js
├── ReponsiveTiles.test.js
├── Row.test.js
├── Spacer.test.js
├── TabPanel.test.js
├── ToolbarButton.test.js
├── api
├── addVersion.test.js
└── getAPIURL.test.js
├── carousel
├── Carousel.test.js
├── CarouselArrows.test.js
├── CarouselDots.test.js
├── CarouselThumbnails.test.js
├── Lightbox.test.js
├── MagnifyHint.test.js
├── Media.test.js
└── MediaCarousel.test.js
├── config
├── jsdom.js
├── mocks.js
├── mountWrapper.js
├── setup.js
├── sleep.js
└── suppressActWarnings.js
├── drawer
├── Drawer.test.js
└── DrawerCloseButton.test.js
├── fetch.test.js
├── hooks
├── useAppStore.test.js
├── useCartTotal.test.js
├── useIntersectionObserver.test.js
├── useJssStyles.test.js
├── useLazyState.test.js
├── useLocalStorage.test.js
├── useNavigationEvent.test.js
├── useStateFromProp.test.js
└── useTraceUpdate.test.js
├── link
├── Link.test.js
└── LinkContextProvider.test.js
├── menu
├── Menu.test.js
├── MenuButton.test.js
├── MenuExpanderIcon.test.js
├── MenuIcon.test.js
├── MenuItem.test.js
├── MenuItemContent.test.js
└── SEOLinks.test.js
├── methods
└── index.js
├── middlewares
└── withAmpFormParser.test.js
├── mocks
├── mockAsset.js
├── mockHelper.js
├── mockNextLink.js
├── mockObservers.js
└── mockRouter.js
├── nav
├── NavTab.test.js
└── NavTabs.test.js
├── option
├── ProductOption.test.js
└── ProductOptionsSelector.test.js
├── plp
├── ButtonFilterGroup.test.js
├── CheckboxFilterGroup.test.js
├── FacetGroup.test.js
├── Filter.test.js
├── FilterButton.test.js
├── FilterFooter.test.js
├── FilterHeader.test.js
├── SearchResultsProvider.test.js
├── ShowMore.test.js
├── Sort.test.js
├── SortButton.test.js
└── useSearchResultsStore.test.js
├── profile.test.js
├── props
├── createLazyProps.test.js
├── fetchFromAPI.test.js
└── fulfillAPIRequest.test.js
├── router
├── replaceState.test.js
├── storeInitialPropsInHistory.test.js
└── useSimpleNavigation.test.js
├── search
├── SearchButton.test.js
├── SearchDrawer.test.js
├── SearchField.test.js
├── SearchForm.test.js
├── SearchHeader.test.js
├── SearchPopover.test.js
├── SearchProvider.test.js
├── SearchSubmitButton.test.js
├── SearchSuggestionGroup.test.js
├── SearchSuggestionItem.test.js
└── SearchSuggetions.test.js
├── server
├── getRoutes.test.js
├── listRoutes.test.js
└── test-app
│ ├── pages-manifest.json
│ └── pages
│ └── p
│ └── [productId].js
├── serviceWorker.test.js
├── session
└── SessionProvider.test.js
├── sw
└── configureServiceWorker.test.js
├── theme
└── createTheme.test.js
└── utils
├── fetchLatest.test.js
├── format.test.js
├── getBase64ForImage.test.js
├── isBrowser.test.js
├── lazyLoadImages.test.js
├── merge.test.js
├── withCaching.test.js
└── withDefaultHandler.test.js
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es2021: true,
5 | jest: true,
6 | node: true,
7 | },
8 | extends: [
9 | 'eslint:recommended',
10 | 'plugin:react/recommended',
11 | 'plugin:react-hooks/recommended',
12 | 'plugin:prettier/recommended',
13 | ],
14 | parserOptions: {
15 | ecmaFeatures: {
16 | jsx: true,
17 | },
18 | ecmaVersion: 13,
19 | sourceType: 'module',
20 | },
21 | plugins: ['react'],
22 | rules: {
23 | 'no-unused-vars': ['error', { ignoreRestSiblings: true }],
24 | 'react/display-name': 'off',
25 | 'react/prop-types': [2, { ignore: ['children'] }],
26 | indent: ['error', 2, { offsetTernaryExpressions: true }],
27 | 'linebreak-style': ['error', 'unix'],
28 | quotes: ['error', 'single', { avoidEscape: true }],
29 | semi: ['error', 'never'],
30 | },
31 | }
32 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/1.Bug_Report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a bug report for React Storefront
4 | ---
5 |
6 | # Bug report
7 |
8 | ## Describe the bug
9 |
10 | A clear and concise description of what the bug is.
11 |
12 | ## To Reproduce
13 |
14 | Steps to reproduce the behavior, please provide code snippets or a repository:
15 |
16 | 1. Go to '...'
17 | 2. Click on '....'
18 | 3. Scroll down to '....'
19 | 4. See error
20 |
21 | ## Expected behavior
22 |
23 | A clear and concise description of what you expected to happen.
24 |
25 | ## Screenshots
26 |
27 | If applicable, add screenshots to help explain your problem.
28 |
29 | ## System information
30 |
31 | - OS: [e.g. macOS, Windows]
32 | - Browser (if applies) [e.g. chrome, safari]
33 | - Version of React Storefront: [e.g. 7.0.2]
34 |
35 | ## Additional context
36 |
37 | Add any other context about the problem here.
38 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/2.Feature_Request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Create a feature request for React Storefront
4 | ---
5 |
6 | # Feature request
7 |
8 | ## Is your feature request related to a problem? Please describe.
9 |
10 | A clear and concise description of what you want and what your use case is.
11 |
12 | ## Describe the solution you'd like
13 |
14 | A clear and concise description of what you want to happen.
15 |
16 | ## Describe alternatives you've considered
17 |
18 | A clear and concise description of any alternative solutions or features you've considered.
19 |
20 | ## Additional context
21 |
22 | Add any other context or screenshots about the feature request here.
23 |
--------------------------------------------------------------------------------
/.github/release-draft-template.yml:
--------------------------------------------------------------------------------
1 | categories:
2 | - title: '🚀 Features'
3 | labels:
4 | - 'feature'
5 | - 'enhancement'
6 | - title: '🐛 Bug Fixes'
7 | labels:
8 | - 'bug'
9 | - title: '🧰 Maintenance'
10 | labels:
11 | - 'chore'
12 | - 'documentation'
13 | template: |
14 | ## What's Changed
15 |
16 | $CHANGES
17 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish to NPM
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | jobs:
8 | publish-npm:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v1
12 | - uses: actions/setup-node@v1
13 | with:
14 | node-version: 12
15 | registry-url: https://registry.npmjs.org/
16 | - name: Publish to npm
17 | run: |
18 | npm ci
19 | OUTPUT=$(npm run release)
20 | VERSION=v$(echo $OUTPUT | rev | cut -d'@' -f 1 | rev)
21 | echo "::set-env name=VERSION::${VERSION}"
22 | env:
23 | NODE_AUTH_TOKEN: ${{secrets.npm_token}}
24 | ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
25 | - uses: actions/checkout@v2
26 | - uses: actions/checkout@v2
27 | with:
28 | repository: storefront-foundation/storefront-foundation.github.io
29 | token: ${{ secrets.MOOVWEB_GITHUB_TOKEN }}
30 | path: pages-repo
31 | - uses: actions/checkout@v2
32 | with:
33 | repository: moovweb/react-storefront-docs
34 | token: ${{ secrets.MOOVWEB_GITHUB_TOKEN }}
35 | path: docs-repo
36 | - uses: actions/setup-node@v1
37 | with:
38 | node-version: 12
39 | registry-url: https://registry.npmjs.org/
40 | - name: Push API docs to GitHub Pages
41 | run: |
42 | npm ci
43 | npm run docs
44 | npm run build-storybook
45 | VERSION=${{ env.VERSION }}
46 | cd pages-repo
47 | mkdir ${VERSION}
48 | mv ../.storybook/build ${VERSION}/storybook
49 | cp ../docs/build/modules.json ${VERSION}
50 | cp -r ../docs-repo/guides ${VERSION}
51 | mv ${VERSION}/guides/guides.json ${VERSION}
52 | sed -i "$ s/$/,\n${VERSION}/" versions.csv
53 | git add -A
54 | git config --local user.email "action@github.com"
55 | git config --local user.name "GitHub Action"
56 | git commit -am "Docs for version ${VERSION}"
57 | git push -u origin
58 |
--------------------------------------------------------------------------------
/.github/workflows/release-drafter.yml:
--------------------------------------------------------------------------------
1 | name: Release Drafter
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | update_release_draft:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: release-drafter/release-drafter@v5
13 | with:
14 | config-name: release-draft-template.yml
15 | env:
16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
17 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Run Tests
2 |
3 | on: [push]
4 |
5 | jobs:
6 | test:
7 | runs-on: ubuntu-latest
8 |
9 | strategy:
10 | matrix:
11 | node-version: [12.x]
12 |
13 | steps:
14 | - uses: actions/checkout@v2
15 | with:
16 | persist-credentials: false
17 | - name: Use Node.js ${{ matrix.node-version }}
18 | uses: actions/setup-node@v2
19 | with:
20 | node-version: ${{ matrix.node-version }}
21 | - name: Reconfigure git to use HTTP authentication
22 | run: >
23 | git config --global url."https://github.com/".insteadOf
24 | ssh://git@github.com/
25 | - name: npm install and test
26 | run: |
27 | npm ci
28 | npm test
29 | env:
30 | CI: true
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 | /dist
18 |
19 | # docs
20 | /docs/build
21 |
22 | # misc
23 | .DS_Store
24 | .env*
25 | .serverless_nextjs
26 |
27 | # debug
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 |
32 | # IDEs
33 | .idea/*
34 |
35 | # Index files are built automatically by create-index
36 | src/**/index.js
37 |
38 | .vscode
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | /src
2 | /test
3 | /coverage
4 | yarn-error.log
5 | yarn.lock
6 | .prettierrc
7 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v12.22.0
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "semi": false,
4 | "trailingComma": "all",
5 | "printWidth": 100
6 | }
7 |
--------------------------------------------------------------------------------
/.storybook/config.js:
--------------------------------------------------------------------------------
1 | import { configure } from '@storybook/react'
2 |
3 | configure(require.context('./stories', true, /\.stories\.js$/), module)
4 |
--------------------------------------------------------------------------------
/.storybook/main.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | addons: ['@storybook/addon-knobs/register'],
3 | }
4 |
--------------------------------------------------------------------------------
/.storybook/stories/Accordion.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ExpandableSection from '../../src/ExpandableSection'
3 | import Accordion from '../../src/Accordion'
4 |
5 | export default { title: 'Accordion' }
6 |
7 | export const defaults = () => (
8 |
9 |
10 | The first section
11 |
12 |
13 | The second section
14 |
15 |
16 | The third section
17 |
18 |
19 | )
20 |
--------------------------------------------------------------------------------
/.storybook/stories/ActionButton.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { withKnobs, text } from '@storybook/addon-knobs'
3 | import ActionButton from '../../src/ActionButton'
4 |
5 | export default { title: 'ActionButton', decorators: [withKnobs] }
6 |
7 | export const options = () => (
8 |
9 | )
10 |
--------------------------------------------------------------------------------
/.storybook/stories/AutoScrollToNewChildren.stories.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import AutoScrollToNewChildren from '../../src/AutoScrollToNewChildren'
3 |
4 | export default { title: 'AutoScrollToNewChildren' }
5 |
6 | const styles = {
7 | background: '#7f8fa6',
8 | color: '#f5f6fa',
9 | fontSize: 30,
10 | fontFamily: 'monospace',
11 | display: 'flex',
12 | justifyContent: 'center',
13 | alignItems: 'center',
14 | height: 700,
15 | marginBottom: 5,
16 | }
17 |
18 | export const defaults = () => {
19 | const [count, setCount] = useState(1)
20 | return (
21 |
22 | {Array(count)
23 | .fill(0)
24 | .map((e, i) => (
25 | setCount(count + 1)} style={styles}>
26 | Click Me ({i})
27 |
28 | ))}
29 |
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/.storybook/stories/BackToTop.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Pets } from '@mui/icons-material'
3 | import { withKnobs, select } from '@storybook/addon-knobs'
4 | import BackToTop from '../../src/BackToTop'
5 |
6 | export default { title: 'BackToTop', decorators: [withKnobs] }
7 |
8 | const styles = {
9 | background: '#7f8fa6',
10 | color: '#f5f6fa',
11 | fontSize: 30,
12 | fontFamily: 'monospace',
13 | display: 'flex',
14 | justifyContent: 'center',
15 | alignItems: 'center',
16 | height: 1500,
17 | }
18 |
19 | export const sizes = () => (
20 |
21 |
Please scroll down
22 |
25 |
26 | )
27 |
28 | export const customIcon = () => (
29 |
30 |
Please scroll down
31 |
32 |
33 | )
34 |
--------------------------------------------------------------------------------
/.storybook/stories/Breadcrumbs.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Breadcrumbs from '../../src/Breadcrumbs'
3 |
4 | export default { title: 'Breadcrumbs' }
5 |
6 | export const defaults = () => (
7 |
15 | )
16 |
--------------------------------------------------------------------------------
/.storybook/stories/Carousel.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { withKnobs, boolean, number, select } from '@storybook/addon-knobs'
3 | import Carousel from '../../src/carousel/Carousel'
4 |
5 | export default { title: 'Carousel', decorators: [withKnobs] }
6 |
7 | const slideStyle = {
8 | width: '100%',
9 | height: 300,
10 | display: 'flex',
11 | justifyContent: 'center',
12 | alignItems: 'center',
13 | fontFamily: 'monospace',
14 | fontSize: 32,
15 | color: 'white',
16 | }
17 |
18 | const slides = [
19 | Red
,
20 | Blue
,
21 | Green
,
22 | ]
23 |
24 | export const options = () => (
25 |
31 | {slides}
32 |
33 | )
34 |
--------------------------------------------------------------------------------
/.storybook/stories/CartButton.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import CartButton from '../../src/CartButton'
3 |
4 | export default { title: 'CartButton' }
5 |
6 | export const defaults = () =>
7 |
--------------------------------------------------------------------------------
/.storybook/stories/CheckboxFilterGroup.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import CheckboxFilterGroup from '../../src/plp/CheckboxFilterGroup'
3 | import SearchResultsContext from '../../src/plp/SearchResultsContext'
4 |
5 | export default { title: 'CheckboxFilterGroup' }
6 |
7 | export const defaults = () => (
8 |
9 |
10 |
11 |
12 |
13 | )
14 |
--------------------------------------------------------------------------------
/.storybook/stories/Drawer.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { withKnobs, text, boolean, select } from '@storybook/addon-knobs'
4 | import Drawer from '../../src/drawer/Drawer'
5 |
6 | export default { title: 'Drawer', decorators: [withKnobs] }
7 |
8 | export const defaults = () => (
9 | <>
10 | Use the knobs to open the drawer.
11 | Knobs can be adjusted when the drawer is closed.
12 |
23 |
33 | These are the drawer contents
34 |
35 |
36 | >
37 | )
38 |
--------------------------------------------------------------------------------
/.storybook/stories/ExpandableSection.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { withKnobs, text, boolean } from '@storybook/addon-knobs'
3 | import ExpandableSection from '../../src/ExpandableSection'
4 | import { Typography } from '@mui/material'
5 |
6 | export default { title: 'ExpandableSection', decorators: [withKnobs] }
7 |
8 | export const options = () => (
9 |
14 | {text('Content', 'This is a help section')}
15 |
16 | )
17 |
--------------------------------------------------------------------------------
/.storybook/stories/Image.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Image from '../../src/Image'
3 |
4 | export default { title: 'Image' }
5 |
6 | const styles = {
7 | display: 'flex',
8 | justifyContent: 'center',
9 | marginTop: 500,
10 | }
11 |
12 | export const lazy = () => (
13 |
14 | Please scroll down
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | )
31 |
--------------------------------------------------------------------------------
/.storybook/stories/Lazy.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Lazy from '../../src/Lazy'
3 | import PWAContext from '../../src/PWAContext'
4 |
5 | export default { title: 'Lazy' }
6 |
7 | const styles = {
8 | display: 'flex',
9 | justifyContent: 'center',
10 | alignItems: 'center',
11 | marginTop: 200,
12 | background: '#7f8fa6',
13 | height: 500,
14 | }
15 |
16 | export const lazy = () => (
17 |
18 |
19 | Please scroll down
20 |
21 |
22 | Hello
23 |
24 |
25 | Hello
26 |
27 |
28 | Hello
29 |
30 |
31 | Hello
32 |
33 |
34 |
35 |
36 | )
37 |
--------------------------------------------------------------------------------
/.storybook/stories/LoadMask.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { withKnobs, boolean, select } from '@storybook/addon-knobs'
4 | import LoadMask from '../../src/LoadMask'
5 |
6 | export default { title: 'LoadMask', decorators: [withKnobs] }
7 |
8 | export const defaults = () => (
9 |
10 |
15 |
This content is being masked.
16 |
17 | This content is also being masked.
18 |
19 |
20 | )
21 |
--------------------------------------------------------------------------------
/.storybook/stories/MediaCarousel.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {
3 | createTheme as createMuiTheme,
4 | ThemeProvider as MuiThemeProvider,
5 | } from '@mui/material/styles'
6 | import MediaCarousel from '../../src/carousel/MediaCarousel'
7 |
8 | export default { title: 'MediaCarousel' }
9 |
10 | const theme = createMuiTheme({
11 | margins: {
12 | container: 16,
13 | },
14 | })
15 |
16 | const media = {
17 | full: [
18 | { src: '//placehold.it/400?text=1', alt: 'One', type: 'image' },
19 | { src: '//placehold.it/400?text=2', alt: 'Two', type: 'image' },
20 | { src: '//placehold.it/400?text=3', alt: 'Three', type: 'image' },
21 | ],
22 | thumbnails: [
23 | { src: '//placehold.it/200?text=1', alt: 'One', type: 'image' },
24 | { src: '//placehold.it/200?text=2', alt: 'Two', type: 'image' },
25 | { src: '//placehold.it/200?text=3', alt: 'Three', type: 'image' },
26 | ],
27 | thumbnail: {
28 | src: '//placehold.it/200?text=1',
29 | alt: 'One',
30 | },
31 | }
32 |
33 | const slideStyle = {
34 | width: '100%',
35 | height: 300,
36 | display: 'flex',
37 | justifyContent: 'center',
38 | alignItems: 'center',
39 | fontFamily: 'monospace',
40 | fontSize: 32,
41 | color: 'white',
42 | background: 'grey',
43 | }
44 |
45 | const Test = props => {
46 | return (
47 |
48 |
49 |
50 | )
51 | }
52 |
53 | export const defaults = () =>
54 |
55 | export const customMediaComponent = () => (
56 | {src}
} />
57 | )
58 |
59 | export const customThumbnailsComponent = () => (
60 | {
62 | return {selected}
63 | }}
64 | />
65 | )
66 |
67 | export const noThumbnails = () =>
68 |
--------------------------------------------------------------------------------
/.storybook/stories/Menu.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Menu from '../../src/menu/Menu'
3 |
4 | export default { title: 'Menu' }
5 |
6 | const root = {
7 | text: 'category',
8 | items: [
9 | { text: 'item1', href: '/item1', as: '/item1', items: [] },
10 | { text: 'item2', href: '/item2', as: '/item2' },
11 | { text: 'item3', href: '/item3', as: '/item3' },
12 | ],
13 | }
14 |
15 | export const open = () =>
16 |
--------------------------------------------------------------------------------
/.storybook/stories/MenuButton.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import MenuButton from '../../src/menu/MenuButton'
3 |
4 | export default { title: 'MenuButton' }
5 |
6 | export const closed = () =>
7 | export const open = () =>
8 |
--------------------------------------------------------------------------------
/.storybook/stories/NavTabs.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import NavTab from '../../src/nav/NavTab'
3 |
4 | export default { title: 'NavTabs' }
5 |
6 | export const defaults = () => (
7 |
8 |
9 |
10 |
11 |
12 |
13 | )
14 |
--------------------------------------------------------------------------------
/.storybook/stories/Offline.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Pets } from '@mui/icons-material'
3 | import { withKnobs, text } from '@storybook/addon-knobs'
4 | import Offline from '../../src/Offline'
5 |
6 | export default { title: 'Offline', decorators: [withKnobs] }
7 |
8 | export const options = () => (
9 |
13 | )
14 | export const customIcon = () =>
15 |
--------------------------------------------------------------------------------
/.storybook/stories/ProductOptionSelector.stories.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import ProductOptionSelector from '../../src/option/ProductOptionSelector'
3 | import SwatchProductOption from '../../src/option/SwatchProductOption'
4 |
5 | export default { title: 'ProductOptionSelector' }
6 |
7 | const colors = [
8 | {
9 | text: 'Red',
10 | id: 'red',
11 | image: {
12 | src: 'https://via.placeholder.com/48x48/f44336?text=%20',
13 | alt: 'red',
14 | },
15 | },
16 | {
17 | text: 'Green',
18 | id: 'green',
19 | image: {
20 | src: 'https://via.placeholder.com/48x48/4caf50?text=%20',
21 | alt: 'green',
22 | },
23 | },
24 | {
25 | text: 'Blue',
26 | id: 'blue',
27 | image: {
28 | src: 'https://via.placeholder.com/48x48/2196f3?text=%20',
29 | alt: 'blue',
30 | },
31 | },
32 | {
33 | text: 'Grey',
34 | id: 'grey',
35 | image: {
36 | src: 'https://via.placeholder.com/48x48/e0e0e0?text=%20',
37 | alt: 'grey',
38 | },
39 | },
40 | ]
41 |
42 | export const swatch = () => {
43 | const [color, setColor] = useState(colors[0])
44 |
45 | return (
46 | setColor(color)} />
47 | )
48 | }
49 |
50 | export const text = () => {
51 | const [color, setColor] = useState(colors[0])
52 |
53 | return (
54 | setColor(color)}
58 | variant="text"
59 | />
60 | )
61 | }
62 |
--------------------------------------------------------------------------------
/.storybook/stories/QuantitySelector.stories.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { ArrowDownward, ArrowUpward } from '@mui/icons-material'
3 | import QuantitySelector from '../../src/QuantitySelector'
4 |
5 | export default { title: 'QuantitySelector' }
6 |
7 | export const plain = () => {
8 | const [count, setCount] = useState(0)
9 | return
10 | }
11 |
12 | export const withMinAndMax = () => {
13 | const [count, setCount] = useState(6)
14 | return
15 | }
16 |
17 | export const customIcons = () => {
18 | const [count, setCount] = useState(0)
19 | return (
20 | }
22 | subtractIcon={}
23 | value={count}
24 | onChange={setCount}
25 | />
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/.storybook/stories/Rating.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Pets } from '@mui/icons-material'
3 | import Rating from '../../src/Rating'
4 |
5 | export default { title: 'Rating' }
6 |
7 | export const defaults = () =>
8 | export const fillEmpty = () =>
9 | export const customIcons = () =>
10 |
--------------------------------------------------------------------------------
/.storybook/stories/ResponsiveTiles.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ResponsiveTiles from '../../src/ResponsiveTiles'
3 |
4 | export default { title: 'ResponsiveTiles' }
5 |
6 | const data = [
7 | { color: 'red', textColor: 'white', label: 'Tile 1' },
8 | { color: 'black', textColor: 'white', label: 'Tile 2' },
9 | { color: 'blue', textColor: 'white', label: 'Tile 3' },
10 | { color: 'skyblue', textColor: 'black', label: 'Tile 4' },
11 | { color: 'purple', textColor: 'white', label: 'Tile 5' },
12 | { color: 'yellow', textColor: 'black', label: 'Tile 6' },
13 | { color: 'gray', textColor: 'white', label: 'Tile 7' },
14 | { color: 'lime', textColor: 'black', label: 'Tile 8' },
15 | { color: 'pink', textColor: 'black', label: 'Tile 9' },
16 | { color: 'aquamarine', textColor: 'black', label: 'Tile 10' },
17 | { color: 'orange', textColor: 'black', label: 'Tile 11' },
18 | { color: 'indigo', textColor: 'white', label: 'Tile 12' },
19 | ]
20 |
21 | export const defaults = () => (
22 |
23 | {data.map(item => (
24 |
36 | {item.label}
37 |
38 | ))}
39 |
40 | )
41 |
--------------------------------------------------------------------------------
/.storybook/stories/TabPanel.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { withKnobs, boolean } from '@storybook/addon-knobs'
4 | import TabPanel from '../../src/TabPanel'
5 |
6 | export default { title: 'TabPanel', decorators: [withKnobs] }
7 |
8 | export const defaults = () => (
9 |
10 | Contents of the first tab
11 | Contents of the second tab
12 | Contents of the third tab
13 | Contents of the fourth tab
14 | Contents of the fifth tab
15 |
16 | )
17 |
--------------------------------------------------------------------------------
/.storybook/stories/ToolbarButton.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Pets } from '@mui/icons-material'
3 | import ToolbarButton from '../../src/ToolbarButton'
4 |
5 | export default { title: 'ToolbarButton' }
6 |
7 | export const defaults = () => } label="Label" />
8 |
--------------------------------------------------------------------------------
/.swcrc:
--------------------------------------------------------------------------------
1 | {
2 | "jsc": {
3 | "parser": {
4 | "syntax": "ecmascript",
5 | "jsx": true,
6 | "dynamicImport": false,
7 | "privateMethod": false,
8 | "functionBind": false,
9 | "exportDefaultFrom": false,
10 | "exportNamespaceFrom": false,
11 | "decorators": false,
12 | "decoratorsBeforeExport": false,
13 | "topLevelAwait": false,
14 | "importMeta": false
15 | },
16 | "transform": null,
17 | "target": "es5",
18 | "loose": false,
19 | "externalHelpers": false,
20 | "keepClassNames": false
21 | },
22 | "module": {
23 | "type": "commonjs",
24 | "strict": false,
25 | "strictMode": true,
26 | "lazy": false,
27 | "noInterop": false
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Apache License Version 2.0
2 |
3 | http://www.apache.org/licenses/
4 |
5 | Copyright 2020 Moov Corporation
6 |
7 | Licensed under the Apache License, Version 2.0 (the "License");
8 | you may not use this file except in compliance with the License.
9 | You may obtain a copy of the License at
10 |
11 | http://www.apache.org/licenses/LICENSE-2.0
12 |
13 | Unless required by applicable law or agreed to in writing, software
14 | distributed under the License is distributed on an "AS IS" BASIS,
15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | See the License for the specific language governing permissions and
17 | limitations under the License.
18 |
--------------------------------------------------------------------------------
/RSF BRANDGUIDE JPG.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storefront-foundation/react-storefront/6338bba57765d7489f2586751b5284d902cba310/RSF BRANDGUIDE JPG.pdf
--------------------------------------------------------------------------------
/RSF Logo trans.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storefront-foundation/react-storefront/6338bba57765d7489f2586751b5284d902cba310/RSF Logo trans.png
--------------------------------------------------------------------------------
/RSF power.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storefront-foundation/react-storefront/6338bba57765d7489f2586751b5284d902cba310/RSF power.png
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['@babel/env', '@babel/preset-react'],
3 | plugins: [
4 | [
5 | '@babel/plugin-transform-runtime',
6 | {
7 | regenerator: true,
8 | },
9 | ],
10 | [
11 | 'babel-plugin-transform-imports',
12 | {
13 | '@mui/material': {
14 | transform: '@mui/material/${member}',
15 | preventFullImport: true,
16 | },
17 | '@mui/styles': {
18 | transform: '@mui/styles/${member}',
19 | preventFullImport: true,
20 | },
21 | '@mui/icons-material': {
22 | transform: '@mui/icons-material/${member}',
23 | preventFullImport: true,
24 | },
25 | '@mui/lab': {
26 | transform: '@mui/lab/${member}',
27 | preventFullImport: true,
28 | },
29 | },
30 | ],
31 | '@babel/plugin-proposal-class-properties',
32 | ],
33 | }
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | clearMocks: true,
3 | testEnvironment: './test/config/jsdom',
4 | setupFiles: ['./test/config/setup.js'],
5 | setupFilesAfterEnv: ['./test/config/mocks.js', './node_modules/jest-enzyme/lib/index.js'],
6 | collectCoverage: true,
7 | collectCoverageFrom: [
8 | './src/**/*.{js,jsx}',
9 | './service-worker/*.{js,jsx}',
10 | '!./src/plugins/*',
11 | '!./src/mock-connector/**/*',
12 | '!**/index.js',
13 | ],
14 | moduleNameMapper: {
15 | 'react-storefront/(.*)': '/src/$1',
16 | },
17 | transformIgnorePatterns: [
18 | 'node_modules/(?!(workbox-expiration|workbox-core|workbox-routing|workbox-strategies|workbox-precaching)/)',
19 | ],
20 | }
21 |
--------------------------------------------------------------------------------
/src/Accordion.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import React, { useState } from 'react'
3 |
4 | /**
5 | * Accordion which only allows one child `ExpandableSection` to be open at a time
6 | *
7 | * ```js
8 | *
9 | *
10 | * The first section
11 | *
12 | *
13 | * The second section
14 | *
15 | *
16 | * The third section
17 | *
18 | *
19 | * ```
20 | */
21 | export default function Accordion({ children }) {
22 | const [expanded, setExpanded] = useState(
23 | () => children && children.findIndex(child => child.props.expanded),
24 | )
25 | if (!children) {
26 | return null
27 | }
28 |
29 | return (
30 | // wrapped in a Fragment so react-docgen recognizes this as a Component:
31 | <>
32 | {React.Children.map(children, (child, i) => {
33 | return React.cloneElement(child, {
34 | expanded: expanded === i,
35 | onChange: (e, expanded) => {
36 | e.preventDefault()
37 | setExpanded(expanded ? i : null)
38 | },
39 | })
40 | })}
41 | >
42 | )
43 | }
44 |
45 | Accordion.propTypes = {
46 | /**
47 | * A list of `ExpandableSection`s that will be controlled.
48 | */
49 | children: PropTypes.node,
50 | }
51 |
--------------------------------------------------------------------------------
/src/AutoScrollToNewChildren.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, useRef } from 'react'
2 |
3 | /**
4 | * A wrapper component which scrolls the first new child into view when
5 | * the number of children increases.
6 | */
7 | const AutoScrollToNewChildren = function({ children }) {
8 | const childCount = React.Children.count(children)
9 | const [priorChildCount, setPriorChildCount] = useState(childCount)
10 | const firstNewChild = useRef(null)
11 |
12 | useEffect(() => {
13 | if (!priorChildCount) {
14 | setPriorChildCount(childCount)
15 | } else if (childCount > priorChildCount) {
16 | firstNewChild.current.scrollIntoView({ behavior: 'smooth' })
17 | setPriorChildCount(childCount)
18 | }
19 | }, [childCount, setPriorChildCount, priorChildCount])
20 |
21 | return (
22 | // wrapped in a Fragment so react-docgen recognizes this as a Component:
23 | <>
24 | {React.Children.map(children, (child, index) => {
25 | return (
26 | <>
27 | {child}
28 | {index === priorChildCount ? : null}
29 | >
30 | )
31 | })}
32 | >
33 | )
34 | }
35 |
36 | export default AutoScrollToNewChildren
37 |
--------------------------------------------------------------------------------
/src/ErrorBoundary.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | /**
5 | * An internal component that catches errors durring rendering, sets the
6 | * error, stack, and page properties of the app state accordingly, and calls
7 | * the registered error reporter if one is configured.
8 | */
9 | export default class ErrorBoundary extends Component {
10 | static propTypes = {
11 | /**
12 | * A function to call whenever an error occurs. The function is passed an
13 | * object with `error` (the error message) and `stack` (the stack trace as a string).
14 | */
15 | onError: PropTypes.func,
16 | }
17 |
18 | static defaultProps = {
19 | onError: Function.prototype,
20 | }
21 |
22 | static getDerivedStateFromError(error) {
23 | return { error }
24 | }
25 |
26 | state = {
27 | error: null,
28 | }
29 |
30 | componentDidMount() {
31 | const { onError } = this.props
32 |
33 | this.windowErrorEvent = window.addEventListener('error', event => {
34 | onError({ error: event.error })
35 | })
36 |
37 | this.windowUnhandledRejectionEvent = window.addEventListener('unhandledrejection', event => {
38 | onError({ error: event.reason })
39 | })
40 | }
41 |
42 | componentWillUnmount() {
43 | window.removeEventListener('error', this.windowErrorEvent)
44 | window.removeEventListener('unhandledrejection', this.windowUnhandledRejectionEvent)
45 | }
46 |
47 | /**
48 | * When an error is caught, call the error reporter and update the app state
49 | * @param {Error} error
50 | * @param {Object} info
51 | */
52 | componentDidCatch(error) {
53 | const { onError } = this.props
54 |
55 | // report the error
56 | onError({ error })
57 | }
58 |
59 | render() {
60 | if (this.state.error) {
61 | return {this.state.error.message}
62 | }
63 |
64 | return this.props.children
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Fill.js:
--------------------------------------------------------------------------------
1 | import React, { Children } from 'react'
2 | import { styled } from '@mui/material/styles'
3 | import clsx from 'clsx'
4 | import PropTypes from 'prop-types'
5 |
6 | const PREFIX = 'Fill'
7 |
8 | const classes = {
9 | root: `${PREFIX}-root`,
10 | child: `${PREFIX}-child`,
11 | }
12 |
13 | const Root = styled('div')(() => ({
14 | /**
15 | * Styles applied to the root element.
16 | */
17 | [`&.${classes.root}`]: {
18 | position: 'relative',
19 | width: '100%',
20 | },
21 |
22 | /**
23 | * Styles applied to the wrapper element for the children.
24 | */
25 | [`& .${classes.child}`]: {
26 | position: 'absolute',
27 | top: 0,
28 | left: 0,
29 | width: '100%',
30 | height: '100%',
31 | '& > *': {
32 | width: '100%',
33 | height: '100%',
34 | },
35 | },
36 | }))
37 |
38 | /**
39 | * This component sizes the height of the child element as a percentage of its width. It expects
40 | * only a single child.
41 | *
42 | * Example:
43 | *
44 | * ```js
45 | *
46 | * this element's height will be the same as its width.
47 | *
48 | * ```
49 | */
50 | export default function Fill({ height, children, className, ...props }) {
51 | children = Children.only(children)
52 |
53 | if (height == null) {
54 | return children
55 | }
56 |
57 | return (
58 |
59 |
60 | {children}
61 |
62 | )
63 | }
64 |
65 | Fill.propTypes = {
66 | /**
67 | * The height as a percentage of the width in a css compatible expression. For example:
68 | * `"100%"` or `"calc(100% + 50px)"`, etc... If omitted, this component does nothing except render
69 | * the provided child.
70 | */
71 | height: PropTypes.string,
72 | className: PropTypes.string,
73 | }
74 |
--------------------------------------------------------------------------------
/src/ForwardThumbnail.js:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useRef, useContext, useEffect } from 'react'
2 | import PWAContext from './PWAContext'
3 |
4 | /**
5 | * Wrap product links in this component to reuse the thumbnail as the main image in the product
6 | * skeleton when transitioning to the PDP to make the transition feel instant. This component
7 | * sets the `thumbnail` ref on the provided `PWAContext` to the `src` prop of the first `img`
8 | * element found amongst the descendant elements in the tree.
9 | *
10 | * Example:
11 | *
12 | * ```js
13 | *
14 | *
15 | *
16 | *
17 | * {product.name}
18 | *
19 | *
20 | *
21 | * ```
22 | */
23 | export default function ForwardThumbnail({ children }) {
24 | const ref = useRef(null)
25 | const context = useContext(PWAContext)
26 | const srcRef = useRef(null)
27 |
28 | const setSrcRef = useCallback(() => {
29 | if (ref.current.querySelector('img')) {
30 | srcRef.current = ref.current.querySelector('img').getAttribute('src')
31 | }
32 | }, [])
33 |
34 | useEffect(setSrcRef, [children])
35 |
36 | const handleClick = useCallback(() => {
37 | if (!srcRef.current) {
38 | setSrcRef()
39 | }
40 | context.thumbnail.current = { src: srcRef.current }
41 | }, [])
42 |
43 | return (
44 |
45 | {children}
46 |
47 | )
48 | }
49 |
--------------------------------------------------------------------------------
/src/Highlight.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useAmp } from 'next/amp'
3 | import PropTypes from 'prop-types'
4 | import { Typography } from '@mui/material'
5 |
6 | const escapeHtml = unsafe =>
7 | unsafe
8 | .replace(/&/g, '&')
9 | .replace(//g, '>')
11 | .replace(/"/g, '"')
12 | .replace(/'/g, ''')
13 |
14 | const addHighlight = (query, text, className = '') => {
15 | if (!text) return ''
16 | return escapeHtml(text).replace(
17 | new RegExp(query, 'gi'),
18 | match => `${match}`,
19 | )
20 | }
21 |
22 | export default function Highlight({ query, text, classes = {}, ...props }) {
23 | if (useAmp()) {
24 | return {text}
25 | }
26 | return (
27 |
31 | )
32 | }
33 |
34 | Highlight.propTypes = {
35 | query: PropTypes.string,
36 | text: PropTypes.string,
37 | classes: PropTypes.object,
38 | }
39 |
--------------------------------------------------------------------------------
/src/Label.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import { styled } from '@mui/material/styles'
3 | import React from 'react'
4 | import clsx from 'clsx'
5 | import { Typography } from '@mui/material'
6 |
7 | const PREFIX = 'RSFLabel'
8 |
9 | const defaultClasses = {
10 | root: `${PREFIX}-root`,
11 | }
12 |
13 | const StyledTypography = styled(Typography)(() => ({
14 | /**
15 | * Styles applied to the root element\.
16 | */
17 | [`&.${defaultClasses.root}`]: {
18 | fontWeight: 500,
19 | marginRight: 10,
20 | },
21 | }))
22 |
23 | export default function Label({ className, classes: c = {}, ...props }) {
24 | const classes = { ...defaultClasses, ...c }
25 | return
26 | }
27 |
28 | Label.propTypes = {
29 | /**
30 | * Override or extend the styles applied to the component. See [CSS API](#css) below for more details.
31 | */
32 | classes: PropTypes.object,
33 | /**
34 | * CSS class to apply to the root element
35 | */
36 | className: PropTypes.string,
37 | }
38 |
--------------------------------------------------------------------------------
/src/NextScript.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { NextScript as OriginalNextScript } from 'next/document'
3 | import PropTypes from 'prop-types'
4 |
5 | /**
6 | * A replacement for NextScript from `next/document` that gives you greater control over how script elements are rendered.
7 | * This should be used in the body of `pages/_document.js` in place of `NextScript`.
8 | */
9 | export default class NextScript extends OriginalNextScript {
10 | static propTypes = {
11 | /**
12 | * Set to `defer` to use `defer` instead of `async` when rendering script elements.
13 | */
14 | mode: PropTypes.oneOf(['async', 'defer']),
15 | }
16 |
17 | static defaultProps = {
18 | mode: 'async',
19 | }
20 |
21 | getScripts(files) {
22 | return super.getScripts(files).map(script => {
23 | return React.cloneElement(script, {
24 | key: script.props.src,
25 | defer: this.props.mode === 'defer' ? true : undefined,
26 | async: this.props.mode === 'async' ? true : undefined,
27 | })
28 | })
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/NoScript.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | /**
4 | * A simple wrapper for HTML noscript that is testable in enzyme. This is
5 | * needed since enzyme won't render the contents of a noscript element.
6 | * @param {*} props
7 | */
8 | export default function NoScript(props) {
9 | if (process.env.NODE_ENV === 'test') {
10 | return
11 | }
12 | return
13 | }
14 |
--------------------------------------------------------------------------------
/src/PWAContext.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const PWAContext = React.createContext()
4 | export default PWAContext
5 |
--------------------------------------------------------------------------------
/src/Row.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import { styled } from '@mui/material/styles'
3 | import React from 'react'
4 |
5 | const PREFIX = 'Row'
6 |
7 | const defaultClasses = {
8 | root: `${PREFIX}-root`,
9 | }
10 |
11 | const Root = styled('div')(({ theme }) => ({
12 | /**
13 | * Styles applied to the root element.
14 | */
15 | [`&.${defaultClasses.root}`]: {
16 | margin: `0 0 ${theme.spacing(2)} 0`,
17 | },
18 | }))
19 |
20 | /**
21 | * A grid item that takes up the full viewport. Provided for backwards compatibility with
22 | * React Storefront 6.
23 | */
24 | export default function Row({ children, classes: c = {}, ...others }) {
25 | const classes = { ...defaultClasses, ...c }
26 |
27 | return (
28 |
29 | {children}
30 |
31 | )
32 | }
33 |
34 | Row.propTypes = {
35 | /**
36 | * Override or extend the styles applied to the component. See [CSS API](#css) below for more details.
37 | */
38 | classes: PropTypes.object,
39 | }
40 |
--------------------------------------------------------------------------------
/src/Spacer.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import { styled } from '@mui/material/styles'
3 | import React from 'react'
4 |
5 | const PREFIX = 'RSFSpacer'
6 |
7 | const defaultClasses = {
8 | root: `${PREFIX}-root`,
9 | }
10 |
11 | const Root = styled('div')(() => ({
12 | /**
13 | * Styles applied to the root element.
14 | */
15 | [`&.${defaultClasses.root}`]: {
16 | flex: 1,
17 | },
18 | }))
19 |
20 | /**
21 | * Renders a simple div with flex: 1 to be used as a spacer. Since this is a
22 | * common case, the main purposed of this class is to minimize the amount of
23 | * css generated for an app.
24 | */
25 | export {}
26 |
27 | /**
28 | * Renders a simple div with flex: 1 to be used as a spacer. Since this is a
29 | * common case, the main purposed of this class is to minimize the amount of
30 | * css generated for an app.
31 | */
32 | export default function Spacer({ classes: c = {} }) {
33 | const classes = { ...defaultClasses, ...c }
34 | return
35 | }
36 |
37 | Spacer.propTypes = {
38 | /**
39 | * Override or extend the styles applied to the component. See [CSS API](#css) below for more details.
40 | */
41 | classes: PropTypes.object,
42 | }
43 |
--------------------------------------------------------------------------------
/src/ToolbarButton.js:
--------------------------------------------------------------------------------
1 | import React, { forwardRef } from 'react'
2 | import { styled } from '@mui/material/styles'
3 | import { IconButton } from '@mui/material'
4 | import PropTypes from 'prop-types'
5 |
6 | const PREFIX = 'RSFToolbarButton'
7 |
8 | const defaultClasses = {
9 | wrap: `${PREFIX}-wrap`,
10 | }
11 |
12 | const StyledIconButton = styled(IconButton)(({ theme }) => ({
13 | /**
14 | * Styles applied to the content wrapper element inside the button
15 | */
16 | [`& .${defaultClasses.wrap}`]: {
17 | display: 'flex',
18 | flexDirection: 'column',
19 | justifyContent: 'center',
20 | alignItems: 'center',
21 | ...theme.typography.caption,
22 | },
23 | }))
24 |
25 | export {}
26 |
27 | /**
28 | * A toolbar button with optional label. Use these in your AppBar. All additional
29 | * props are spread to the underlying mui IconButton.
30 | */
31 | const ToolbarButton = forwardRef(({ icon, label, children, classes: c = {}, ...others }, ref) => {
32 | const classes = { ...defaultClasses, ...c }
33 | const { wrap, ...buttonClasses } = classes
34 |
35 | return (
36 |
37 |
38 | {icon}
39 |
{label}
40 |
41 | {children}
42 |
43 | )
44 | })
45 |
46 | ToolbarButton.propTypes = {
47 | /**
48 | * Override or extend the styles applied to the component. See [CSS API](#css) below for more details.
49 | */
50 | classes: PropTypes.object,
51 |
52 | /**
53 | * The icon to use for the button.
54 | */
55 | icon: PropTypes.element,
56 |
57 | /**
58 | * The label text for the button.
59 | */
60 | label: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
61 | }
62 |
63 | export default ToolbarButton
64 |
--------------------------------------------------------------------------------
/src/api/addVersion.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 | /**
3 | * The query param that is added to fetch alls to /api routes to ensure that
4 | * cached results from previous versions of the app are not served to new versions
5 | * of the app.
6 | */
7 | export const VERSION_PARAM = '__v__'
8 |
9 | /**
10 | * Adds the next build id as the __v__ query parameter to the given url if
11 | * the hostname is the same as the URL from which the app was served.
12 | * @param {URL|string} url Any URL
13 | * @return {URL}
14 | */
15 | export default function addVersion(url) {
16 | if (!url) return url
17 |
18 | let appOrigin = 'http://throwaway.api'
19 |
20 | /* istanbul ignore else */
21 | if (typeof window !== 'undefined') {
22 | appOrigin = window.location.href
23 | }
24 |
25 | if (typeof url === 'string') {
26 | url = new URL(url, appOrigin)
27 | }
28 |
29 | /* istanbul ignore next */
30 | if (typeof window === 'undefined') return url
31 |
32 | if (!url.searchParams.has(VERSION_PARAM) && typeof __NEXT_DATA__ !== 'undefined') {
33 | url.searchParams.append(VERSION_PARAM, __NEXT_DATA__.buildId)
34 | }
35 |
36 | return url
37 | }
38 |
--------------------------------------------------------------------------------
/src/api/getAPIURL.js:
--------------------------------------------------------------------------------
1 | import addVersion from './addVersion'
2 |
3 | /**
4 | * Returns the API URL for the given page
5 | * @param {String} pageURI The page URI
6 | * @return {String}
7 | */
8 | export default function getAPIURL(pageURI) {
9 | const parsed = addVersion(pageURI)
10 | return `/api${parsed.pathname.replace(/\/$/, '')}${parsed.search}`
11 | }
12 |
--------------------------------------------------------------------------------
/src/fetch.js:
--------------------------------------------------------------------------------
1 | import addVersion from './api/addVersion'
2 | import isSameOrigin from './utils/isSameOrigin'
3 |
4 | /**
5 | * Returns the parsed URL for the specified request
6 | * @param {Request|String} request A request instance or a URL string
7 | * @return {URL}
8 | */
9 | function getURL(request) {
10 | let { url } = request
11 |
12 | if (typeof request === 'string') {
13 | url = request
14 | }
15 |
16 | return new URL(url, window.location.href)
17 | }
18 |
19 | // Here we patch fetch and XMLHttpRequest to always add version parameter to api calls so that cached results
20 | // from previous versions of the app aren't served to new versions.
21 | /* istanbul ignore else */
22 | if (typeof window !== 'undefined') {
23 | const originalFetch = window.fetch
24 |
25 | window.fetch = function rsfVersionedFetch(url, init) {
26 | const parsed = getURL(url)
27 |
28 | if (!isSameOrigin(parsed)) {
29 | return originalFetch(url, init)
30 | }
31 |
32 | if (typeof url === 'string') {
33 | url = addVersion(parsed).toString()
34 | } else {
35 | // the first param can be a request object
36 | url = new Request(addVersion(parsed).toString(), url)
37 | }
38 |
39 | return originalFetch(url, init)
40 | }
41 | }
42 |
43 | /* istanbul ignore else */
44 | if (typeof XMLHttpRequest !== 'undefined') {
45 | const originalOpen = XMLHttpRequest.prototype.open
46 |
47 | XMLHttpRequest.prototype.open = function rsfVersionedOpen(method, url, ...others) {
48 | const parsed = getURL(url)
49 |
50 | if (isSameOrigin(parsed)) {
51 | return originalOpen.call(this, method, addVersion(parsed).toString(), ...others)
52 | }
53 | return originalOpen.call(this, method, url, ...others)
54 | }
55 | }
56 |
57 | /**
58 | * An isomorphic implementation of the fetch API. You should always use this to fetch data on both the client and server.
59 | * When making requests to /api routes, ?__v__={next_build_id} will always be added to ensure that cached results
60 | * from previous versions of the app aren't served to new versions.
61 | */
62 | export default require('isomorphic-unfetch')
63 |
--------------------------------------------------------------------------------
/src/hooks/useAppStore.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 |
3 | /**
4 | * Provides a store for app-level data that is shared by all pages, such as
5 | * the main menu, nav, and footer items.
6 | * @param {Object} props Data fetched from `getInitialProps`, which should include an `appData` key.
7 | * @return {Array} A state and an updater function, similar to the result of React's `useState` hook.
8 | * The state will contain the value of `appData` returned by `getInitialProps`.
9 | */
10 | export default function useAppStore(props) {
11 | const result = useState(props.appData)
12 | const [, setState] = result
13 |
14 | useEffect(() => {
15 | if (props.appData) {
16 | setState(props.appData)
17 | }
18 | }, [props.appData])
19 |
20 | return result
21 | }
22 |
--------------------------------------------------------------------------------
/src/hooks/useCartTotal.js:
--------------------------------------------------------------------------------
1 | import { useContext, useMemo } from 'react'
2 | import get from 'lodash/get'
3 | import SessionContext from '../session/SessionContext'
4 |
5 | function useCartTotal() {
6 | const context = useContext(SessionContext)
7 | const items = get(context, 'session.cart.items', [])
8 | const total = useMemo(() => items.reduce((totalAcc, item) => item.quantity + totalAcc, 0), [
9 | items,
10 | ])
11 | return total
12 | }
13 |
14 | export default useCartTotal
15 |
--------------------------------------------------------------------------------
/src/hooks/useIntersectionObserver.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 |
3 | function getElement(ref) {
4 | if (ref && ref.current) {
5 | return ref.current
6 | }
7 | return ref
8 | }
9 |
10 | /**
11 | * Calls a provided callback when the provided element moves into or out of the viewport.
12 | *
13 | * Example:
14 | *
15 | * ```js
16 | * import React, { useRef, useCallback } from 'react'
17 | * import useIntersectionObserver from 'react-storefront/hooks/useIntersectionObserver'
18 | *
19 | * function MyComponent() {
20 | * const ref = useRef(null)
21 | *
22 | * const onVisibilityChange = useCallback((visible, disconnect) => {
23 | * if (visible) {
24 | * // do some side effect here
25 | * // and optionally stop observing by calling: disconnect()
26 | * }
27 | * }, [])
28 | *
29 | * useIntersectionObserver(() => ref, onVisibilityChange, [])
30 | * return
31 | * }
32 | *
33 | * ```
34 | *
35 | * @param {Function} getRef A function that returns a ref pointing to the element to observe OR the element itself
36 | * @param {Function} cb A callback to call when visibility changes
37 | * @param {Object[]} deps The IntersectionObserver will be updated to observe a new ref whenever any of these change
38 | * @param {Function} notSupportedCallback Callback fired when IntersectionObserver is not supported
39 | */
40 | export default function useIntersectionObserver(getRef, cb, deps, notSupportedCallback) {
41 | useEffect(() => {
42 | if (!window.IntersectionObserver) {
43 | notSupportedCallback &&
44 | notSupportedCallback(new Error('IntersectionObserver is not available'))
45 | return
46 | }
47 | const observer = new IntersectionObserver(entries => {
48 | // if intersectionRatio is 0, the element is out of view and we do not need to do anything.
49 | cb(entries[0].intersectionRatio > 0, () => observer.disconnect())
50 | })
51 | const el = getElement(getRef())
52 | if (el instanceof Element) {
53 | observer.observe(el)
54 | return () => observer.disconnect()
55 | }
56 | }, deps)
57 | }
58 |
--------------------------------------------------------------------------------
/src/hooks/useJssStyles.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 |
3 | export default function useJssStyles() {
4 | useEffect(() => {
5 | // Remove the server-side injected CSS.
6 | const jssStyles = document.querySelector('#jss-server-side')
7 |
8 | if (jssStyles) {
9 | jssStyles.parentNode.removeChild(jssStyles)
10 | }
11 | }, [])
12 | }
13 |
--------------------------------------------------------------------------------
/src/hooks/useLocalStorage.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 |
3 | export default function useLocalStorage(key, initialValue) {
4 | // State to store our value
5 | // Pass initial state function to useState so logic is only executed once
6 | const [storedValue, setStoredValue] = useState(() => {
7 | try {
8 | // Get from local storage by key
9 | const item = window.localStorage.getItem(key)
10 | // Parse stored json or if none return initialValue
11 | return item ? JSON.parse(item) : initialValue
12 | } catch (error) {
13 | // If error also return initialValue
14 | console.log(error)
15 | return initialValue
16 | }
17 | })
18 |
19 | // Return a wrapped version of useState's setter function that ...
20 | // ... persists the new value to localStorage.
21 | const setValue = value => {
22 | try {
23 | // Allow value to be a function so we have same API as useState
24 | const valueToStore = value instanceof Function ? value(storedValue) : value
25 | // Save state
26 | setStoredValue(valueToStore)
27 | // Save to local storage
28 | window.localStorage.setItem(key, JSON.stringify(valueToStore))
29 | } catch (error) {
30 | // A more advanced implementation would handle the error case
31 | console.log(error)
32 | }
33 | }
34 |
35 | return [storedValue, setValue]
36 | }
37 |
--------------------------------------------------------------------------------
/src/hooks/useNavigationEvent.js:
--------------------------------------------------------------------------------
1 | import Router from 'next/router'
2 | import { useEffect } from 'react'
3 |
4 | export default function useNavigationEvent(cb, deps = []) {
5 | useEffect(() => {
6 | Router.events.on('routeChangeStart', cb)
7 | return () => Router.events.off('routeChangeStart', cb)
8 | }, deps)
9 | }
10 |
--------------------------------------------------------------------------------
/src/hooks/useStateFromProp.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useRef } from 'react'
2 |
3 | /**
4 | * The same as React's `useState`, but automatically updated when the specified prop value changes.
5 | * @param {Object} prop
6 | * @return {Array} The same as what's returned from React's useState hook.
7 | */
8 | export default function useStateFromProp(prop) {
9 | const state = useState(prop)
10 | const [, setValue] = state
11 | const mounted = useRef(false)
12 |
13 | useEffect(() => {
14 | if (mounted.current) {
15 | setValue(prop)
16 | }
17 | mounted.current = true
18 | }, [prop])
19 |
20 | return state
21 | }
22 |
--------------------------------------------------------------------------------
/src/hooks/useTraceUpdate.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react'
2 |
3 | export default function useTraceUpdate(props) {
4 | const prev = useRef(null)
5 |
6 | useEffect(() => {
7 | if (prev.current != null) {
8 | // don't output during initial render
9 |
10 | const changedProps = Object.entries(props).reduce((ps, [k, v]) => {
11 | if (prev.current[k] !== v) {
12 | ps[k] = [prev.current[k], v]
13 | }
14 | return ps
15 | }, {})
16 |
17 | if (Object.keys(changedProps).length > 0) {
18 | console.log('Changed props:', changedProps)
19 | } else {
20 | console.log('nothing changed')
21 | }
22 | }
23 |
24 | prev.current = props
25 | })
26 | }
27 |
28 | if (process.env.NODE_ENV !== 'production') {
29 | global.useTraceUpdate = useTraceUpdate
30 | }
31 |
--------------------------------------------------------------------------------
/src/link/LinkContext.js:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react'
2 |
3 | const LinkContext = createContext()
4 | export default LinkContext
5 |
--------------------------------------------------------------------------------
/src/link/LinkContextProvider.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from 'react'
2 | import Router from 'next/router'
3 | import LinkContext from './LinkContext'
4 |
5 | /**
6 | * Provides a context that allows links to pass data directly to pages via the `pageData` prop.
7 | */
8 | export default function LinkContextProvider({ children }) {
9 | const linkPageData = useRef(null)
10 |
11 | useEffect(() => {
12 | const onRouteChangeComplete = () => (linkPageData.current = undefined)
13 | Router.events.on('routeChangeComplete', onRouteChangeComplete)
14 |
15 | return () => {
16 | Router.events.off('routeChangeComplete', onRouteChangeComplete)
17 | }
18 | }, [])
19 |
20 | return {children}
21 | }
22 |
--------------------------------------------------------------------------------
/src/menu/MenuBack.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { ListItem, ListItemIcon, ListItemText } from '@mui/material'
4 | import { ChevronLeft } from '@mui/icons-material'
5 | import MenuContext from './MenuContext'
6 |
7 | export default function MenuBack({ goBack, item, backButtonProps }) {
8 | const { classes, renderBack } = useContext(MenuContext)
9 |
10 | return (
11 |
12 |
13 |
14 |
15 |
19 | {typeof renderBack === 'function' ? renderBack(item) : item.text}
20 |
21 | }
22 | />
23 |
24 | )
25 | }
26 |
27 | MenuBack.propTypes = {
28 | /**
29 | * Goes back to the previous item in the menu hierarchy
30 | */
31 | goBack: PropTypes.func,
32 | /**
33 | * The menu item being rendered
34 | */
35 | item: PropTypes.shape({
36 | text: PropTypes.string,
37 | }).isRequired,
38 | backButtonProps: PropTypes.object,
39 | }
40 |
--------------------------------------------------------------------------------
/src/menu/MenuBody.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react'
2 | import PropTypes from 'prop-types'
3 | import MenuContext from './MenuContext'
4 | import MenuCard from './MenuCard'
5 |
6 | const MenuBody = React.memo(
7 | ({ CardComponent, wrapProps, card, cards, rootHeader, rootFooter, children }) => {
8 | const { classes, drawerWidth } = useContext(MenuContext)
9 | const position = -drawerWidth * card
10 |
11 | return (
12 | <>
13 | {children}
14 |
19 | {cards.map((item, depth) => (
20 |
28 | ))}
29 |
30 | >
31 | )
32 | },
33 | )
34 |
35 | MenuBody.propTypes = {
36 | /**
37 | * Additional props for the wrap element
38 | */
39 | wrapProps: PropTypes.object,
40 |
41 | /**
42 | * Overrides the default component used to display menu cards
43 | */
44 | CardComponent: PropTypes.elementType.isRequired,
45 | card: PropTypes.number,
46 | cards: PropTypes.array,
47 | rootHeader: PropTypes.any,
48 | rootFooter: PropTypes.any,
49 | }
50 |
51 | MenuBody.defaultProps = {
52 | wrapProps: {},
53 | CardComponent: MenuCard,
54 | }
55 |
56 | export default MenuBody
57 |
--------------------------------------------------------------------------------
/src/menu/MenuButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { styled } from '@mui/material/styles'
3 | import { Hidden } from '@mui/material'
4 | import PropTypes from 'prop-types'
5 | import clsx from 'clsx'
6 | import ToolbarButton from '../ToolbarButton'
7 | import MenuIcon from './MenuIcon'
8 |
9 | const PREFIX = 'RSFMenuButton'
10 |
11 | const classes = {
12 | link: `${PREFIX}-link`,
13 | }
14 |
15 | const StyledHidden = styled(Hidden)(() => ({
16 | [`& .${classes.link}`]: {
17 | textDecoration: 'none',
18 | },
19 | }))
20 |
21 | export {}
22 |
23 | /**
24 | * The button that controls that opens and closes the main app menu.
25 | */
26 | export default function MenuButton({ MenuIcon, menuIconProps, open, onClick, className, style }) {
27 | return (
28 |
29 |
34 | }
39 | />
40 |
41 |
42 | )
43 | }
44 |
45 | MenuButton.propTypes = {
46 | /**
47 | * A react component to use for the menu icon
48 | */
49 | MenuIcon: PropTypes.elementType,
50 |
51 | /**
52 | * Props for the menu icon
53 | */
54 | menuIconProps: PropTypes.object,
55 | open: PropTypes.bool,
56 | onClick: PropTypes.func,
57 | className: PropTypes.string,
58 | style: PropTypes.object,
59 | }
60 |
61 | MenuButton.defaultProps = {
62 | MenuIcon,
63 | menuIconProps: {},
64 | }
65 |
--------------------------------------------------------------------------------
/src/menu/MenuCard.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react'
2 | import { List } from '@mui/material'
3 | import PropTypes from 'prop-types'
4 | import MenuItem from './MenuItem'
5 | import MenuBack from './MenuBack'
6 | import MenuHeader from './MenuHeader'
7 | import MenuFooter from './MenuFooter'
8 | import MenuContext from './MenuContext'
9 |
10 | export default function MenuCard({ item, depth, headerProps }) {
11 | const { goBack, classes, expandFirstItem, drawerWidth } = useContext(MenuContext)
12 |
13 | return (
14 |
22 | {!item.root && (
23 | goBack(depth - 1)}
26 | item={item}
27 | card={depth}
28 | {...headerProps}
29 | />
30 | )}
31 |
32 |
33 |
34 | {item.items &&
35 | item.items.map((child, i) => (
36 |
42 | ))}
43 |
44 |
45 |
46 | )
47 | }
48 |
49 | MenuCard.propTypes = {
50 | /**
51 | * Addition props for the header element
52 | */
53 | headerProps: PropTypes.object,
54 | item: PropTypes.object,
55 | depth: PropTypes.number,
56 | }
57 |
58 | MenuCard.defaultProps = {}
59 |
--------------------------------------------------------------------------------
/src/menu/MenuContext.js:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react'
2 |
3 | const MenuContext = createContext()
4 | export default MenuContext
5 |
--------------------------------------------------------------------------------
/src/menu/MenuExpanderIcon.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react'
2 | import { ChevronRight, ExpandLess, ExpandMore } from '@mui/icons-material'
3 | import PropTypes from 'prop-types'
4 | import { useAmp } from 'next/amp'
5 | import clsx from 'clsx'
6 | import MenuContext from './MenuContext'
7 |
8 | function ExpanderIcon({ ExpandIcon, CollapseIcon, showExpander, sublist, expanded }) {
9 | const { classes } = useContext(MenuContext)
10 | const amp = useAmp()
11 |
12 | ExpandIcon = ExpandIcon || ExpandMore
13 | CollapseIcon = CollapseIcon || ExpandLess
14 |
15 | if (!showExpander) return
16 |
17 | if (amp) {
18 | return (
19 | <>
20 | rsfMenuState.sublist == '${sublist}' ? '${classes.visible} ${classes.icon}' : '${classes.hidden} ${classes.icon}'`}
23 | />
24 | rsfMenuState.sublist == '${sublist}' ? '${classes.hidden} ${classes.icon}' : '${classes.visible} ${classes.icon}'`}
27 | />
28 | >
29 | )
30 | }
31 | return expanded ? (
32 |
33 | ) : (
34 |
35 | )
36 | }
37 |
38 | ExpanderIcon.propTypes = {
39 | ExpandIcon: PropTypes.any,
40 | CollapseIcon: PropTypes.any,
41 | showExpander: PropTypes.bool,
42 | sublist: PropTypes.any,
43 | expanded: PropTypes.any,
44 | }
45 |
46 | export default ExpanderIcon
47 |
--------------------------------------------------------------------------------
/src/menu/MenuFooter.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react'
2 | import { styled } from '@mui/material/styles'
3 | import PropTypes from 'prop-types'
4 | import CmsSlot from '../CmsSlot'
5 | import MenuContext from './MenuContext'
6 |
7 | const PREFIX = 'RSFMenuFooter'
8 |
9 | const classes = {
10 | root: `${PREFIX}-root`,
11 | }
12 |
13 | const Root = styled('div')(({ theme }) => ({
14 | [`&.${classes.root}`]: {
15 | padding: theme.spacing(2),
16 | },
17 | }))
18 |
19 | export {}
20 |
21 | export default function MenuFooter({ item }) {
22 | const { renderFooter } = useContext(MenuContext)
23 |
24 | if (typeof renderFooter === 'function') {
25 | return {renderFooter(item)}
26 | }
27 |
28 | if (item.footer) {
29 | return (
30 |
31 | {item.footer}
32 |
33 | )
34 | }
35 |
36 | return null
37 | }
38 |
39 | MenuFooter.propTypes = {
40 | /**
41 | * The menu item record
42 | */
43 | item: PropTypes.object,
44 | }
45 |
46 | MenuFooter.defaultProps = {}
47 |
--------------------------------------------------------------------------------
/src/menu/MenuHeader.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react'
2 | import { styled } from '@mui/material/styles'
3 | import PropTypes from 'prop-types'
4 | import CmsSlot from '../CmsSlot'
5 | import MenuContext from './MenuContext'
6 |
7 | const PREFIX = 'RSFMenuHeader'
8 |
9 | const classes = {
10 | root: `${PREFIX}-root`,
11 | }
12 |
13 | const Root = styled('div')(({ theme }) => ({
14 | [`&.${classes.root}`]: {
15 | padding: theme.spacing(2),
16 | borderBottom: `1px solid ${theme.palette.divider}`,
17 | },
18 | }))
19 |
20 | export {}
21 |
22 | export default function MenuHeader({ item }) {
23 | const { renderHeader } = useContext(MenuContext)
24 |
25 | if (typeof renderHeader === 'function') {
26 | return {renderHeader(item)}
27 | }
28 |
29 | if (item.header) {
30 | return (
31 |
32 | {item.header}
33 |
34 | )
35 | }
36 |
37 | return null
38 | }
39 |
40 | MenuHeader.propTypes = {
41 | /**
42 | * The menu item record
43 | */
44 | item: PropTypes.object,
45 | }
46 |
47 | MenuHeader.defaultProps = {}
48 |
--------------------------------------------------------------------------------
/src/menu/MenuItem.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react'
2 | import PropTypes from 'prop-types'
3 | import MenuBranch from './MenuBranch'
4 | import MenuLeaf from './MenuLeaf'
5 | import MenuContext from './MenuContext'
6 |
7 | export default function MenuItem({ BranchComponent, LeafComponent, item, ...props }) {
8 | const { renderItem } = useContext(MenuContext)
9 |
10 | let NodeType = LeafComponent
11 | let result = null
12 |
13 | if (item.items) {
14 | NodeType = BranchComponent
15 | }
16 |
17 | if (renderItem) {
18 | result = renderItem(item, item.leaf)
19 | }
20 |
21 | if (result == null) {
22 | result =
23 | }
24 |
25 | return result
26 | }
27 |
28 | MenuItem.propTypes = {
29 | BranchComponent: PropTypes.elementType.isRequired,
30 | LeafComponent: PropTypes.any,
31 | }
32 |
33 | MenuItem.defaultProps = {
34 | BranchComponent: MenuBranch,
35 | LeafComponent: MenuLeaf,
36 | }
37 |
--------------------------------------------------------------------------------
/src/menu/MenuLeaf.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react'
2 | import PropTypes from 'prop-types'
3 | import MenuItemContent from './MenuItemContent'
4 | import Link from '../link/Link'
5 | import MenuContext from './MenuContext'
6 |
7 | const MenuLeaf = function({ item, ...others }) {
8 | const { close, classes } = useContext(MenuContext)
9 |
10 | return (
11 |
18 |
19 |
20 |
21 |
22 | )
23 | }
24 |
25 | MenuLeaf.propTypes = {
26 | item: PropTypes.shape({
27 | as: PropTypes.string,
28 | pageData: PropTypes.object,
29 | href: PropTypes.string,
30 | }),
31 | }
32 |
33 | export default React.memo(MenuLeaf)
34 |
--------------------------------------------------------------------------------
/src/menu/SEOLinks.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import NoScript from '../NoScript'
4 |
5 | function SEOLinks({ root }) {
6 | if (!root) return null
7 |
8 | const links = []
9 | let key = 0
10 |
11 | const findLinks = ({ items }) => {
12 | if (!items) return
13 |
14 | for (let i = 0; i < items.length; i++) {
15 | const item = items[i]
16 |
17 | if (item.href) {
18 | links.push(
19 |
20 | {item.text}
21 | ,
22 | )
23 | }
24 |
25 | if (item.items) {
26 | findLinks(item)
27 | }
28 | }
29 | }
30 |
31 | findLinks(root)
32 |
33 | return (
34 |
48 | )
49 | }
50 |
51 | const itemShape = {
52 | as: PropTypes.string,
53 | href: PropTypes.string,
54 | }
55 |
56 | itemShape.items = PropTypes.arrayOf(PropTypes.shape(itemShape))
57 | const root = PropTypes.shape(itemShape)
58 |
59 | SEOLinks.propTypes = {
60 | root,
61 | }
62 |
63 | export default SEOLinks
64 |
--------------------------------------------------------------------------------
/src/middlewares/withAmpFormParser.js:
--------------------------------------------------------------------------------
1 | import formidable from 'formidable'
2 |
3 | /**
4 | * Wraps the provided handler in a middleware that parses AMP form submissions correctly. By
5 | * default, next.js's body parser doesn't handle multipart form posts properly, so you
6 | * won't be able to receive data posted from a form in AMP.
7 | *
8 | * When using this middleware, you should always disable next's default body parser by adding
9 | * the following to your api endpoint:
10 | *
11 | * ```js
12 | * export const config = {
13 | * api: {
14 | * bodyParser: false
15 | * }
16 | * }
17 | * ```
18 | *
19 | * @param {Function} handler An API endpoint
20 | * @return {Function} Your API function with body parsing middleware added.
21 | */
22 | export default function withAmpFormParser(handler) {
23 | return (req, res) => {
24 | const form = formidable()
25 |
26 | if (req.method.toLowerCase() === 'post') {
27 | try {
28 | form.parse(req, (err, fields) => {
29 | if (err) {
30 | res.status(500).end(err.message)
31 | } else {
32 | req.body = fields
33 | return handler(req, res)
34 | }
35 | })
36 | } catch (err) {
37 | /* istanbul ignore next */
38 | res.status(500).end(err.message)
39 | }
40 | } else {
41 | return handler(req, res)
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/mock-connector/.gitignore:
--------------------------------------------------------------------------------
1 | !/index.js
--------------------------------------------------------------------------------
/src/mock-connector/account.js:
--------------------------------------------------------------------------------
1 | import fulfillAPIRequest from '../props/fulfillAPIRequest'
2 | import createAppData from './utils/createAppData'
3 |
4 | export default async function account(req) {
5 | return await fulfillAPIRequest(req, {
6 | appData: createAppData,
7 | pageData: () =>
8 | Promise.resolve({
9 | title: 'My Account',
10 | account: {},
11 | breadcrumbs: [
12 | {
13 | text: 'Home',
14 | href: '/',
15 | },
16 | ],
17 | }),
18 | })
19 | }
20 |
--------------------------------------------------------------------------------
/src/mock-connector/addToCart.js:
--------------------------------------------------------------------------------
1 | import { addItem } from './utils/cartStore'
2 |
3 | export default async function addToCart({ product, quantity }, req, res) {
4 | return { cart: { items: addItem(product.id, quantity, req, res) } }
5 | }
6 |
--------------------------------------------------------------------------------
/src/mock-connector/cart.js:
--------------------------------------------------------------------------------
1 | import fulfillAPIRequest from '../props/fulfillAPIRequest'
2 | import createAppData from './utils/createAppData'
3 | import { getProducts } from './utils/cartStore'
4 |
5 | export default async function cart(req, res) {
6 | return fulfillAPIRequest(req, {
7 | appData: createAppData,
8 | pageData: () =>
9 | Promise.resolve({
10 | title: 'My Cart',
11 | breadcrumbs: [
12 | {
13 | text: 'Home',
14 | href: '/',
15 | },
16 | ],
17 | cart: {
18 | items: getProducts(req, res),
19 | },
20 | }),
21 | })
22 | }
23 |
--------------------------------------------------------------------------------
/src/mock-connector/home.js:
--------------------------------------------------------------------------------
1 | import fulfillAPIRequest from '../props/fulfillAPIRequest'
2 | import createAppData from './utils/createAppData'
3 |
4 | export default async function home(req) {
5 | return await fulfillAPIRequest(req, {
6 | appData: createAppData,
7 | pageData: () =>
8 | Promise.resolve({
9 | title: 'React Storefront',
10 | slots: {
11 | heading: 'Welcome to your new React Storefront app.',
12 | description: `
13 |
14 | Here you'll find mock home, category, subcategory, product, and cart pages that you can
15 | use as a starting point to build your PWA.
16 |
17 | Happy coding!
18 | `,
19 | },
20 | }),
21 | })
22 | }
23 |
--------------------------------------------------------------------------------
/src/mock-connector/index.js:
--------------------------------------------------------------------------------
1 | export { default as cart } from './cart.js'
2 | export { default as account } from './account.js'
3 | export { default as addToCart } from './addToCart.js'
4 | export { default as updateCartItem } from './updateCartItem.js'
5 | export { default as removeCartItem } from './removeCartItem.js'
6 | export { default as home } from './home.js'
7 | export { default as product } from './product.js'
8 | export { default as productMedia } from './productMedia.js'
9 | export { default as productSuggestions } from './productSuggestions.js'
10 | export { default as search } from './search.js'
11 | export { default as searchSuggestions } from './searchSuggestions.js'
12 | export { default as session } from './session.js'
13 | export { default as subcategory } from './subcategory.js'
14 |
--------------------------------------------------------------------------------
/src/mock-connector/product.js:
--------------------------------------------------------------------------------
1 | import getBase64ForImage from 'react-storefront/utils/getBase64ForImage'
2 | import fulfillAPIRequest from '../props/fulfillAPIRequest'
3 | import createProduct from './utils/createProduct'
4 | import createAppData from './utils/createAppData'
5 |
6 | function asciiSum(string = '') {
7 | return string.split('').reduce((s, e) => s + e.charCodeAt(), 0)
8 | }
9 |
10 | export default async function product(params, req) {
11 | const { id, color, size } = params
12 |
13 | const result = await fulfillAPIRequest(req, {
14 | appData: createAppData,
15 | pageData: () => getPageData(id),
16 | })
17 |
18 | // When a query parameter exists, we can fetch custom product data
19 | // pertaining to specific filters.
20 | if (color || size) {
21 | const data = await getPageData(id)
22 | data.carousel = { index: 0 }
23 | // A price for the fetched product variant would be included in
24 | // the response, but for demo purposes only, we are setting the
25 | // price based on the color name.
26 | const mockPrice = (asciiSum(color) + asciiSum(size)) / 100
27 | data.product.price = mockPrice
28 | data.product.priceText = `$${mockPrice.toFixed(2)}`
29 | return data
30 | }
31 |
32 | return result
33 | }
34 |
35 | async function getPageData(id) {
36 | const result = {
37 | title: `Product ${id}`,
38 | product: createProduct(id),
39 | breadcrumbs: [
40 | {
41 | text: 'Home',
42 | href: '/',
43 | },
44 | {
45 | text: `Subcategory ${id}`,
46 | as: `/s/${id}`,
47 | href: '/s/[subcategoryId]',
48 | },
49 | ],
50 | }
51 |
52 | const mainProductImage = result.product.media.full[0]
53 | mainProductImage.src = await getBase64ForImage(mainProductImage.src)
54 |
55 | return result
56 | }
57 |
--------------------------------------------------------------------------------
/src/mock-connector/productMedia.js:
--------------------------------------------------------------------------------
1 | import createMedia from './utils/createMedia'
2 |
3 | export default async function productMedia({ id, color }) {
4 | return { media: createMedia(id, color) }
5 | }
6 |
--------------------------------------------------------------------------------
/src/mock-connector/productSuggestions.js:
--------------------------------------------------------------------------------
1 | import createProduct from './utils/createProduct'
2 |
3 | /**
4 | * An example endpoint that returns mock product suggestions for a PDP.
5 | * @param {*} req
6 | * @param {*} res
7 | */
8 | export default async function productSuggestions() {
9 | const products = []
10 |
11 | for (let i = 1; i <= 10; i++) {
12 | products.push(createProduct(i))
13 | }
14 |
15 | return products
16 | }
17 |
--------------------------------------------------------------------------------
/src/mock-connector/removeCartItem.js:
--------------------------------------------------------------------------------
1 | import { removeItem } from './utils/cartStore'
2 |
3 | export default async function removeCartItem(item, req, res) {
4 | return { cart: { items: removeItem(item.id, req, res) } }
5 | }
6 |
--------------------------------------------------------------------------------
/src/mock-connector/search.js:
--------------------------------------------------------------------------------
1 | export { default } from './subcategory'
2 |
--------------------------------------------------------------------------------
/src/mock-connector/session.js:
--------------------------------------------------------------------------------
1 | import { getProducts } from './utils/cartStore'
2 |
3 | export default async function session(req, res) {
4 | return {
5 | name: 'Mark',
6 | email: 'mark@domain.com',
7 | cart: {
8 | items: getProducts(req, res),
9 | },
10 | currency: 'USD',
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/mock-connector/updateCartItem.js:
--------------------------------------------------------------------------------
1 | import { updateItem } from './utils/cartStore'
2 |
3 | export default function updateCartItem(item, quantity, req, res) {
4 | return { cart: { items: updateItem(item.id, quantity, req, res) } }
5 | }
6 |
--------------------------------------------------------------------------------
/src/mock-connector/utils/cartStore.js:
--------------------------------------------------------------------------------
1 | import createProduct from './createProduct'
2 |
3 | const CART_COOKIE = 'rsf_mock_cart'
4 |
5 | const initialStore = [
6 | { id: 1, quantity: 1 },
7 | { id: 2, quantity: 1 },
8 | ]
9 |
10 | function getStore(req, res) {
11 | if (!req.cookies[CART_COOKIE]) {
12 | res.setHeader('Set-Cookie', `${CART_COOKIE}=${JSON.stringify(initialStore)}; Path=/`)
13 | }
14 | const store = req.cookies[CART_COOKIE] || initialStore
15 | try {
16 | return JSON.parse(store)
17 | } catch (err) {
18 | console.log('Failed parsing store from cookie', req.cookies[CART_COOKIE])
19 | return []
20 | }
21 | }
22 |
23 | function toProduct({ id, quantity }) {
24 | return { ...createProduct(id), quantity }
25 | }
26 |
27 | export function getProducts(req, res) {
28 | return getStore(req, res).map(toProduct)
29 | }
30 |
31 | export function updateItem(id, quantity, req, res) {
32 | const newStore = [...getStore(req, res)]
33 | const item = newStore.find(e => e.id === id)
34 | item.quantity = quantity
35 | res.setHeader('Set-Cookie', `${CART_COOKIE}=${JSON.stringify(newStore)}; Path=/`)
36 | return newStore.map(toProduct)
37 | }
38 |
39 | export function removeItem(id, req, res) {
40 | const newStore = [...getStore(req, res)].filter(e => e.id !== id)
41 | res.setHeader('Set-Cookie', `${CART_COOKIE}=${JSON.stringify(newStore)}; Path=/`)
42 | return newStore.map(toProduct)
43 | }
44 |
45 | export function addItem(id, quantity, req, res) {
46 | const newStore = [{ id, quantity }, ...getStore(req, res)]
47 | res.setHeader('Set-Cookie', `${CART_COOKIE}=${JSON.stringify(newStore)}; Path=/`)
48 | return newStore.map(toProduct)
49 | }
50 |
--------------------------------------------------------------------------------
/src/mock-connector/utils/colors.js:
--------------------------------------------------------------------------------
1 | import { red, green, blue, grey, teal, orange, purple } from '@mui/material/colors'
2 |
3 | // eslint-disable-next-line no-useless-escape
4 | const color = c => c.toString().replace(/\#/, '')
5 |
6 | const colors = {
7 | red: { background: color(red[500]), foreground: 'ffffff' },
8 | green: { background: color(green[500]), foreground: 'ffffff' },
9 | blue: { background: color(blue[500]), foreground: 'ffffff' },
10 | grey: { background: color(grey[300]), foreground: color(grey[600]) },
11 | teal: { background: color(teal[500]), foreground: 'ffffff' },
12 | orange: { background: color(orange[500]), foreground: 'ffffff' },
13 | purple: { background: color(purple[500]), foreground: 'ffffff' },
14 | black: { background: color(grey[800]), foreground: 'ffffff' },
15 | }
16 |
17 | export default colors
18 |
19 | export function colorForId(id) {
20 | const keys = Object.keys(colors)
21 | const index = id % keys.length
22 | return keys[index]
23 | }
24 |
25 | export function indexForColor(color) {
26 | return Object.keys(colors).indexOf(color)
27 | }
28 |
--------------------------------------------------------------------------------
/src/mock-connector/utils/createAppData.js:
--------------------------------------------------------------------------------
1 | import createMenu from './createMenu'
2 | import createTabs from './createTabs'
3 |
4 | export default function createAppData() {
5 | return Promise.resolve({ menu: createMenu(), tabs: createTabs() })
6 | }
7 |
--------------------------------------------------------------------------------
/src/mock-connector/utils/createFacets.js:
--------------------------------------------------------------------------------
1 | import capitalize from 'lodash/capitalize'
2 | import colors from './colors'
3 |
4 | export default function createFacets() {
5 | return [
6 | {
7 | name: 'Color',
8 | ui: 'buttons',
9 | options: Object.keys(colors).map(name => ({
10 | name: capitalize(name),
11 | code: `color:${name}`,
12 | image: {
13 | src: `https://dummyimage.com/48x48/${colors[name].background}?text=${encodeURIComponent(
14 | ' ',
15 | )}`,
16 | alt: name,
17 | },
18 | })),
19 | },
20 | {
21 | name: 'Size',
22 | ui: 'buttons',
23 | options: [
24 | { name: 'SM', code: 'size:sm' },
25 | { name: 'MD', code: 'size:md' },
26 | { name: 'LG', code: 'size:lg' },
27 | { name: 'XL', code: 'size:xl' },
28 | { name: 'XXL', code: 'size:xxl' },
29 | ],
30 | },
31 | {
32 | name: 'Type',
33 | ui: 'checkboxes',
34 | options: [
35 | { name: 'New', code: 'type:new', matches: 100 },
36 | { name: 'Used', code: 'type:used', matches: 20 },
37 | ],
38 | },
39 | ]
40 | }
41 |
--------------------------------------------------------------------------------
/src/mock-connector/utils/createMedia.js:
--------------------------------------------------------------------------------
1 | import colors from './colors'
2 |
3 | export default function createMedia(id, color) {
4 | return {
5 | full: [color].map((key, i) => ({
6 | src: `https://dummyimage.com/${i === 2 ? 400 : 600}x${i === 1 ? 400 : 600}/${
7 | colors[key].background
8 | }/${colors[key].foreground}?text=${encodeURIComponent(`Product ${id}`)}`,
9 | alt: `Product ${id}`,
10 | magnify: {
11 | height: i === 1 ? 800 : 1200,
12 | width: i === 2 ? 800 : 1200,
13 | src: `https://dummyimage.com/${i === 2 ? 800 : 1200}x${i === 1 ? 800 : 1200}/${
14 | colors[key].background
15 | }/${colors[key].foreground}?text=${encodeURIComponent(`Product ${id}`)}`,
16 | },
17 | })),
18 | thumbnails: [color].map((key, i) => ({
19 | src: `https://dummyimage.com/${i === 2 ? 300 : 400}x${i === 1 ? 300 : 400}/${
20 | colors[key].background
21 | }?text=${encodeURIComponent(`Product ${id}`)}`,
22 | alt: key,
23 | })),
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/mock-connector/utils/createMenu.js:
--------------------------------------------------------------------------------
1 | export default function createMenu() {
2 | const items = []
3 |
4 | for (let i = 1; i <= 5; i++) {
5 | items.push(createCategoryItem(i))
6 | }
7 |
8 | return {
9 | items,
10 | header: 'header',
11 | footer: 'footer',
12 | }
13 | }
14 |
15 | function createCategoryItem(i) {
16 | const items = []
17 |
18 | for (let j = 1; j <= 5; j++) {
19 | items.push(createSubcategoryItem(j))
20 | }
21 |
22 | return {
23 | text: `Category ${i}`,
24 | items,
25 | }
26 | }
27 |
28 | function createSubcategoryItem(i) {
29 | const items = []
30 |
31 | for (let j = 1; j <= 5; j++) {
32 | items.push(createProductItem(j))
33 | }
34 |
35 | return {
36 | text: `Subcategory ${i}`,
37 | href: '/s/[...categorySlug]',
38 | as: `/s/${i}`,
39 | expanded: i === 1,
40 | items,
41 | }
42 | }
43 |
44 | function createProductItem(i) {
45 | return {
46 | text: `Product ${i}`,
47 | href: '/p/[productId]',
48 | as: `/p/${i}`,
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/mock-connector/utils/createProducts.js:
--------------------------------------------------------------------------------
1 | import createProduct from './createProduct'
2 |
3 | export default function createProducts(count, page = 0) {
4 | const products = []
5 |
6 | const start = page * count
7 |
8 | for (let i = 0; i < count; i++) {
9 | products.push(createProduct(start + i + 1))
10 | }
11 |
12 | return products
13 | }
14 |
--------------------------------------------------------------------------------
/src/mock-connector/utils/createSortOptions.js:
--------------------------------------------------------------------------------
1 | export default function createSortOptions() {
2 | return [
3 | { name: 'Price - Lowest', code: 'price_asc' },
4 | { name: 'Price - Highest', code: 'price_desc' },
5 | { name: 'Most Popular', code: 'pop' },
6 | { name: 'Highest Rated', code: 'rating' },
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/src/mock-connector/utils/createTabs.js:
--------------------------------------------------------------------------------
1 | export default function createTabs() {
2 | const tabs = []
3 | const subcategories = []
4 |
5 | for (let i = 1; i <= 3; i++) {
6 | subcategories.push({ as: `/s/${i}`, href: '/s/[...categorySlug]', text: `Subcategory ${i}` })
7 | }
8 |
9 | for (let i = 1; i <= 10; i++) {
10 | tabs.push({
11 | as: `/s/${i}`,
12 | href: '/s/[...categorySlug]',
13 | text: `Category ${i}`,
14 | items: subcategories,
15 | })
16 | }
17 |
18 | return tabs
19 | }
20 |
--------------------------------------------------------------------------------
/src/plp/SearchResultsContext.js:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react'
2 |
3 | const SearchResultsContext = createContext()
4 |
5 | export default SearchResultsContext
6 |
--------------------------------------------------------------------------------
/src/plp/useSearchResultsStore.js:
--------------------------------------------------------------------------------
1 | import useLazyState from '../hooks/useLazyState'
2 |
3 | /**
4 | * Allows for using search results.
5 | * @param lazyProps
6 | * @return {*[]}
7 | */
8 | export default function useSearchResultsStore(lazyProps) {
9 | const additionalData = {
10 | reloading: false,
11 | pageData: Object.freeze({
12 | page: 0,
13 | filters: [],
14 | sort: 'rating',
15 | sortSaved: 'rating',
16 | sortOptions: [],
17 | filtersChanged: false,
18 | }),
19 | }
20 |
21 | return useLazyState(lazyProps, additionalData)
22 | }
23 |
--------------------------------------------------------------------------------
/src/plugins/withServiceWorker.js:
--------------------------------------------------------------------------------
1 | const withOffline = require('next-offline')
2 | const { join } = require('path')
3 |
4 | module.exports = function withServiceWorker(config) {
5 | const generateInDevMode = process.env.SERVICE_WORKER === 'true'
6 |
7 | if (generateInDevMode) {
8 | console.log('> Using service worker in development mode')
9 | }
10 |
11 | return withOffline({
12 | ...config,
13 | generateInDevMode,
14 | generateSw: false,
15 | workboxOpts: {
16 | swDest: 'static/service-worker.js',
17 | swSrc: join(process.cwd(), 'sw', 'service-worker.js'),
18 | // The asset names for page chunks contain square brackets, eg [productId].js
19 | // Next internally injects these chunks encoded, eg %5BproductId%5D.js
20 | // For precaching to work the cache keys need to match the name of the assets
21 | // requested, therefore we need to transform the manifest entries with encoding.
22 | manifestTransforms: [
23 | manifestEntries => {
24 | console.log('> Creating service worker...')
25 | const manifest = manifestEntries
26 | .filter(entry => !entry.url.includes('next/dist')) // these paths fail in development resulting in the service worker not being installed
27 | .map(entry => {
28 | entry.url = encodeURI(entry.url)
29 | return entry
30 | })
31 | return { manifest, warnings: [] }
32 | },
33 | ],
34 | },
35 | })
36 | }
37 |
--------------------------------------------------------------------------------
/src/profile.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Executes the callback and returns its result while logging the execution time to the console.
3 | * @param {String} label A string to preface the console.log
4 | * @param {Function} cb A function to execute
5 | * @return {Object} The result of the function
6 | */
7 | export default function profile(label, cb) {
8 | if (process.env.NODE_ENV === 'production') {
9 | return cb()
10 | }
11 | const start = new Date().getTime()
12 | const result = cb()
13 | const end = new Date().getTime()
14 | console.log(label, `${end - start} ms`)
15 | return result
16 | }
17 |
18 | global.rsf_profile = profile
19 |
--------------------------------------------------------------------------------
/src/props/fetchFromAPI.js:
--------------------------------------------------------------------------------
1 | import fetch from '../fetch'
2 | import getAPIURL from '../api/getAPIURL'
3 |
4 | /**
5 | * A convenience function to be used in `getInitialProps` to fetch data for the page from an
6 | * API endpoint at the same path as the page being requested. So for example, when rendering
7 | * `/p/1`, this function will fetch data from `/api/p/1?__v__={__NEXT_DATA__.buildId}`.
8 | *
9 | * ```js
10 | * import fetchFromAPI from 'react-storefront/props/fetchFromAPI'
11 | * import createLazyProps from 'react-storefront/props/createLazyProps'
12 | *
13 | * Product.getInitialProps = createLazyProps(opts => {
14 | * return fetchFromAPI(opts)
15 | * })
16 | * ```
17 | *
18 | * Or simply:
19 | *
20 | * ```js
21 | * Product.getInitialProps = createLazyProps(fetchFromAPI)
22 | * ```
23 | *
24 | * @param {Object} opts The options object provided to `getInitialProps`
25 | * @return {Promise} A promise that resolves to the data that the page should display
26 | */
27 | export default function fetchFromAPI({ req, asPath, pathname }) {
28 | const host = req ? process.env.API_HOST || req.headers.host : ''
29 |
30 | let protocol = ''
31 |
32 | if (req) {
33 | protocol = 'https://'
34 |
35 | if (host.startsWith('localhost') || host.startsWith('127.0.0.1')) {
36 | protocol = 'http://'
37 | }
38 | }
39 |
40 | let uri = getAPIURL(asPath)
41 | let headers = {}
42 |
43 | if (req) {
44 | // on the server
45 | if (uri.indexOf('?') === -1) {
46 | uri += '?_includeAppData=1'
47 | } else {
48 | uri += '&_includeAppData=1'
49 | }
50 |
51 | headers = {
52 | host: req.headers.host,
53 | 'x-next-page': `/api${pathname.replace(/\/$/, '')}`,
54 | cookie: req.headers.cookie,
55 | }
56 | }
57 |
58 | const url = `${protocol}${host}${uri}`
59 |
60 | return fetch(url, { credentials: 'include', headers }).then(res => res.json())
61 | }
62 |
--------------------------------------------------------------------------------
/src/props/fetchServerSideProps.js:
--------------------------------------------------------------------------------
1 | import fetch from '../fetch'
2 | import getAPIURL from '../api/getAPIURL'
3 |
4 | export default function fetchServerSideProps({ req, resolvedUrl }) {
5 | const host = process.env.API_HOST || req.headers.host
6 |
7 | const protocol =
8 | host.startsWith('localhost') || host.startsWith('127.0.0.1') ? 'http://' : 'https://'
9 |
10 | let uri = getAPIURL(resolvedUrl)
11 |
12 | if (uri.indexOf('?') === -1) {
13 | uri += '?_includeAppData=1'
14 | } else {
15 | uri += '&_includeAppData=1'
16 | }
17 |
18 | const headers = {
19 | host: req.headers.host,
20 | 'x-next-page': `/api${resolvedUrl.split('?')[0].replace(/\/$/, '')}`,
21 | cookie: req.headers.cookie,
22 | }
23 |
24 | const url = `${protocol}${host}${uri}`
25 |
26 | return fetch(url, { credentials: 'include', headers })
27 | .then(res => res.json())
28 | .then(props => ({ props }))
29 | }
30 |
--------------------------------------------------------------------------------
/src/props/fulfillAPIRequest.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Creates an API response that contains app level data only when `?_includeAppData=1` is present in
3 | * the query string. Otherwise, the `appData` promise provided will not be resolved.
4 | *
5 | * @param {Request} req The request being served
6 | * @param {Object} options
7 | * @param {Function} options.appData An async function that returns a data for shared component in
8 | * the app such as menu, nav, and footer
9 | * @param {Function} options.pageData An async function that return data for the page component
10 | * @return {Object} the result of `appData` and `pageData` merged into a single object.
11 | */
12 | export default async function fulfillAPIRequest(req, { appData, pageData }) {
13 | const promises = [pageData(req).then(pageData => ({ pageData }))]
14 |
15 | if (req.query._includeAppData === '1') {
16 | promises.push(appData(req).then(appData => ({ appData })))
17 | }
18 |
19 | const results = await Promise.all(promises)
20 | const data = {}
21 |
22 | for (const result of results) {
23 | Object.assign(data, result)
24 | }
25 |
26 | return data
27 | }
28 |
--------------------------------------------------------------------------------
/src/router/replaceState.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Replaces the history state in a way that is compatible with next.js. Use this function
3 | * instead of `history.replaceState` to ensure that next.js uses your new state's URL when going back.
4 | * @param {Object} state A new state. If `null`, the existing state will be preserved.
5 | * @param {String} title A new title for the document, if `null`, the existing title will be preserved.
6 | * @param {String} url The new URL
7 | */
8 | export default function replaceState(state, title, url) {
9 | if (state == null) {
10 | state = history.state
11 | }
12 |
13 | history.replaceState({ ...state, as: url }, title || document.title, url)
14 | }
15 |
--------------------------------------------------------------------------------
/src/router/storeInitialPropsInHistory.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Patches `history.pushState` and `history.replaceState` to stores the props of the
3 | * current main component in `history.state` before navigating. This allows us to instantly
4 | * render the main component when the user goes back
5 | */
6 | export default function storeInitialPropsInHistory() {
7 | if (typeof window === 'undefined') return
8 |
9 | const { replaceState } = window.history
10 |
11 | history.replaceState = (data, title, url) => {
12 | let { state } = history
13 | if (!state) state = {}
14 | replaceState.call(history, { rsf: state.rsf, ...data }, title, url)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/router/useSimpleNavigation.js:
--------------------------------------------------------------------------------
1 | import delegate from 'delegate'
2 | import { useEffect, useRef, useCallback } from 'react'
3 | import Router from 'next/router'
4 | import qs from 'qs'
5 | import fetch from '../fetch'
6 | import getAPIURL from '../api/getAPIURL'
7 |
8 | /**
9 | * @private
10 | * Watches for clicks on HTML anchor tags and performs client side navigation if
11 | * the URL matches a next route.
12 | */
13 | export default function useSimpleNavigation() {
14 | const routes = useRef({})
15 | const nextNavigation = useRef(false)
16 |
17 | const onNextNavigation = useCallback(() => {
18 | nextNavigation.current = true
19 | }, [])
20 |
21 | const onNextNavigationEnd = useCallback(() => {
22 | nextNavigation.current = false
23 | }, [])
24 |
25 | useEffect(() => {
26 | async function doEffect() {
27 | delegate('a', 'click', e => {
28 | const { delegateTarget } = e
29 | const as = delegateTarget.getAttribute('href')
30 | const href = getRoute(as, routes.current)
31 |
32 | // catch if not next link
33 | if (href && !nextNavigation.current) {
34 | e.preventDefault()
35 | const url = toNextURL(href, as)
36 | Router.push(url, as)
37 | }
38 | })
39 |
40 | routes.current = await fetchRouteManifest()
41 | }
42 |
43 | doEffect()
44 | Router.events.on('routeChangeStart', onNextNavigation)
45 | Router.events.on('routeChangeComplete', onNextNavigationEnd)
46 |
47 | return () => Router.events.off('routeChangeStart', onNextNavigation)
48 | }, [])
49 | }
50 |
51 | function toNextURL(href, as) {
52 | const url = new URL(as, `${window.location.protocol}//${window.location.hostname}`)
53 |
54 | return {
55 | pathname: href,
56 | query: qs.parse(url.search, { ignoreQueryPrefix: true }),
57 | }
58 | }
59 |
60 | function fetchRouteManifest() {
61 | return fetch(getAPIURL('/routes')).then(res => res.json())
62 | }
63 |
64 | function getRoute(href, routes) {
65 | for (const pattern in routes) {
66 | if (new RegExp(pattern, 'i').test(href)) {
67 | return routes[pattern].as
68 | }
69 | }
70 |
71 | return null
72 | }
73 |
--------------------------------------------------------------------------------
/src/search/SearchButton.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import { styled } from '@mui/material/styles'
3 | import React from 'react'
4 | import { IconButton } from '@mui/material'
5 | import { Search } from '@mui/icons-material'
6 |
7 | const PREFIX = 'RSFSearchButton'
8 |
9 | const defaultClasses = {
10 | icon: `${PREFIX}-icon`,
11 | large: `${PREFIX}-large`,
12 | }
13 |
14 | const StyledIconButton = styled(IconButton)(({ theme }) => ({
15 | /**
16 | * Styles applied to the icon, if [children](#prop-children) is empty.
17 | */
18 | [`& .${defaultClasses.icon}`]: {
19 | color: theme.palette.action.active,
20 | },
21 |
22 | /**
23 | * Styles applied to the element containing the button's label.
24 | */
25 | [`& .${defaultClasses.large}`]: {
26 | fontSize: '28px',
27 | },
28 | }))
29 |
30 | export {}
31 |
32 | /**
33 | * A button that can be used to open a search drawer.
34 | */
35 | export default function SearchButton({ children, classes: c = {}, ...other }) {
36 | const classes = { ...defaultClasses, ...c }
37 | return (
38 |
45 | {children || }
46 |
47 | )
48 | }
49 |
50 | SearchButton.propTypes = {
51 | /**
52 | * Override or extend the styles applied to the component. See [CSS API](#css) below for more details.
53 | */
54 | classes: PropTypes.object,
55 |
56 | /**
57 | * Optional content to use for the button contents. If empty, a search icon is used.
58 | */
59 | children: PropTypes.node,
60 | }
61 |
--------------------------------------------------------------------------------
/src/search/SearchContext.js:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react'
2 |
3 | const SearchContext = createContext()
4 | export default SearchContext
5 |
--------------------------------------------------------------------------------
/src/search/SearchForm.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import { styled } from '@mui/material/styles'
3 | import React, { useRef } from 'react'
4 | import { useRouter } from 'next/router'
5 | import qs from 'qs'
6 |
7 | const PREFIX = 'RSFSearchForm'
8 |
9 | const defaultClasses = {
10 | root: `${PREFIX}-root`,
11 | }
12 |
13 | const Root = styled('form')(() => ({
14 | /**
15 | * Styles applied to the root element.
16 | */
17 | [`&.${defaultClasses.root}`]: {
18 | position: 'relative',
19 | height: '100%',
20 | display: 'flex',
21 | flexDirection: 'column',
22 | },
23 | }))
24 |
25 | export {}
26 |
27 | /**
28 | * A form used to submit a search query.
29 | */
30 | export default function SearchForm({ classes: c = {}, children, action, autoComplete }) {
31 | const classes = { ...defaultClasses, ...c }
32 | const ref = useRef()
33 | const router = useRouter()
34 |
35 | const handleSubmit = async e => {
36 | e.preventDefault()
37 |
38 | const data = new FormData(ref.current)
39 | const query = {}
40 |
41 | for (const [name, value] of data.entries()) {
42 | query[name] = value
43 | }
44 |
45 | const url = `${action}${action.includes('?') ? '&' : '?'}${qs.stringify(query)}`
46 | router.push(action.split(/\?/)[0], url)
47 | return false
48 | }
49 |
50 | return (
51 |
59 | {children}
60 |
61 | )
62 | }
63 |
64 | SearchForm.propTypes = {
65 | /**
66 | * Override or extend the styles applied to the component. See [CSS API](#css) below for more details.
67 | */
68 | classes: PropTypes.object,
69 |
70 | /**
71 | * Children to be rendered inside the form.
72 | */
73 | children: PropTypes.node,
74 |
75 | /**
76 | * An `action` attribute to use for the `