├── .gitignore ├── LICENSE.txt ├── README.md ├── screenshot.png ├── starter.json └── starter ├── .eslintrc ├── .gitignore ├── .prettierignore ├── LICENSE ├── gatsby-browser.js ├── gatsby-config.js ├── gatsby-node.js ├── package.json ├── postcss.config.js ├── src ├── components │ ├── category-list.js │ ├── footer.js │ ├── header.js │ ├── image.js │ ├── layout.js │ ├── product-list.js │ ├── product-search.js │ ├── search-results.js │ ├── seo.js │ └── styled │ │ ├── card.js │ │ └── page-heading.js ├── helpers │ ├── currency-formatter.js │ └── hooks.js ├── images │ ├── close.svg │ ├── facebook.svg │ ├── favicon.png │ ├── github.svg │ ├── search-icon.svg │ ├── strapi.png │ └── twitter.svg ├── pages │ ├── 404.js │ ├── categories │ │ └── {StrapiCategory.slug}.js │ ├── index.js │ ├── products │ │ ├── index.js │ │ └── {StrapiProduct.slug}.js │ └── using-typescript.tsx └── styles │ └── global.css ├── tailwind.config.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/macos,visualstudiocode,windows 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,visualstudiocode,windows 3 | 4 | ### macOS ### 5 | # General 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Icon must end with two \r 11 | Icon 12 | 13 | # Thumbnails 14 | ._* 15 | 16 | # Files that might appear in the root of a volume 17 | .DocumentRevisions-V100 18 | .fseventsd 19 | .Spotlight-V100 20 | .TemporaryItems 21 | .Trashes 22 | .VolumeIcon.icns 23 | .com.apple.timemachine.donotpresent 24 | 25 | # Directories potentially created on remote AFP share 26 | .AppleDB 27 | .AppleDesktop 28 | Network Trash Folder 29 | Temporary Items 30 | .apdisk 31 | 32 | ### VisualStudioCode ### 33 | .vscode/* 34 | !.vscode/tasks.json 35 | !.vscode/launch.json 36 | !.vscode/extensions.json 37 | *.code-workspace 38 | 39 | ### VisualStudioCode Patch ### 40 | # Ignore all local history of files 41 | .history 42 | 43 | ### Windows ### 44 | # Windows thumbnail cache files 45 | Thumbs.db 46 | Thumbs.db:encryptable 47 | ehthumbs.db 48 | ehthumbs_vista.db 49 | 50 | # Dump file 51 | *.stackdump 52 | 53 | # Folder config file 54 | [Dd]esktop.ini 55 | 56 | # Recycle Bin used on file shares 57 | $RECYCLE.BIN/ 58 | 59 | # Windows Installer files 60 | *.cab 61 | *.msi 62 | *.msix 63 | *.msm 64 | *.msp 65 | 66 | # Windows shortcuts 67 | *.lnk 68 | 69 | # End of https://www.toptal.com/developers/gitignore/api/macos,visualstudiocode,windows 70 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **:no_entry: Deprecated** 2 | 3 | This repository is no longer maintained and only works for Strapi v3. To find the newest Strapi v4 starters, check out the [starters-and-templates monorepo](https://github.com/strapi/starters-and-templates/). 4 | 5 | --- 6 | 7 | # Strapi Starter Gatsby Catalog 8 | 9 | ![screenshot image](/screenshot.png) 10 | 11 | This starter allows you to create a Catalog using Gatsby with a Strapi backend. The starter, and its associated template, uses musical instruments as sample seed data, but it could be customized for any product. 12 | 13 | This is an open source project. Feel free to contribute by adding features and reporting bugs. 14 | 15 | This starter uses the [Strapi catalog template](https://github.com/strapi/strapi-template-catalog) 16 | 17 | Check out all of our starters [here](https://strapi.io/starters) 18 | 19 | ## Features 20 | 21 | - 2 Collection types: Product, Category 22 | - Specifications component for products 23 | - Related products 24 | - Local Search using [gatsby-plugin-local-search](https://www.gatsbyjs.com/plugins/gatsby-plugin-local-search/) 25 | - Responsive design using Tailwind CSS 26 | 27 | ## Getting started 28 | 29 | Use our `create-strapi-starter` CLI to create your project. 30 | 31 | ```sh 32 | npx create-strapi-starter@3 my-project gatsby-catalog 33 | ``` 34 | 35 | The CLI will create a monorepo, install dependencies, and run your project automatically. 36 | 37 | The Gatsby frontend server will run here => [http://localhost:8000](http://localhost:8000) 38 | 39 | The Strapi backend server will run here => [http://localhost:1337](http://localhost:1337) 40 | 41 | ## Deploying to production 42 | 43 | You will need to deploy the `frontend` and `backend` projects separately. Here are the docs to deploy each one: 44 | 45 | - [Deploy Strapi](https://strapi.io/documentation/developer-docs/latest/setup-deployment-guides/deployment.html#hosting-provider-guides) 46 | - [Deploy Gatsby](https://www.gatsbyjs.com/docs/deploying-and-hosting/) 47 | 48 | Don't forget to setup the environment variables on your production app: 49 | 50 | For the frontend the following environment variable is required: 51 | - `API_URL`: URL of your Strapi backend, without trailing slash 52 | 53 | 54 | Enjoy this starter! 55 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strapi/strapi-starter-gatsby-catalog/64e4795bd9f66d68686e1b00c89d467c2d4c592a/screenshot.png -------------------------------------------------------------------------------- /starter.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "https://github.com/strapi/strapi-template-catalog" 3 | } 4 | -------------------------------------------------------------------------------- /starter/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "import/resolver": { 4 | "alias": [ 5 | [ 6 | "~", 7 | "./src" 8 | ] 9 | ] 10 | } 11 | }, 12 | "rules": { 13 | "prettier/prettier": [ 14 | "error", 15 | { 16 | "arrowParens": "avoid", 17 | "semi": false 18 | } 19 | ], 20 | "react/react-in-jsx-scope": 0, 21 | "jsx-a11y/anchor-is-valid": 0 22 | }, 23 | "extends": ["react-app", "plugin:prettier/recommended"] 24 | } -------------------------------------------------------------------------------- /starter/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # dotenv environment variable files 55 | .env* 56 | 57 | # gatsby files 58 | .cache/ 59 | public 60 | 61 | # Mac files 62 | .DS_Store 63 | 64 | # Yarn 65 | yarn-error.log 66 | .pnp/ 67 | .pnp.js 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | *.vscode -------------------------------------------------------------------------------- /starter/.prettierignore: -------------------------------------------------------------------------------- 1 | .cache 2 | package.json 3 | package-lock.json 4 | public 5 | -------------------------------------------------------------------------------- /starter/LICENSE: -------------------------------------------------------------------------------- 1 | The BSD Zero Clause License (0BSD) 2 | 3 | Copyright (c) 2020 Gatsby Inc. 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 9 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 10 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 11 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 12 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 13 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 14 | PERFORMANCE OF THIS SOFTWARE. 15 | -------------------------------------------------------------------------------- /starter/gatsby-browser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Implement Gatsby's Browser APIs in this file. 3 | * 4 | * See: https://www.gatsbyjs.com/docs/browser-apis/ 5 | */ 6 | 7 | // You can delete this file if you're not using it 8 | import "./src/styles/global.css" 9 | -------------------------------------------------------------------------------- /starter/gatsby-config.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config({ 2 | path: `.env`, 3 | }) 4 | 5 | module.exports = { 6 | flags: { 7 | DEV_SSR: false, 8 | }, 9 | plugins: [ 10 | `gatsby-plugin-react-helmet`, 11 | { 12 | resolve: `gatsby-source-filesystem`, 13 | options: { 14 | name: `images`, 15 | path: `${__dirname}/src/images`, 16 | }, 17 | }, 18 | `gatsby-plugin-image`, 19 | `gatsby-transformer-sharp`, 20 | `gatsby-plugin-sharp`, 21 | "gatsby-plugin-postcss", 22 | { 23 | resolve: `gatsby-plugin-manifest`, 24 | options: { 25 | name: `gatsby-starter-default`, 26 | short_name: `starter`, 27 | start_url: `/`, 28 | background_color: `#663399`, 29 | theme_color: `#663399`, 30 | display: `minimal-ui`, 31 | icon: `src/images/favicon.png`, // This path is relative to the root of the site. 32 | }, 33 | }, 34 | { 35 | resolve: `gatsby-source-strapi`, 36 | options: { 37 | apiURL: process.env.API_URL || `http://localhost:1337`, 38 | queryLimit: 1000, // Default to 100 39 | collectionTypes: [`product`, `category`], 40 | singleTypes: [`global`], 41 | }, 42 | }, 43 | // You can have multiple instances of this plugin to create indexes with 44 | // different names or engines. For example, multi-lingual sites could create 45 | // an index for each language. 46 | { 47 | resolve: "gatsby-plugin-local-search", 48 | options: { 49 | // A unique name for the search index. This should be descriptive of 50 | // what the index contains. This is required. 51 | name: "pages", 52 | // Set the search engine to create the index. This is required. 53 | // The following engines are supported: flexsearch, lunr 54 | engine: "flexsearch", 55 | // Provide options to the engine. This is optional and only recommended 56 | // for advanced users. 57 | // 58 | // Note: Only the flexsearch engine supports options. 59 | engineOptions: { 60 | profile: "speed", 61 | // Partial search moving forward 62 | tokenize: "forward", 63 | }, 64 | // GraphQL query used to fetch all data for the search index. This is 65 | // required. 66 | query: ` 67 | query { 68 | allStrapiProduct { 69 | edges { 70 | node { 71 | specifications { 72 | key 73 | value 74 | } 75 | id 76 | title 77 | price 78 | slug 79 | description 80 | image { 81 | localFile { 82 | childImageSharp { 83 | gatsbyImageData(layout: FULL_WIDTH, placeholder: BLURRED, aspectRatio: 1.3) 84 | } 85 | } 86 | } 87 | } 88 | } 89 | } 90 | } 91 | `, 92 | // Field used as the reference value for each document. 93 | // Default: 'id'. 94 | ref: "slug", 95 | // List of keys to index. The values of the keys are taken from the 96 | // normalizer function below. 97 | // Default: all fields 98 | index: ["title", "description"], 99 | // List of keys to store and make available in your UI. The values of 100 | // the keys are taken from the normalizer function below. 101 | // Default: all fields 102 | store: ["slug", "title", "description", "image", "id", "price"], 103 | // Function used to map the result from the GraphQL query. This should 104 | // return an array of items to index in the form of flat objects 105 | // containing properties to index. The objects must contain the `ref` 106 | // field above (default: 'id'). This is required. 107 | normalizer: ({ data }) => 108 | data.allStrapiProduct.edges.map(({ node }) => { 109 | return { 110 | title: node.title, 111 | description: node.description, 112 | slug: node.slug, 113 | image: node.image, 114 | id: node.id, 115 | price: node.price, 116 | } 117 | }), 118 | }, 119 | }, 120 | ], 121 | } 122 | -------------------------------------------------------------------------------- /starter/gatsby-node.js: -------------------------------------------------------------------------------- 1 | const path = require("path") 2 | 3 | exports.onCreateWebpackConfig = ({ actions, plugins, stage }) => { 4 | actions.setWebpackConfig({ 5 | resolve: { 6 | alias: { 7 | "~": path.resolve(__dirname, "src"), 8 | path: require.resolve("path-browserify"), 9 | }, 10 | fallback: { 11 | fs: false, 12 | }, 13 | }, 14 | }) 15 | if (stage === "build-javascript" || stage === "develop") { 16 | actions.setWebpackConfig({ 17 | plugins: [plugins.provide({ process: "process/browser" })], 18 | }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /starter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gatsby-starter-default", 3 | "private": true, 4 | "description": "A simple starter to get up and developing quickly with Gatsby", 5 | "version": "0.1.0", 6 | "author": "Kyle Mathews ", 7 | "dependencies": { 8 | "@tailwindcss/typography": "^0.4.0", 9 | "autoprefixer": "^10.2.4", 10 | "babel-preset-gatsby": "^1.3.0", 11 | "flexsearch": "^0.6.32", 12 | "gatsby": "^3.3.0", 13 | "gatsby-plugin-image": "^1.6.0", 14 | "gatsby-plugin-local-search": "^2.0.1", 15 | "gatsby-plugin-manifest": "^3.0.0", 16 | "gatsby-plugin-offline": "^4.3.0", 17 | "gatsby-plugin-postcss": "^4.3.0", 18 | "gatsby-plugin-react-helmet": "^4.3.0", 19 | "gatsby-plugin-sharp": "^3.6.0", 20 | "gatsby-source-filesystem": "^3.6.0", 21 | "gatsby-source-strapi": "^1.0.1", 22 | "gatsby-transformer-sharp": "^3.6.0", 23 | "global": "^4.4.0", 24 | "path-browserify": "^1.0.1", 25 | "postcss": "^8.2.10", 26 | "process": "^0.11.10", 27 | "prop-types": "^15.7.2", 28 | "react": "^17.0.2", 29 | "react-dom": "^17.0.2", 30 | "react-helmet": "^6.1.0", 31 | "react-markdown": "^6.0.0", 32 | "react-use-flexsearch": "^0.1.1", 33 | "typescript": "^4.2.4" 34 | }, 35 | "devDependencies": { 36 | "@typescript-eslint/eslint-plugin": "4.22.0", 37 | "@typescript-eslint/parser": "4.22.0", 38 | "babel-eslint": "10.x", 39 | "eslint": "7.24.0", 40 | "eslint-config-prettier": "^8.2.0", 41 | "eslint-config-react-app": "^6.0.0", 42 | "eslint-plugin-flowtype": "5.7.1", 43 | "eslint-plugin-import": "2.x", 44 | "eslint-plugin-jsx-a11y": "6.x", 45 | "eslint-plugin-prettier": "^3.4.0", 46 | "eslint-plugin-react": "7.23.2", 47 | "eslint-plugin-react-hooks": "4.2.0", 48 | "prettier": "^2.1.2", 49 | "tailwindcss": "^2.1.1" 50 | }, 51 | "keywords": [ 52 | "gatsby" 53 | ], 54 | "license": "0BSD", 55 | "scripts": { 56 | "build": "gatsby build", 57 | "develop": "gatsby develop --open", 58 | "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"", 59 | "lint": "eslint .", 60 | "lint:fix": "eslint . --fix", 61 | "start": "npm run develop", 62 | "serve": "gatsby serve", 63 | "clean": "gatsby clean", 64 | "test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\" && exit 1" 65 | }, 66 | "repository": { 67 | "type": "git", 68 | "url": "https://github.com/gatsbyjs/gatsby-starter-default" 69 | }, 70 | "bugs": { 71 | "url": "https://github.com/gatsbyjs/gatsby/issues" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /starter/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /starter/src/components/category-list.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Link } from "gatsby" 3 | import PropTypes from "prop-types" 4 | 5 | import Image from "~/components/image" 6 | import Card from "~/components/styled/card" 7 | 8 | const CategoryList = ({ categories }) => ( 9 |
10 | {categories.map(({ node }) => { 11 | return ( 12 | 13 | 14 | Category Image 19 |

{node.name}

20 | 21 |
22 | ) 23 | })} 24 |
25 | ) 26 | 27 | CategoryList.propTypes = { 28 | categories: PropTypes.array, 29 | } 30 | 31 | export default CategoryList 32 | -------------------------------------------------------------------------------- /starter/src/components/footer.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | import github from "~/images/github.svg" 4 | import twitter from "~/images/twitter.svg" 5 | import facebook from "~/images/facebook.svg" 6 | 7 | const Footer = () => { 8 | return ( 9 | 28 | ) 29 | } 30 | 31 | export default Footer 32 | -------------------------------------------------------------------------------- /starter/src/components/header.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Link } from "gatsby" 3 | import PropTypes from "prop-types" 4 | 5 | import StrapiLogo from "~/images/strapi.png" 6 | 7 | const Header = ({ setOpenModal }) => { 8 | return ( 9 |
10 |
11 | 12 | strapi catalog logo 13 | 14 |
15 | 16 | Categories 17 | 18 | 19 | Products 20 | 21 | 27 |
28 |
29 |
30 |
31 | ) 32 | } 33 | 34 | Header.propTypes = { 35 | siteName: PropTypes.string, 36 | } 37 | 38 | Header.defaultProps = { 39 | siteName: ``, 40 | } 41 | 42 | export default Header 43 | -------------------------------------------------------------------------------- /starter/src/components/image.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { useStaticQuery, graphql } from "gatsby" 3 | import { GatsbyImage, getImage } from "gatsby-plugin-image" 4 | import PropTypes from "prop-types" 5 | 6 | const Image = ({ image, className, alt }) => { 7 | const data = useStaticQuery(graphql` 8 | query { 9 | strapiGlobal { 10 | placeHolder { 11 | localFile { 12 | childImageSharp { 13 | gatsbyImageData( 14 | layout: FULL_WIDTH 15 | placeholder: BLURRED 16 | aspectRatio: 1.3 17 | ) 18 | } 19 | } 20 | } 21 | } 22 | } 23 | `) 24 | 25 | if (!image) { 26 | return ( 27 | 32 | ) 33 | } 34 | 35 | return ( 36 | 41 | ) 42 | } 43 | 44 | Image.propTypes = { 45 | image: PropTypes.object.isRequired, 46 | className: PropTypes.string, 47 | alt: PropTypes.string.isRequired, 48 | } 49 | 50 | export default Image 51 | -------------------------------------------------------------------------------- /starter/src/components/layout.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react" 2 | import PropTypes from "prop-types" 3 | import { useStaticQuery, graphql } from "gatsby" 4 | 5 | import SearchResults from "~/components/search-results" 6 | import Footer from "~/components/footer" 7 | 8 | import Header from "~/components/header" 9 | 10 | const Layout = ({ children }) => { 11 | const data = useStaticQuery(graphql` 12 | query SiteNameQuery { 13 | strapiGlobal { 14 | siteName 15 | } 16 | } 17 | `) 18 | 19 | const [openModal, setOpenModal] = useState(false) 20 | 21 | return ( 22 |
23 |
27 |
28 |
{children}
29 |
30 |
31 | {openModal && ( 32 |
33 | 34 |
35 | )} 36 |
37 | ) 38 | } 39 | 40 | Layout.propTypes = { 41 | children: PropTypes.node.isRequired, 42 | } 43 | 44 | export default Layout 45 | -------------------------------------------------------------------------------- /starter/src/components/product-list.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Link } from "gatsby" 3 | import PropTypes from "prop-types" 4 | 5 | import Card from "~/components/styled/card" 6 | import Image from "~/components/image" 7 | 8 | import { formatPrice } from "~/helpers/currency-formatter" 9 | 10 | const ProductList = ({ products, gridCols }) => { 11 | return ( 12 |
13 | {products.map(product => { 14 | return ( 15 | 16 | 17 | Product Image 22 |
23 |

{product.title}

24 |

25 | {product.price && formatPrice(product.price)} 26 |

27 |
28 | 29 |
30 | ) 31 | })} 32 |
33 | ) 34 | } 35 | 36 | ProductList.propTypes = { 37 | products: PropTypes.array, 38 | gridCols: PropTypes.string, 39 | } 40 | 41 | ProductList.defaultProps = { 42 | gridCols: "grid-cols-1 md:grid-cols-3", 43 | } 44 | 45 | export default ProductList 46 | -------------------------------------------------------------------------------- /starter/src/components/product-search.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react" 2 | import PropTypes from "prop-types" 3 | 4 | import SearchIcon from "~/images/search-icon.svg" 5 | 6 | const ProductSearch = ({ searchQuery, setSearchQuery, openModal }) => { 7 | const inputEl = useRef(null) 8 | 9 | useEffect(() => { 10 | if (openModal) { 11 | inputEl.current.focus() 12 | } 13 | }, [openModal]) 14 | 15 | return ( 16 |
17 | Search Icon 18 | setSearchQuery(e.target.value)} 22 | type="text" 23 | placeholder="Search" 24 | className="border-b-2 w-full p-2 focus:outline-none bg-transparent" 25 | /> 26 |
27 | ) 28 | } 29 | 30 | ProductSearch.propTypes = { 31 | searchQuery: PropTypes.string, 32 | setSearchQuery: PropTypes.func, 33 | } 34 | 35 | export default ProductSearch 36 | -------------------------------------------------------------------------------- /starter/src/components/search-results.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef } from "react" 2 | import { useStaticQuery, graphql } from "gatsby" 3 | import { useFlexSearch } from "react-use-flexsearch" 4 | 5 | import ProductList from "~/components/product-list" 6 | import PageHeading from "~/components/styled/page-heading" 7 | import ProductSearch from "~/components/product-search" 8 | 9 | import CloseIcon from "~/images/close.svg" 10 | 11 | import { useOnClickOutside, useOnKeypress } from "~/helpers/hooks" 12 | 13 | const SearchResults = ({ setOpenModal, openModal }) => { 14 | const data = useStaticQuery(graphql` 15 | query LocalSearchQuery { 16 | localSearchPages { 17 | index 18 | store 19 | } 20 | } 21 | `) 22 | 23 | const { 24 | localSearchPages: { index, store }, 25 | } = data 26 | const [searchQuery, setSearchQuery] = useState("") 27 | const results = useFlexSearch(searchQuery, index, store) 28 | const modal = useRef() 29 | useOnClickOutside(modal, () => setOpenModal(false)) 30 | useOnKeypress(() => setOpenModal(false)) 31 | 32 | const hasNoResults = searchQuery.length > 0 && results.length === 0 33 | 34 | return ( 35 |
39 |
40 | 46 | 51 |
52 | {results.length > 0 && ( 53 |
54 | Search Results 55 | 56 |
57 | )} 58 | {hasNoResults && ( 59 |

60 | Your search didn't return any results 61 |

62 | )} 63 |
64 | ) 65 | } 66 | 67 | export default SearchResults 68 | -------------------------------------------------------------------------------- /starter/src/components/seo.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import PropTypes from "prop-types" 3 | import { Helmet } from "react-helmet" 4 | import { useStaticQuery, graphql } from "gatsby" 5 | 6 | const SEO = ({ seo = {} }) => { 7 | const query = graphql` 8 | query { 9 | strapiGlobal { 10 | siteName 11 | favicon { 12 | localFile { 13 | publicURL 14 | } 15 | } 16 | defaultSeo { 17 | metaTitle 18 | metaDescription 19 | shareImage { 20 | localFile { 21 | publicURL 22 | } 23 | } 24 | } 25 | } 26 | } 27 | ` 28 | const { strapiGlobal } = useStaticQuery(query) 29 | const { defaultSeo, siteName, favicon } = strapiGlobal 30 | 31 | // Merge default and page-specific SEO values 32 | const fullSeo = { ...defaultSeo, ...seo } 33 | 34 | const getMetaTags = () => { 35 | const tags = [] 36 | 37 | if (fullSeo.metaTitle) { 38 | tags.push( 39 | { 40 | property: "og:title", 41 | content: fullSeo.metaTitle, 42 | }, 43 | { 44 | name: "twitter:title", 45 | content: fullSeo.metaTitle, 46 | } 47 | ) 48 | } 49 | if (fullSeo.metaDescription) { 50 | tags.push( 51 | { 52 | name: "description", 53 | content: fullSeo.metaDescription, 54 | }, 55 | { 56 | property: "og:description", 57 | content: fullSeo.metaDescription, 58 | }, 59 | { 60 | name: "twitter:description", 61 | content: fullSeo.metaDescription, 62 | } 63 | ) 64 | } 65 | if (fullSeo.shareImage) { 66 | const imageUrl = 67 | (process.env.API_URL || "http://localhost:8000") + 68 | fullSeo.shareImage.localFile.publicURL 69 | tags.push( 70 | { 71 | name: "image", 72 | content: imageUrl, 73 | }, 74 | { 75 | property: "og:image", 76 | content: imageUrl, 77 | }, 78 | { 79 | name: "twitter:image", 80 | content: imageUrl, 81 | } 82 | ) 83 | } 84 | if (fullSeo.article) { 85 | tags.push({ 86 | property: "og:type", 87 | content: "article", 88 | }) 89 | } 90 | tags.push({ name: "twitter:card", content: "summary_large_image" }) 91 | 92 | return tags 93 | } 94 | 95 | const metaTags = getMetaTags() 96 | 97 | return ( 98 | 109 | ) 110 | } 111 | 112 | SEO.propTypes = { 113 | title: PropTypes.string, 114 | image: PropTypes.string, 115 | } 116 | 117 | SEO.defaultProps = { 118 | title: null, 119 | image: null, 120 | } 121 | 122 | export default SEO 123 | -------------------------------------------------------------------------------- /starter/src/components/styled/card.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | const Card = ({ children }) => ( 4 |
{children}
5 | ) 6 | 7 | export default Card 8 | -------------------------------------------------------------------------------- /starter/src/components/styled/page-heading.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | const PageHeading = ({ children }) => ( 4 |

5 | {children} 6 |

7 | ) 8 | 9 | export default PageHeading 10 | -------------------------------------------------------------------------------- /starter/src/helpers/currency-formatter.js: -------------------------------------------------------------------------------- 1 | export const formatPrice = (price, currency = "USD") => { 2 | const formatter = new Intl.NumberFormat("en-US", { 3 | style: "currency", 4 | currency: currency, 5 | minimumFractionDigits: 2, 6 | }) 7 | 8 | return formatter.format(price) 9 | } 10 | -------------------------------------------------------------------------------- /starter/src/helpers/hooks.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react" 2 | 3 | export function useOnClickOutside(ref, handler) { 4 | useEffect(() => { 5 | const listener = event => { 6 | // Do nothing if clicking ref's element or descendent elements 7 | if (!ref.current || ref.current.contains(event.target)) { 8 | return 9 | } 10 | 11 | handler(event) 12 | } 13 | 14 | document.addEventListener("mousedown", listener) 15 | document.addEventListener("touchstart", listener) 16 | 17 | return () => { 18 | document.removeEventListener("mousedown", listener) 19 | document.removeEventListener("touchstart", listener) 20 | } 21 | }, [ref, handler]) 22 | } 23 | 24 | export function useOnKeypress(handler) { 25 | useEffect(() => { 26 | const listener = event => { 27 | if (event.key === "Escape" || event.key === "Esc") { 28 | handler(event) 29 | } 30 | } 31 | 32 | document.addEventListener("keydown", listener) 33 | document.addEventListener("keyup", listener) 34 | 35 | return () => { 36 | document.removeEventListener("keydown", listener) 37 | document.removeEventListener("keyup", listener) 38 | } 39 | }, [handler]) 40 | } 41 | -------------------------------------------------------------------------------- /starter/src/images/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /starter/src/images/facebook.svg: -------------------------------------------------------------------------------- 1 | 10 | 13 | -------------------------------------------------------------------------------- /starter/src/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strapi/strapi-starter-gatsby-catalog/64e4795bd9f66d68686e1b00c89d467c2d4c592a/starter/src/images/favicon.png -------------------------------------------------------------------------------- /starter/src/images/github.svg: -------------------------------------------------------------------------------- 1 | 9 | 12 | -------------------------------------------------------------------------------- /starter/src/images/search-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /starter/src/images/strapi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strapi/strapi-starter-gatsby-catalog/64e4795bd9f66d68686e1b00c89d467c2d4c592a/starter/src/images/strapi.png -------------------------------------------------------------------------------- /starter/src/images/twitter.svg: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /starter/src/pages/404.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | import Layout from "~/components/layout" 4 | import SEO from "~/components/seo" 5 | 6 | const NotFoundPage = () => ( 7 | 8 | 9 |

404: Not Found

10 |

You just hit a route that doesn't exist... the sadness.

11 |
12 | ) 13 | 14 | export default NotFoundPage 15 | -------------------------------------------------------------------------------- /starter/src/pages/categories/{StrapiCategory.slug}.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { graphql } from "gatsby" 3 | 4 | import Layout from "~/components/layout" 5 | import PageHeading from "~/components/styled/page-heading" 6 | import ProductList from "~/components/product-list" 7 | import SEO from "~/components/seo" 8 | 9 | const CategoryPage = ({ data }) => { 10 | const products = data.strapiCategory.products 11 | const seo = { 12 | title: data.strapiCategory.name, 13 | } 14 | 15 | return ( 16 | 17 | 18 |
19 | {data.strapiCategory.name} 20 | 21 |
22 |
23 | ) 24 | } 25 | 26 | export const query = graphql` 27 | query CategoryQuery($slug: String!) { 28 | strapiCategory(slug: { eq: $slug }) { 29 | name 30 | products { 31 | title 32 | slug 33 | price 34 | id 35 | image { 36 | localFile { 37 | childImageSharp { 38 | gatsbyImageData( 39 | layout: FULL_WIDTH 40 | placeholder: BLURRED 41 | aspectRatio: 1.3 42 | ) 43 | } 44 | } 45 | } 46 | } 47 | } 48 | } 49 | ` 50 | 51 | export default CategoryPage 52 | -------------------------------------------------------------------------------- /starter/src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { graphql } from "gatsby" 3 | 4 | import Layout from "~/components/layout" 5 | import SEO from "~/components/seo" 6 | import CategoryList from "~/components/category-list" 7 | import PageHeading from "~/components/styled/page-heading" 8 | 9 | const IndexPage = ({ data: { allStrapiCategory } }) => { 10 | const categories = allStrapiCategory.edges 11 | const seo = { title: "Categories" } 12 | return ( 13 | 14 | 15 | Categories 16 | 17 | 18 | ) 19 | } 20 | 21 | export const query = graphql` 22 | query CategoriesQuery { 23 | allStrapiCategory { 24 | edges { 25 | node { 26 | name 27 | id 28 | slug 29 | image { 30 | localFile { 31 | childImageSharp { 32 | gatsbyImageData( 33 | layout: FULL_WIDTH 34 | placeholder: BLURRED 35 | aspectRatio: 1.3 36 | ) 37 | } 38 | } 39 | } 40 | } 41 | } 42 | } 43 | } 44 | ` 45 | 46 | export default IndexPage 47 | -------------------------------------------------------------------------------- /starter/src/pages/products/index.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { graphql } from "gatsby" 3 | 4 | import Layout from "~/components/layout" 5 | import PageHeading from "~/components/styled/page-heading" 6 | import ProductList from "~/components/product-list" 7 | import SEO from "~/components/seo" 8 | 9 | const SearchPage = ({ 10 | data: { 11 | allStrapiProduct: { edges }, 12 | }, 13 | }) => { 14 | const flatProducts = edges.map(({ node }) => node) 15 | 16 | const seo = { title: "Products" } 17 | 18 | return ( 19 | 20 | 21 | Products 22 | 23 | 24 | ) 25 | } 26 | 27 | export const searchPageQuery = graphql` 28 | query ProductSearchQuery { 29 | allStrapiProduct { 30 | edges { 31 | node { 32 | specifications { 33 | key 34 | value 35 | } 36 | title 37 | price 38 | slug 39 | id 40 | description 41 | image { 42 | localFile { 43 | childImageSharp { 44 | gatsbyImageData( 45 | layout: FULL_WIDTH 46 | placeholder: BLURRED 47 | aspectRatio: 1.3 48 | ) 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | ` 57 | 58 | export default SearchPage 59 | -------------------------------------------------------------------------------- /starter/src/pages/products/{StrapiProduct.slug}.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactMarkdown from "react-markdown" 3 | import { graphql } from "gatsby" 4 | 5 | import Layout from "~/components/layout" 6 | import ProductList from "~/components/product-list" 7 | import SEO from "~/components/seo" 8 | import Image from "~/components/image" 9 | 10 | import { formatPrice } from "~/helpers/currency-formatter" 11 | 12 | const ProductPage = ({ data }) => { 13 | const product = data.strapiProduct 14 | 15 | const seo = { 16 | title: product.title, 17 | shareImage: product.image, 18 | } 19 | 20 | const flexJustify = product.specifications.length > 0 ? "between" : "center" 21 | 22 | return ( 23 | 24 | 25 |
26 | {product.image && ( 27 |
28 | Product Image 33 |
34 | )} 35 |
36 |
37 |

{product.title}

38 | {product.price && ( 39 |
40 |

Price

41 |

{formatPrice(product.price)}

42 |
43 | )} 44 |
45 |
46 | {product.specifications && 47 | product.specifications.map((spec, index) => ( 48 |
52 | {spec.key} 53 | {spec.value} 54 |
55 | ))} 56 |
57 | 63 | Shop Online 64 | 65 |
66 |
67 |
68 |

Product Description

69 |
70 | 74 |
75 | {product.relatedProducts.length > 0 && ( 76 |
77 |

Related Products

78 |
79 | 83 |
84 | )} 85 |
86 | ) 87 | } 88 | 89 | export const query = graphql` 90 | query ProductQuery($slug: String!) { 91 | strapiProduct(slug: { eq: $slug }) { 92 | title 93 | description 94 | id 95 | price 96 | dealerUrl 97 | image { 98 | localFile { 99 | publicURL 100 | childImageSharp { 101 | gatsbyImageData( 102 | layout: FULL_WIDTH 103 | placeholder: BLURRED 104 | aspectRatio: 1.3 105 | ) 106 | } 107 | } 108 | } 109 | specifications { 110 | key 111 | value 112 | } 113 | relatedProducts { 114 | title 115 | price 116 | id 117 | slug 118 | image { 119 | localFile { 120 | childImageSharp { 121 | gatsbyImageData( 122 | layout: FULL_WIDTH 123 | placeholder: BLURRED 124 | aspectRatio: 1.3 125 | ) 126 | } 127 | } 128 | } 129 | } 130 | } 131 | } 132 | ` 133 | 134 | export default ProductPage 135 | -------------------------------------------------------------------------------- /starter/src/pages/using-typescript.tsx: -------------------------------------------------------------------------------- 1 | // If you don't want to use TypeScript you can delete this file! 2 | import React from "react" 3 | import { PageProps, Link, graphql } from "gatsby" 4 | 5 | import Layout from "../components/layout" 6 | import SEO from "../components/seo" 7 | 8 | type DataProps = { 9 | site: { 10 | buildTime: string 11 | } 12 | } 13 | 14 | const UsingTypescript: React.FC> = ({ data, path }) => ( 15 | 16 | 17 |

Gatsby supports TypeScript by default!

18 |

19 | This means that you can create and write .ts/.tsx files for your 20 | pages, components etc. Please note that the gatsby-*.js files 21 | (like gatsby-node.js) currently don't support TypeScript yet. 22 |

23 |

24 | For type checking you'll want to install typescript via npm and 25 | run tsc --init to create a .tsconfig file. 26 |

27 |

28 | You're currently on the page "{path}" which was built on{" "} 29 | {data.site.buildTime}. 30 |

31 |

32 | To learn more, head over to our{" "} 33 | 34 | documentation about TypeScript 35 | 36 | . 37 |

38 | Go back to the homepage 39 |
40 | ) 41 | 42 | export default UsingTypescript 43 | 44 | export const query = graphql` 45 | { 46 | site { 47 | buildTime(formatString: "YYYY-MM-DD hh:mm a z") 48 | } 49 | } 50 | ` 51 | -------------------------------------------------------------------------------- /starter/src/styles/global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | 3 | @layer base { 4 | h1 { 5 | @apply text-4xl; 6 | } 7 | h2 { 8 | @apply text-2xl; 9 | } 10 | h3 { 11 | @apply text-xl; 12 | } 13 | a { 14 | @apply no-underline; 15 | } 16 | 17 | p, 18 | a, 19 | html, 20 | body { 21 | @apply text-gray-700; 22 | } 23 | 24 | h1, 25 | h2, 26 | h3 { 27 | @apply text-gray-900 28 | } 29 | } 30 | 31 | @tailwind components; 32 | @tailwind utilities; 33 | -------------------------------------------------------------------------------- /starter/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: ["./src/**/*.{js,jsx,ts,tsx}"], 3 | darkMode: false, // or 'media' or 'class' 4 | theme: { 5 | fontFamily: { 6 | serif: ["georgia", "serif"], 7 | }, 8 | }, 9 | variants: { 10 | extend: {}, 11 | }, 12 | plugins: [require("@tailwindcss/typography")], 13 | } 14 | --------------------------------------------------------------------------------