├── .gitignore
├── LICENSE
├── README.md
├── components
├── BackToProductButton.js
├── CartTable.js
├── CheckOutButton.js
├── Footer.js
├── Layout.js
├── Nav.js
├── PageTitle.js
├── Price.js
├── ProductCard.js
├── ProductDetails.js
├── ProductForm.js
├── ProductImage.js
├── ProductInfo.js
├── ProductListings.js
├── ProductSection.js
├── SEO.js
└── StoreHeading.js
├── context
└── Store.js
├── jsconfig.json
├── lib
└── shopify.js
├── next.config.js
├── package.json
├── pages
├── _app.js
├── _document.js
├── cart.js
├── index.js
└── products
│ └── [product].js
├── postcss.config.js
├── public
├── favicon.ico
├── icon.svg
├── icons
│ ├── apple-icon.png
│ ├── icon-144x144.png
│ ├── icon-150x150.png
│ ├── icon-16x16.png
│ ├── icon-192x192.png
│ ├── icon-32x32.png
│ ├── icon-512x512.png
│ └── icon-70x70.png
├── images
│ ├── demo-store.gif
│ ├── desktop-lighthouse.png
│ ├── main.jpg
│ ├── mobile-lighthouse.png
│ ├── responsive-cart.gif
│ └── responsive-main.gif
└── manifest.json
├── styles
└── globals.css
├── tailwind.config.js
├── utils
└── helpers.js
└── yarn.lock
/.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 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | # vercel
34 | .vercel
35 |
36 | # pwa
37 | **/public/precache.*.*.js
38 | **/public/sw.js
39 | **/public/workbox-*.js
40 | **/public/worker-*.js
41 | **/public/precache.*.*.js.map
42 | **/public/sw.js.map
43 | **/public/workbox-*.js.map
44 | **/public/worker-*.js.map
45 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Bilal Tahir
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Next.js + Tailwind CSS + Shopify Starter
2 |
3 |
11 |
12 | This is a fully functional eCommerce store that uses Next.js + Tailwind CSS in the front end and leverages the Shopify Storefront API to interact with your Shopify backend. You can see a Live Demo [here](https://doggystickers.vercel.app/ "Shopify store").
13 |
14 | We use GraphQL to query our Shopify data and store the cart information in localStorage to persist user session. Finally - we use Shopify Checkout to let the user
15 | purchase the items. You can see this play out in the example store. Yes - the store is functional and you can buy the stickers. :smiley:
16 |
17 | ## High Performance
18 |
19 |
20 |
21 |
22 | Desktop
23 | Mobile
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | ## Mobile Responsive
35 |
36 |
37 |
38 |
39 | Listings
40 | Cart
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | ## The Tech
53 |
54 | * Next.js + Tailwind CSS
55 | * GraphQL
56 | * localStorage to persist user session
57 | * Shopify
58 | * Vercel
59 | * Font Awesome Icons
60 | * Josefin Sans Google Font
61 |
62 | ## How to use
63 |
64 | By default, the store is set to query and show all products in one collection.
65 | You can extend this to query multiple collections or your whole store.
66 |
67 | #### A note on pagination in the GraphQL queries
68 |
69 | The graphQL queries are all hardcoded to pull the maximum number of products/variants/images which
70 | is set to 250 by Shopify. I did this to keep things simple. Pagination would have made the queries complicated
71 | and 250 items is enough for most use cases.
72 |
73 | If you require pagination you will have to keep track of the [cursor](https://youtu.be/S37WsC8GzSA "graphql pagination") field and keep querying the data until you fetch all items.
74 |
75 | ### Setup Environment variables
76 |
77 | Create a .env.local file in the root directory. You need to add these 4 variables:
78 |
79 | ```
80 | NEXT_PUBLIC_SHOPIFY_STORE_FRONT_ACCESS_TOKEN=
81 | NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN=
82 | NEXT_PUBLIC_SHOPIFY_COLLECTION=
83 | NEXT_PUBLIC_LOCAL_STORAGE_NAME=
84 | ```
85 |
86 | The NEXT_PUBLIC_SHOPIFY_STORE_FRONT_ACCESS_TOKEN and NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN (it will be something like DOMAIN_NAME.myshopify.com) are needed to access
87 | the Shopify Storefront API (make sure you have set it up in your Shopify store. See [docs](https://shopify.dev/docs/storefront-api/getting-started "Shopify store") for more information.
88 |
89 | NEXT_PUBLIC_SHOPIFY_COLLECTION is the name of the collection you want to pull in and NEXT_PUBLIC_LOCAL_STORAGE_NAME is the name of the key
90 | your users will store their cart information under. The exact name isn't that important although I suggest you make it unique so
91 | it is less likely to clash with other stored objects. Something like yourStoreNameShopifyStore where yourStoreName is your shopify store name will suffice.
92 |
93 | ### Installation
94 |
95 | Change into the project directory and run the following command:
96 |
97 | ```
98 | yarn && yarn dev
99 | ```
100 |
101 | ### Update Site Metadata
102 |
103 | You can update your site metadata in the next.config.js file.
104 |
105 | ```
106 | env: {
107 | siteTitle: 'Your Company',
108 | siteDescription: 'Your company description.',
109 | siteKeywords: 'your company keywords',
110 | siteUrl: 'https://doggystickers.xyz',
111 | siteImagePreviewUrl: '/images/main.jpg',
112 | twitterHandle: '@your_handle'
113 | }
114 | ```
115 |
116 | ### Update Colors
117 |
118 | You can update the color palette in tailwind.config.js file.
119 |
120 | ```
121 | colors: {
122 | palette: {
123 | lighter: '',
124 | light: '',
125 | primary: '',
126 | dark: '',
127 | },
128 | },
129 | ```
130 | ### Update Progressive Web App (PWA) data
131 |
132 | Update the manifest.json file and the icons under the public/images/icons folder.
133 |
134 | You can use free tools online such as https://realfavicongenerator.net/ to quickly generate all the different icon sizes and favicon.ico file.
135 |
136 | ### Deployment
137 |
138 | You can deploy this using any number of services. Vercel and Netlify are the ones I prefer and very easy to setup and sync with your Github repo.
139 |
140 | ### Credit
141 |
142 | The store was inspired by the awesome [Gatsby Swag Store](https://github.com/gatsbyjs/store.gatsbyjs.org "gatsby store") as well
143 | as countless other devs much more capable than me who put out their awesome work for free.
144 |
145 | ### License
146 |
147 | I have open sourced this code under the MIT License in the hope that if this helps people navigate their way around JAMStack eCommerce stores
148 | as the Gatsby Swag Store did for me when I first started out.
149 |
150 | ### Buy Me Coffee! :coffee:
151 |
152 | If you did find this useful and want to show your appreciation you can buy me a [coffee](https://www.buymeacoffee.com/neum "coffee") :smiley:
153 |
154 | You can also buy some Doggy Stickers from the [store](https://doggystickers.vercel.app/ "store")! :dog:
155 |
--------------------------------------------------------------------------------
/components/BackToProductButton.js:
--------------------------------------------------------------------------------
1 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
2 | import { faArrowLeft } from '@fortawesome/free-solid-svg-icons'
3 | import Link from 'next/link'
4 |
5 | function BackToProductButton() {
6 | return (
7 |
8 |
13 |
14 | Back To All Products
15 |
16 |
17 | )
18 | }
19 |
20 | export default BackToProductButton
21 |
--------------------------------------------------------------------------------
/components/CartTable.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 | import { useUpdateCartQuantityContext } from '@/context/Store'
3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
4 | import { faTimes } from '@fortawesome/free-solid-svg-icons'
5 | import Link from 'next/link'
6 | import Price from '@/components/Price'
7 | import { getCartSubTotal } from '@/utils/helpers'
8 |
9 | function CartTable({ cart }) {
10 | const updateCartQuantity = useUpdateCartQuantityContext()
11 | const [cartItems, setCartItems] = useState([])
12 | const [subtotal, setSubtotal] = useState(0)
13 |
14 | useEffect(() => {
15 | setCartItems(cart)
16 | setSubtotal(getCartSubTotal(cart))
17 | }, [cart])
18 |
19 | function updateItem(id, quantity) {
20 | updateCartQuantity(id, quantity)
21 | }
22 |
23 | return (
24 |
102 | )
103 | }
104 |
105 | export default CartTable
106 |
--------------------------------------------------------------------------------
/components/CheckOutButton.js:
--------------------------------------------------------------------------------
1 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
2 | import { faArrowRight } from '@fortawesome/free-solid-svg-icons'
3 |
4 | function CheckOutButton({ webUrl }) {
5 | return (
6 |
12 | Check Out
13 |
14 |
15 | )
16 | }
17 |
18 | export default CheckOutButton
19 |
--------------------------------------------------------------------------------
/components/Footer.js:
--------------------------------------------------------------------------------
1 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
2 | import { faHeart } from '@fortawesome/free-solid-svg-icons'
3 |
4 | function Footer() {
5 | return (
6 |
17 | )
18 | }
19 |
20 | export default Footer
21 |
--------------------------------------------------------------------------------
/components/Layout.js:
--------------------------------------------------------------------------------
1 | import { CartProvider } from '@/context/Store'
2 | import Nav from '@/components/Nav'
3 | import Footer from '@/components/Footer'
4 |
5 | function Layout({ children }) {
6 |
7 | return (
8 |
9 |
10 |
11 |
12 |
13 | {children}
14 |
15 |
16 |
17 |
18 |
19 | )
20 | }
21 |
22 | export default Layout
23 |
--------------------------------------------------------------------------------
/components/Nav.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 | import Link from 'next/link'
3 | import { useCartContext } from '@/context/Store'
4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
5 | import { faShoppingCart } from '@fortawesome/free-solid-svg-icons'
6 |
7 | function Nav() {
8 | const cart = useCartContext()[0]
9 | const [cartItems, setCartItems] = useState(0)
10 |
11 | useEffect(() => {
12 | let numItems = 0
13 | cart.forEach(item => {
14 | numItems += item.variantQuantity
15 | })
16 | setCartItems(numItems)
17 | }, [cart])
18 |
19 | return (
20 |
54 | )
55 | }
56 |
57 | export default Nav
58 |
--------------------------------------------------------------------------------
/components/PageTitle.js:
--------------------------------------------------------------------------------
1 | function PageTitle({ text }) {
2 | return (
3 |
4 | {text}
5 |
6 | )
7 | }
8 |
9 | export default PageTitle
10 |
--------------------------------------------------------------------------------
/components/Price.js:
--------------------------------------------------------------------------------
1 | function Price({currency, num, numSize }) {
2 | return (
3 | <>
4 | {currency}{num}
5 | >
6 | )
7 | }
8 |
9 | export default Price
10 |
--------------------------------------------------------------------------------
/components/ProductCard.js:
--------------------------------------------------------------------------------
1 | import Image from 'next/image'
2 | import Link from 'next/link'
3 | import Price from '@/components/Price'
4 |
5 | function ProductCard({ product }) {
6 | const handle = product.node.handle
7 | const title = product.node.title
8 | const description = product.node.description
9 | const price = product.node.variants.edges[0].node.price
10 |
11 | const imageNode = product.node.images.edges[0].node
12 |
13 | return (
14 |
18 |
19 |
20 |
26 |
27 |
28 |
29 | {title}
30 |
31 |
32 | {description}
33 |
34 |
44 |
45 |
46 |
47 | )
48 | }
49 |
50 | export default ProductCard
51 |
--------------------------------------------------------------------------------
/components/ProductDetails.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import BackToProductButton from '@/components/BackToProductButton'
3 | import ProductInfo from '@/components/ProductInfo'
4 | import ProductForm from '@/components/ProductForm'
5 |
6 | function ProductDetails({ productData }) {
7 | const [variantPrice, setVariantPrice] = useState(productData.variants.edges[0].node.price)
8 |
9 | return (
10 |
25 | )
26 | }
27 |
28 | export default ProductDetails
29 |
--------------------------------------------------------------------------------
/components/ProductForm.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
3 | import { faShoppingCart } from '@fortawesome/free-solid-svg-icons'
4 | import { useCartContext, useAddToCartContext } from '@/context/Store'
5 |
6 | function ProductForm({ title, handle, variants, setVariantPrice, mainImg }) {
7 | const [quantity, setQuantity] = useState(1)
8 | const [variantId, setVariantId] = useState(variants[0].node.id)
9 | const [variant, setVariant] = useState(variants[0])
10 | const isLoading = useCartContext()[2]
11 | const addToCart = useAddToCartContext()
12 |
13 | const atcBtnStyle = isLoading ?
14 | `pt-3 pb-2 bg-palette-primary text-white w-full mt-2 rounded-sm font-primary font-semibold text-xl flex
15 | justify-center items-baseline hover:bg-palette-dark opacity-25 cursor-none`
16 | :
17 | `pt-3 pb-2 bg-palette-primary text-white w-full mt-2 rounded-sm font-primary font-semibold text-xl flex
18 | justify-center items-baseline hover:bg-palette-dark`
19 |
20 | function handleSizeChange(e) {
21 | setVariantId(e)
22 | // send back size change
23 | const selectedVariant = variants.filter(v => v.node.id === e).pop()
24 | setVariantPrice(selectedVariant.node.price)
25 |
26 | // update variant
27 | setVariant(selectedVariant)
28 | }
29 |
30 | async function handleAddToCart() {
31 | const varId = variant.node.id
32 | // update store context
33 | if (quantity !== '') {
34 | addToCart({
35 | productTitle: title,
36 | productHandle: handle,
37 | productImage: mainImg,
38 | variantId: varId,
39 | variantPrice: variant.node.price,
40 | variantTitle: variant.node.title,
41 | variantQuantity: quantity
42 | })
43 | }
44 | }
45 |
46 | function updateQuantity(e) {
47 | if (e === '') {
48 | setQuantity('')
49 | } else {
50 | setQuantity(Math.floor(e))
51 | }
52 | }
53 |
54 | return (
55 |
56 |
57 |
58 | Qty.
59 | updateQuantity(e.target.value)}
68 | className="text-gray-900 form-input border border-gray-300 w-16 rounded-sm focus:border-palette-light focus:ring-palette-light"
69 | />
70 |
71 |
72 | Size
73 | handleSizeChange(event.target.value)}
77 | value={variantId}
78 | className="form-select border border-gray-300 rounded-sm w-full text-gray-900 focus:border-palette-light focus:ring-palette-light"
79 | >
80 | {
81 | variants.map(item => (
82 |
87 | {item.node.title}
88 |
89 | ))
90 | }
91 |
92 |
93 |
94 |
99 | Add To Cart
100 |
101 |
102 |
103 | )
104 | }
105 |
106 | export default ProductForm
107 |
--------------------------------------------------------------------------------
/components/ProductImage.js:
--------------------------------------------------------------------------------
1 | import { useState, useRef } from 'react'
2 | import Image from 'next/image'
3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
4 | import { faArrowLeft, faArrowRight } from '@fortawesome/free-solid-svg-icons'
5 |
6 | function ProductImage({ images }) {
7 | const [mainImg, setMainImg] = useState(images[0].node)
8 | const ref = useRef()
9 |
10 | function scroll(scrollOffset) {
11 | ref.current.scrollLeft += scrollOffset
12 | }
13 |
14 | return (
15 |
16 |
17 |
23 |
24 |
25 |
scroll(-300)}
29 | >
30 |
31 |
32 |
37 | {
38 | images.map((imgItem, index) => (
39 | setMainImg(imgItem.node)}
43 | >
44 |
50 |
51 | ))
52 | }
53 |
54 |
scroll(300)}
58 | >
59 |
60 |
61 |
62 |
63 | )
64 | }
65 |
66 | export default ProductImage
67 |
--------------------------------------------------------------------------------
/components/ProductInfo.js:
--------------------------------------------------------------------------------
1 | import Price from '@/components/Price'
2 |
3 | function ProductInfo({ title, description, price }) {
4 | return (
5 |
6 |
7 | {title}
8 |
9 |
10 | {description}
11 |
12 |
19 |
20 | )
21 | }
22 |
23 | export default ProductInfo
24 |
--------------------------------------------------------------------------------
/components/ProductListings.js:
--------------------------------------------------------------------------------
1 | import ProductCard from '@/components/ProductCard'
2 |
3 | function ProductListings({ products }) {
4 | return (
5 |
6 | {
7 | products.map((product, index) => (
8 |
9 | ))
10 | }
11 |
12 | )
13 | }
14 |
15 | export default ProductListings
16 |
--------------------------------------------------------------------------------
/components/ProductSection.js:
--------------------------------------------------------------------------------
1 | import ProductImage from '@/components/ProductImage'
2 | import ProductDetails from '@/components/ProductDetails'
3 |
4 | function ProductSection({ productData }) {
5 | return (
6 |
10 | )
11 | }
12 |
13 | export default ProductSection
14 |
--------------------------------------------------------------------------------
/components/SEO.js:
--------------------------------------------------------------------------------
1 | import Head from 'next/head'
2 |
3 | function SEO({ title }) {
4 | // customize meta properties
5 | // you can pass them as an argument like title in case you want to change for each page
6 | const description = process.env.siteDescription
7 | const keywords = process.env.siteKeywords
8 | const siteURL = process.env.siteUrl
9 | const twitterHandle = process.env.twitterHandle
10 | const imagePreview = `${siteURL}/${process.env.siteImagePreviewUrl}`
11 |
12 | return (
13 |
14 |
15 |
16 |
17 |
18 | {/* Twitter */}
19 |
20 |
21 |
22 | {/* Open Graph */}
23 |
24 |
25 |
26 |
27 |
28 | {title}
29 |
30 |
31 |
38 |
45 |
46 |
47 |
48 | )
49 | }
50 |
51 | export default SEO
52 |
--------------------------------------------------------------------------------
/components/StoreHeading.js:
--------------------------------------------------------------------------------
1 | import PageTitle from '@/components/PageTitle'
2 |
3 | function StoreHeading() {
4 | return (
5 |
6 |
7 |
8 | Times are tough. Liven up your home with some cute Doggy Stickers. 🐶
9 |
10 |
11 | )
12 | }
13 |
14 | export default StoreHeading
15 |
--------------------------------------------------------------------------------
/context/Store.js:
--------------------------------------------------------------------------------
1 | import { createContext, useContext, useState, useEffect } from 'react'
2 | import { createShopifyCheckout, updateShopifyCheckout, setLocalData, saveLocalData } from '@/utils/helpers'
3 |
4 | const CartContext = createContext()
5 | const AddToCartContext = createContext()
6 | const UpdateCartQuantityContext = createContext()
7 |
8 | export function useCartContext() {
9 | return useContext(CartContext)
10 | }
11 |
12 | export function useAddToCartContext() {
13 | return useContext(AddToCartContext)
14 | }
15 |
16 | export function useUpdateCartQuantityContext() {
17 | return useContext(UpdateCartQuantityContext)
18 | }
19 |
20 | export function CartProvider({ children }) {
21 | const [cart, setCart] = useState([])
22 | const [checkoutId, setCheckoutId] = useState('')
23 | const [checkoutUrl, setCheckoutUrl] = useState('')
24 | const [isLoading, setisLoading] = useState(false)
25 |
26 | useEffect(() => {
27 | setLocalData(setCart, setCheckoutId, setCheckoutUrl)
28 | }, [])
29 |
30 | useEffect(() => {
31 | // do this to make sure multiple tabs are always in sync
32 | const onReceiveMessage = (e) => {
33 | console.log(e)
34 | setLocalData(setCart, setCheckoutId, setCheckoutUrl)
35 | }
36 |
37 | window.addEventListener("storage", onReceiveMessage);
38 | return () => {
39 | window.removeEventListener("storage", onReceiveMessage);
40 | }
41 | }, [])
42 |
43 | async function addToCart(newItem) {
44 | setisLoading(true)
45 | // empty cart
46 | if (cart.length === 0) {
47 | setCart([
48 | ...cart,
49 | newItem
50 | ])
51 |
52 | const response = await createShopifyCheckout(newItem)
53 | setCheckoutId(response.id)
54 | setCheckoutUrl(response.webUrl)
55 | saveLocalData(newItem, response.id, response.webUrl)
56 |
57 | } else {
58 | let newCart = [...cart]
59 | let itemAdded = false
60 | // loop through all cart items to check if variant
61 | // already exists and update quantity
62 | newCart.map(item => {
63 | if (item.variantId === newItem.variantId) {
64 | item.variantQuantity += newItem.variantQuantity
65 | itemAdded = true
66 | }
67 | })
68 |
69 | let newCartWithItem = [...newCart]
70 | if (itemAdded) {
71 | } else {
72 | // if its a new item than add it to the end
73 | newCartWithItem = [...newCart, newItem]
74 | }
75 |
76 | setCart(newCartWithItem)
77 | await updateShopifyCheckout(newCartWithItem, checkoutId)
78 | saveLocalData(newCartWithItem, checkoutId, checkoutUrl)
79 | }
80 | setisLoading(false)
81 | }
82 |
83 | async function updateCartItemQuantity(id, quantity) {
84 | setisLoading(true)
85 | let newQuantity = Math.floor(quantity)
86 | if (quantity === '') {
87 | newQuantity = ''
88 | }
89 | let newCart = [...cart]
90 | newCart.forEach(item => {
91 | if (item.variantId === id) {
92 | item.variantQuantity = newQuantity
93 | }
94 | })
95 |
96 | // take out zeroes items
97 | newCart = newCart.filter(i => i.variantQuantity !== 0)
98 | setCart(newCart)
99 |
100 | await updateShopifyCheckout(newCart, checkoutId)
101 | saveLocalData(newCart, checkoutId, checkoutUrl)
102 | setisLoading(false)
103 | }
104 |
105 | return (
106 |
107 |
108 |
109 | {children}
110 |
111 |
112 |
113 | )
114 | }
115 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "@/components/*": ["components/*"],
6 | "@/lib/*": ["lib/*"],
7 | "@/styles/*": ["styles/*"],
8 | "@/context/*": ["context/*"],
9 | "@/utils/*": ["utils/*"],
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/lib/shopify.js:
--------------------------------------------------------------------------------
1 | const domain = process.env.NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN
2 | const storefrontAccessToken = process.env.NEXT_PUBLIC_SHOPIFY_STORE_FRONT_ACCESS_TOKEN
3 | const collection = process.env.NEXT_PUBLIC_SHOPIFY_COLLECTION
4 |
5 | async function callShopify(query) {
6 | const fetchUrl = `https://${domain}/api/2021-01/graphql.json`;
7 |
8 | const fetchOptions = {
9 | endpoint: fetchUrl,
10 | method: "POST",
11 | headers: {
12 | "X-Shopify-Storefront-Access-Token": storefrontAccessToken,
13 | "Accept": "application/json",
14 | "Content-Type": "application/json",
15 | },
16 | body: JSON.stringify({ query }),
17 | };
18 |
19 | try {
20 | const data = await fetch(fetchUrl, fetchOptions).then((response) =>
21 | response.json(),
22 | );
23 | return data;
24 | } catch (error) {
25 | throw new Error("Could not fetch products!");
26 | }
27 | }
28 |
29 | export async function getAllProductsInCollection() {
30 | const query =
31 | `{
32 | collectionByHandle(handle: "${collection}") {
33 | id
34 | title
35 | products(first: 250) {
36 | edges {
37 | node {
38 | id
39 | title
40 | description
41 | handle
42 | images(first: 250) {
43 | edges {
44 | node {
45 | id
46 | originalSrc
47 | height
48 | width
49 | altText
50 | }
51 | }
52 | }
53 | variants(first: 250) {
54 | edges {
55 | node {
56 | id
57 | title
58 | price
59 | }
60 | }
61 | }
62 | }
63 | }
64 | }
65 | }
66 | }`
67 | ;
68 |
69 | const response = await callShopify(query);
70 |
71 | const allProducts = response.data.collectionByHandle.products.edges
72 | ? response.data.collectionByHandle.products.edges
73 | : [];
74 |
75 | return allProducts;
76 | }
77 |
78 | export async function getProductSlugs() {
79 | const query =
80 | `{
81 | collectionByHandle(handle: "${collection}") {
82 | products(first: 250) {
83 | edges {
84 | node {
85 | handle
86 | }
87 | }
88 | }
89 | }
90 | }`
91 | ;
92 |
93 | const response = await callShopify(query);
94 |
95 | const slugs = response.data.collectionByHandle.products.edges
96 | ? response.data.collectionByHandle.products.edges
97 | : [];
98 |
99 | return slugs;
100 | }
101 |
102 | export async function getProduct(handle) {
103 | const query =
104 | `{
105 | productByHandle(handle: "${handle}") {
106 | id
107 | title
108 | handle
109 | description
110 | images(first: 250) {
111 | edges {
112 | node {
113 | id
114 | originalSrc
115 | height
116 | width
117 | altText
118 | }
119 | }
120 | }
121 | variants(first: 250) {
122 | edges {
123 | node {
124 | id
125 | title
126 | price
127 | }
128 | }
129 | }
130 | }
131 | }`
132 | ;
133 |
134 | const response = await callShopify(query);
135 |
136 | const product = response.data.productByHandle
137 | ? response.data.productByHandle
138 | : [];
139 |
140 | return product;
141 | }
142 |
143 | export async function createCheckout(id, quantity) {
144 | const query =
145 | `mutation
146 | {
147 | checkoutCreate(input: {
148 | lineItems: [{ variantId: "${id}", quantity: ${quantity} }]
149 | }) {
150 | checkout {
151 | id
152 | webUrl
153 | lineItems(first: 250) {
154 | edges {
155 | node {
156 | id
157 | title
158 | quantity
159 | }
160 | }
161 | }
162 | }
163 | }
164 | }
165 | `
166 | ;
167 |
168 | const response = await callShopify(query);
169 |
170 | const checkout = response.data.checkoutCreate.checkout
171 | ? response.data.checkoutCreate.checkout
172 | : [];
173 |
174 | return checkout;
175 | }
176 |
177 | export async function updateCheckout(id, lineItems) {
178 | const formattedLineItems = lineItems.map(item => {
179 | return `{
180 | variantId: "${item.variantId}",
181 | quantity:${item.quantity}
182 | }`
183 | })
184 |
185 | const query =
186 | `mutation
187 | {
188 | checkoutLineItemsReplace(lineItems: [${formattedLineItems}], checkoutId: "${id}") {
189 | checkout {
190 | id
191 | webUrl
192 | lineItems(first: 250) {
193 | edges {
194 | node {
195 | id
196 | title
197 | quantity
198 | }
199 | }
200 | }
201 | }
202 | }
203 | }
204 | `
205 | ;
206 |
207 | const response = await callShopify(query);
208 |
209 | const checkout = response.data.checkoutLineItemsReplace.checkout
210 | ? response.data.checkoutLineItemsReplace.checkout
211 | : [];
212 |
213 | return checkout;
214 | }
215 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | const withPWA = require('next-pwa');
2 |
3 | module.exports = withPWA({
4 | future: { webpack5: true },
5 | pwa: {
6 | dest: 'public',
7 | disable: process.env.NODE_ENV === 'development',
8 | },
9 | env: {
10 | siteTitle: 'Doggy Stickers',
11 | siteDescription: 'Get some Doggy Stickers!',
12 | siteKeywords: 'dog, stickers, fun',
13 | siteUrl: 'https://www.doggystickers.xyz',
14 | siteImagePreviewUrl: '/images/main.jpg',
15 | twitterHandle: '@deepwhitman'
16 | },
17 | images: {
18 | domains: ['cdn.shopify.com'],
19 | },
20 | })
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "with-tailwindcss",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start"
9 | },
10 | "dependencies": {
11 | "@fortawesome/fontawesome-svg-core": "^1.2.34",
12 | "@fortawesome/free-brands-svg-icons": "^5.15.2",
13 | "@fortawesome/free-solid-svg-icons": "^5.15.2",
14 | "@fortawesome/react-fontawesome": "^0.1.14",
15 | "@tailwindcss/forms": "^0.3.2",
16 | "autoprefixer": "^10.0.4",
17 | "next": "latest",
18 | "next-pwa": "^5.0.5",
19 | "postcss": "^8.1.10",
20 | "react": "^17.0.1",
21 | "react-dom": "^17.0.1",
22 | "tailwindcss": "^2.0.2",
23 | "webpack": "^5.24.2"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | import Layout from '@/components/Layout'
2 | import SEO from '@/components/SEO'
3 | import '@/styles/globals.css'
4 |
5 | function MyApp({ Component, pageProps }) {
6 |
7 | return (
8 |
9 |
12 |
13 |
14 | )
15 | }
16 |
17 | export default MyApp
18 |
--------------------------------------------------------------------------------
/pages/_document.js:
--------------------------------------------------------------------------------
1 | import Document, { Html, Head, Main, NextScript } from 'next/document'
2 |
3 | class MyDocument extends Document {
4 | static async getInitialProps(ctx) {
5 | const initialProps = await Document.getInitialProps(ctx)
6 | return { ...initialProps }
7 | }
8 |
9 | render() {
10 | return (
11 |
12 |
14 |
15 |
16 |
17 |
18 | )
19 | }
20 | }
21 |
22 | export default MyDocument
--------------------------------------------------------------------------------
/pages/cart.js:
--------------------------------------------------------------------------------
1 | import SEO from '@/components/SEO'
2 | import PageTitle from '@/components/PageTitle'
3 | import CartTable from '@/components/CartTable'
4 | import CheckOutButton from '@/components/CheckOutButton'
5 | import BackToProductButton from '@/components/BackToProductButton'
6 | import { useCartContext } from '@/context/Store'
7 |
8 | function CartPage() {
9 | const pageTitle = `Cart | ${process.env.siteTitle}`
10 | const [cart, checkoutUrl] = useCartContext()
11 |
12 | return (
13 |
14 |
15 |
16 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | )
26 | }
27 |
28 | export default CartPage
29 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import StoreHeading from '@/components/StoreHeading'
2 | import ProductListings from '@/components/ProductListings'
3 | import { getAllProductsInCollection } from '@/lib/shopify'
4 |
5 | function IndexPage({ products }) {
6 |
7 | return (
8 |
12 | )
13 | }
14 |
15 | export async function getStaticProps() {
16 | const products = await getAllProductsInCollection()
17 |
18 | return {
19 | props: {
20 | products
21 | },
22 | }
23 | }
24 |
25 | export default IndexPage
26 |
--------------------------------------------------------------------------------
/pages/products/[product].js:
--------------------------------------------------------------------------------
1 | import { getProductSlugs, getProduct } from '@/lib/shopify'
2 | import ProductSection from '@/components/ProductSection'
3 |
4 | function ProductPage({ productData }) {
5 |
6 | return (
7 |
10 | )
11 | }
12 |
13 | export async function getStaticPaths() {
14 | const productSlugs = await getProductSlugs()
15 |
16 | const paths = productSlugs.map((slug) => {
17 | const product = String(slug.node.handle)
18 | return {
19 | params: { product }
20 | }
21 | })
22 |
23 | return {
24 | paths,
25 | fallback: false,
26 | }
27 | }
28 |
29 | export async function getStaticProps({ params }) {
30 | const productData = await getProduct(params.product)
31 |
32 | return {
33 | props: {
34 | productData,
35 | },
36 | }
37 | }
38 |
39 | export default ProductPage
40 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | // If you want to use other PostCSS plugins, see the following:
2 | // https://tailwindcss.com/docs/using-with-preprocessors
3 | module.exports = {
4 | plugins: {
5 | tailwindcss: {},
6 | autoprefixer: {},
7 | },
8 | }
9 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wilmerdev0127/nextjs-shopify-starter/261662009a5684c9de0e962f0903781f44cdaeaa/public/favicon.ico
--------------------------------------------------------------------------------
/public/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/public/icons/apple-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wilmerdev0127/nextjs-shopify-starter/261662009a5684c9de0e962f0903781f44cdaeaa/public/icons/apple-icon.png
--------------------------------------------------------------------------------
/public/icons/icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wilmerdev0127/nextjs-shopify-starter/261662009a5684c9de0e962f0903781f44cdaeaa/public/icons/icon-144x144.png
--------------------------------------------------------------------------------
/public/icons/icon-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wilmerdev0127/nextjs-shopify-starter/261662009a5684c9de0e962f0903781f44cdaeaa/public/icons/icon-150x150.png
--------------------------------------------------------------------------------
/public/icons/icon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wilmerdev0127/nextjs-shopify-starter/261662009a5684c9de0e962f0903781f44cdaeaa/public/icons/icon-16x16.png
--------------------------------------------------------------------------------
/public/icons/icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wilmerdev0127/nextjs-shopify-starter/261662009a5684c9de0e962f0903781f44cdaeaa/public/icons/icon-192x192.png
--------------------------------------------------------------------------------
/public/icons/icon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wilmerdev0127/nextjs-shopify-starter/261662009a5684c9de0e962f0903781f44cdaeaa/public/icons/icon-32x32.png
--------------------------------------------------------------------------------
/public/icons/icon-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wilmerdev0127/nextjs-shopify-starter/261662009a5684c9de0e962f0903781f44cdaeaa/public/icons/icon-512x512.png
--------------------------------------------------------------------------------
/public/icons/icon-70x70.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wilmerdev0127/nextjs-shopify-starter/261662009a5684c9de0e962f0903781f44cdaeaa/public/icons/icon-70x70.png
--------------------------------------------------------------------------------
/public/images/demo-store.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wilmerdev0127/nextjs-shopify-starter/261662009a5684c9de0e962f0903781f44cdaeaa/public/images/demo-store.gif
--------------------------------------------------------------------------------
/public/images/desktop-lighthouse.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wilmerdev0127/nextjs-shopify-starter/261662009a5684c9de0e962f0903781f44cdaeaa/public/images/desktop-lighthouse.png
--------------------------------------------------------------------------------
/public/images/main.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wilmerdev0127/nextjs-shopify-starter/261662009a5684c9de0e962f0903781f44cdaeaa/public/images/main.jpg
--------------------------------------------------------------------------------
/public/images/mobile-lighthouse.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wilmerdev0127/nextjs-shopify-starter/261662009a5684c9de0e962f0903781f44cdaeaa/public/images/mobile-lighthouse.png
--------------------------------------------------------------------------------
/public/images/responsive-cart.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wilmerdev0127/nextjs-shopify-starter/261662009a5684c9de0e962f0903781f44cdaeaa/public/images/responsive-cart.gif
--------------------------------------------------------------------------------
/public/images/responsive-main.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wilmerdev0127/nextjs-shopify-starter/261662009a5684c9de0e962f0903781f44cdaeaa/public/images/responsive-main.gif
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Doggy Stickers",
3 | "short_name": "Get some Doggy Stickers!",
4 | "theme_color": "#ffffff",
5 | "background_color": "#EF4444",
6 | "display": "standalone",
7 | "orientation": "portrait",
8 | "scope": "/",
9 | "start_url": "/",
10 | "icons": [
11 | {
12 | "src": "icons/icon-70x70.png",
13 | "sizes": "70x70",
14 | "type": "image/png",
15 | "purpose": "any maskable"
16 | },
17 | {
18 | "src": "icons/icon-144x144.png",
19 | "sizes": "144x144",
20 | "type": "image/png",
21 | "purpose": "any maskable"
22 | },
23 | {
24 | "src": "icons/icon-150x150.png",
25 | "sizes": "150x150",
26 | "type": "image/png",
27 | "purpose": "any maskable"
28 | },
29 | {
30 | "src": "icons/icon-192x192.png",
31 | "sizes": "192x192",
32 | "type": "image/png",
33 | "purpose": "any maskable"
34 | },
35 | {
36 | "src": "icons/icon-512x512.png",
37 | "sizes": "512x512",
38 | "type": "image/png",
39 | "purpose": "any maskable"
40 | }
41 | ],
42 | "splash_pages": null
43 | }
44 |
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Josefin+Sans:wght@300;400;600;700&display=swap');
2 |
3 | @tailwind base;
4 | @tailwind components;
5 | @tailwind utilities;
6 |
7 | .triangle {
8 |
9 | }
10 |
11 | .triangle::before {
12 | content: '';
13 | height: 100%;
14 | position: absolute;
15 | top: 0;
16 | left: 0em;
17 | border: 1em solid transparent;
18 | border-left-color: #ffffff;
19 | }
20 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
3 | darkMode: false, // or 'media' or 'class'
4 | theme: {
5 | extend: {
6 | height: theme => ({
7 | '112': '28rem',
8 | '120': '30rem',
9 | }),
10 | minHeight: theme => ({
11 | '80': '20rem',
12 | }),
13 | colors: {
14 | palette: {
15 | lighter: '#F5F3FF',
16 | light: '#DDD6FE',
17 | primary: '#5B21B6',
18 | dark: '#4C1D95',
19 | },
20 | },
21 | fontFamily: {
22 | primary: ['"Josefin Sans"']
23 | }
24 | },
25 | },
26 | variants: {
27 | extend: {},
28 | },
29 | plugins: [
30 | require("@tailwindcss/forms")({
31 | strategy: 'class',
32 | }),
33 | ],
34 | }
35 |
--------------------------------------------------------------------------------
/utils/helpers.js:
--------------------------------------------------------------------------------
1 | import { createCheckout, updateCheckout } from '@/lib/shopify'
2 |
3 | export function saveLocalData(cart, checkoutId, checkoutUrl) {
4 | localStorage.setItem(process.env.NEXT_PUBLIC_LOCAL_STORAGE_NAME, JSON.stringify([cart, checkoutId, checkoutUrl]))
5 | }
6 |
7 | function getLocalData() {
8 | return JSON.parse(localStorage.getItem(process.env.NEXT_PUBLIC_LOCAL_STORAGE_NAME))
9 | }
10 |
11 | export function setLocalData(setCart, setCheckoutId, setCheckoutUrl) {
12 | const localData = getLocalData()
13 |
14 | if (localData) {
15 | if (Array.isArray(localData[0])) {
16 | setCart([...localData[0]])
17 | }
18 | else {
19 | setCart([localData[0]])
20 | }
21 | setCheckoutId(localData[1])
22 | setCheckoutUrl(localData[2])
23 | }
24 | }
25 |
26 | export async function createShopifyCheckout(newItem) {
27 | const data = await createCheckout( newItem['variantId'], newItem['variantQuantity'])
28 | return data
29 | }
30 |
31 | export async function updateShopifyCheckout(updatedCart, checkoutId) {
32 | const lineItems = updatedCart.map(item => {
33 | return {
34 | variantId: item['variantId'],
35 | quantity: item['variantQuantity']
36 | }
37 | })
38 | await updateCheckout(checkoutId, lineItems)
39 | }
40 |
41 | export function getCartSubTotal(cart) {
42 | if (cart.length === 0) {
43 | return 0
44 | }
45 | else {
46 | let totalPrice = 0
47 | cart.forEach(item => totalPrice += parseInt(item.variantQuantity) * parseFloat(item.variantPrice))
48 | return Math.round(totalPrice * 100) / 100
49 | }
50 | }
--------------------------------------------------------------------------------