├── .env-example ├── .gitignore ├── README.md ├── demos ├── Checkout-page.gif ├── home-demo.gif ├── order-received-demo.gif ├── paypal-payment-demo.gif └── stripe-demo.gif ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.js ├── api │ ├── create-order.js │ ├── get-stripe-session.js │ ├── stripe-web-hook.js │ └── stripe │ │ └── [...nextstripe].js ├── cart.js ├── categories.js ├── category │ └── [slug].js ├── checkout.js ├── index.js ├── product │ └── [slug].js └── thank-you.js ├── postcss.config.js ├── public └── cart-spinner.gif ├── src ├── components │ ├── ApolloClient.js │ ├── Footer.js │ ├── Header.js │ ├── Layout.js │ ├── Nav.js │ ├── Product.js │ ├── cart │ │ ├── AddToCartButton.js │ │ ├── CartIcon.js │ │ └── cart-page │ │ │ ├── CartItem.js │ │ │ └── CartItemsContainer.js │ ├── category │ │ └── category-block │ │ │ ├── ParentCategoriesBlock.js │ │ │ └── ParentCategoryBlock.js │ ├── checkout │ │ ├── Address.js │ │ ├── CheckoutCartItem.js │ │ ├── CheckoutForm.js │ │ ├── CountrySelection.js │ │ ├── Error.js │ │ ├── OrderSuccess.js │ │ ├── PaymentModes.js │ │ ├── StatesSelection.js │ │ ├── YourOrder.js │ │ └── form-elements │ │ │ ├── Abbr.js │ │ │ ├── CheckboxField.js │ │ │ └── InputField.js │ ├── context │ │ └── AppContext.js │ ├── home │ │ └── hero-carousel │ │ │ └── index.js │ ├── icons │ │ ├── ArrowDown.js │ │ ├── Cross.js │ │ ├── Facebook.js │ │ ├── Instagram.js │ │ ├── Loading.js │ │ ├── ShoppingCart.js │ │ ├── Twitter.js │ │ ├── Youtube.js │ │ ├── index.js │ │ └── svgs │ │ │ ├── arrow-down.svg │ │ │ ├── cross.svg │ │ │ ├── facebook.svg │ │ │ ├── instagram.svg │ │ │ ├── shopping-cart.svg │ │ │ ├── twitter.svg │ │ │ └── youtube.svg │ └── single-product │ │ ├── gallery-carousel │ │ └── index.js │ │ └── price │ │ └── index.js ├── constants │ └── urls.js ├── functions.js ├── image │ └── index.js ├── img │ └── girls.jpg ├── mutations │ ├── add-to-cart.js │ ├── checkout.js │ ├── clear-cart.js │ └── update-cart.js ├── queries │ ├── get-cart.js │ ├── get-categories.js │ ├── get-countries.js │ ├── get-order.js │ ├── get-states.js │ ├── product-and-categories.js │ ├── product-by-category.js │ └── product-by-slug.js ├── styles │ ├── main.scss │ └── style.scss ├── utils │ ├── cart.js │ ├── checkout.js │ └── order.js └── validator │ ├── checkout.js │ └── isEmpty.js ├── tailwind.config.js ├── wordpress └── plugins │ ├── headless-cms.zip │ ├── wp-graphql-woocommerce.zip │ └── wp-graphql.zip └── yarn.lock /.env-example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_WORDPRESS_URL=https://example.com 2 | 3 | NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxx 4 | STRIPE_SECRET_KEY=sk_test_xxx 5 | 6 | STRIPE_WEBHOOK_ENDPOINT_SECRET=whsec_xxxx 7 | 8 | WC_CONSUMER_KEY=ck_xxx 9 | WC_CONSUMER_SECRET=cs_xxx 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.log 3 | npm-debug.log 4 | yarn-error.log 5 | .DS_Store 6 | build/ 7 | node_modules/ 8 | dist/ 9 | .cache 10 | .next 11 | .env 12 | .now 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [WooCommerce Nextjs React Theme](https://woo-next-imranhsayed.vercel.app/) :rocket: 2 | [![Project Status: Active.](https://www.repostatus.org/badges/latest/active.svg)](https://www.repostatus.org/#active) 3 | ![Stars](https://img.shields.io/github/stars/imranhsayed/woo-next?label=%E2%AD%90%20Stars) 4 | ![Forks](https://img.shields.io/github/forks/imranhsayed/woo-next?color=%23ff69b4) 5 | ![Contributors](https://img.shields.io/github/contributors/imranhsayed/woo-next?color=blue) 6 | ![Follow](https://img.shields.io/github/followers/imranhsayed?label=Please%20follow%20%20to%20support%20my%20work%20%F0%9F%99%8F&style=social) 7 | 8 | > * This is a React WooCommerce theme, built with Next JS, Webpack, Babel, Node, GraphQl 9 | 10 | 📹 [Full Course Video Tutorial](https://codeytek.com/course/woocommerce-with-react-course/) 11 | 12 | ## Important Note ⭐: 13 | The code for the Video Tutorial is in the branch [youtube-course](https://github.com/imranhsayed/woo-next/tree/youtube-course) 14 | However, since there is continuous contribution and updates to this project. The 'master' 15 | branch is upto date with those changes. You can also refer to the PR notes for changes. 16 | 17 | ## Demo Desktop :video_camera: 18 | 19 | ![](demos/home-demo.gif) 20 | 21 | ## Live Demo: 22 | 23 | [Live Demo Site](https://woo-next-imranhsayed.vercel.app/) 24 | 25 | ## Checkout Page Demo 26 | ![](demos/Checkout-page.gif) 27 | 28 | ## Payment Demo ( Paypal example ) 29 | ![](demos/paypal-payment-demo.gif) 30 | 31 | ## Order Received Demo 32 | ![](demos/order-received-demo.gif) 33 | 34 | ## Stripe Checkout 35 | [Stripe Demo Video](https://youtu.be/i75_Vtx-CnA) 36 | ![](demos/stripe-demo.gif) 37 | 38 | # Features: 39 | 40 | 1. WooCommerce Store in React( contains: Products Page, Single Product Page, AddToCart, CartPage and Checkout Page with country selection ). 41 | 2. SSR 42 | 3. SEO friendly 43 | 4. Automatic Code Splitting 44 | 5. Hot Reloading 45 | 6. Prefetching 46 | 8. Incremental Static (Re)generation ( Next.js 10 support ) 47 | 9. GraphQL with Apollo Client 48 | 10. Tailwindcss 49 | 11. Stripe Checkout ( with Stripe Session and Stripe webhook) 50 | 51 | ## Getting Started :rocket: 52 | 53 | These instructions will get you a copy of the project up and running on your local machine for development purposes. 54 | 55 | ### Prerequisites :page_facing_up: 56 | 57 | ### Installing :wrench: 58 | 59 | 1. Clone this repo using terminal `git clone git@github.com:imranhsayed/woo-next` 60 | 2. `cd woo-next` 61 | 3. `yarn install` 62 | 63 | ## Add GraphQl support for WordPress 64 | 65 | 1. Download and activate the following plugins , in your WordPress plugin directory: 66 | 67 | * [wp-graphql](https://github.com/imranhsayed/woo-next/tree/master/wordpress/plugins) Exposes graphql for WordPress ( **Tested with v-1.3.8** of this plugin ) 68 | * [wp-graphql-woocommerce](https://github.com/imranhsayed/woo-next/tree/master/wordpress/plugins) Adds Woocommerce functionality to a WPGraphQL schema ( **Tested with v-0.8.1** of this plugin ) 69 | * [headless-cms](https://github.com/imranhsayed/woo-next/tree/master/wordpress/plugins) Extends WPGraphQL Schema ( **Tested with v-1.8.0** of this plugin ) 70 | 71 | * Make sure Woocommerce plugin is also installed in your WordPress site. You can also import default wooCommerce products that come with wooCommerce Plugin for development ( if you don't have any products in your WordPress install ) `WP Dashboard > Tools > Import > WooCommerce products(CSV)`: The WooCommerce default products csv file is available at `wp-content/plugins/woocommerce/sample-data/sample_products.csv` 72 | 73 | ## Hero Carousel. 74 | To use Hero carousel, create a category called 'offers' from WordPress Dashboard > Products > Categories. 75 | Now create and assign as many child categories to this parent 'offers' category with name, description and image. 76 | These Child categories data will automatically be used to create hero carousel on the frontend. 77 | 78 | 79 | ## Configuration(for GraphQL implementation) :wrench: 80 | 81 | * _Note_ Below is for GraphQL implementation , for REST API check [feature/rest-api](https://github.com/imranhsayed/woo-next/tree/feature/rest-api) branch 82 | 83 | 1. (Required) Create a `.env` file taking reference from `.env-example` and update your WordPressSite URL. 84 | - `NEXT_PUBLIC_WORDPRESS_URL=https://example.com` 85 | 86 | ## Branch details 87 | 88 | 1. [feature/rest-api](https://github.com/imranhsayed/woo-next/tree/feature/rest-api) Contains REST API Implementation. 89 | 90 | 2. The `master` branch has the GraphQL implementation. 91 | 92 | ## Common Commands :computer: 93 | 94 | * `dev` Runs server in development mode 95 | 96 | ## Code Contributors ✰ 97 | 98 | Thanks to all the people who contributed to the code of this project 🤝 99 | 100 |
101 | Imran Sayed 102 | Daniel F 103 | Fandi Rahmawan 104 | yudyananda 105 |
106 | 107 | 108 | ## Contributing :busts_in_silhouette: 109 | 110 | Please read [CONTRIBUTING.md](https://gist.github.com/PurpleBooth/b24679402957c63ec426) for details on our code of conduct, and the process for submitting pull requests to us. 111 | 112 | ## Versioning :bookmark_tabs: 113 | 114 | I use [Git](https://github.com/) for versioning. 115 | 116 | ## Author :bust_in_silhouette: 117 | 118 | * **[Imran Sayed](https://twitter.com/imranhsayed)** 119 | 120 | ## License :page_with_curl: 121 | 122 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details 123 | -------------------------------------------------------------------------------- /demos/Checkout-page.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imranhsayed/woo-next/9a69b5710e9b5b470832d20f106e16d2d75f60ae/demos/Checkout-page.gif -------------------------------------------------------------------------------- /demos/home-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imranhsayed/woo-next/9a69b5710e9b5b470832d20f106e16d2d75f60ae/demos/home-demo.gif -------------------------------------------------------------------------------- /demos/order-received-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imranhsayed/woo-next/9a69b5710e9b5b470832d20f106e16d2d75f60ae/demos/order-received-demo.gif -------------------------------------------------------------------------------- /demos/paypal-payment-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imranhsayed/woo-next/9a69b5710e9b5b470832d20f106e16d2d75f60ae/demos/paypal-payment-demo.gif -------------------------------------------------------------------------------- /demos/stripe-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imranhsayed/woo-next/9a69b5710e9b5b470832d20f106e16d2d75f60ae/demos/stripe-demo.gif -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const allowedImageWordPressDomain = new URL(process.env.NEXT_PUBLIC_WORDPRESS_URL).hostname 3 | 4 | module.exports = { 5 | trailingSlash: true, 6 | webpackDevMiddleware: (config) => { 7 | config.watchOptions = { 8 | poll: 1000, 9 | aggregateTimeout: 300, 10 | }; 11 | 12 | return config; 13 | }, 14 | sassOptions: { 15 | includePaths: [path.join(__dirname, "styles")], 16 | }, 17 | /** 18 | * We specify which domains are allowed to be optimized. 19 | * This is needed to ensure that external urls can't be abused. 20 | * @see https://nextjs.org/docs/basic-features/image-optimization#domains 21 | */ 22 | images: { 23 | domains: [ allowedImageWordPressDomain, 'via.placeholder.com' ], 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "woo-next", 3 | "version": "2.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "svg": "svgr -d src/components/icons src/components/icons/svgs" 10 | }, 11 | "dependencies": { 12 | "@apollo/client": "^3.3.16", 13 | "@stripe/stripe-js": "^1.14.0", 14 | "@svgr/cli": "^5.5.0", 15 | "@woocommerce/woocommerce-rest-api": "^1.0.1", 16 | "axios": "^0.21.1", 17 | "classnames": "^2.3.1", 18 | "graphql": "^15.4.0", 19 | "lodash": "^4.17.20", 20 | "micro": "^9.3.4", 21 | "next": "^11.1.1", 22 | "next-stripe": "^1.0.0-beta.9", 23 | "node-fetch": "^2.6.1", 24 | "nprogress": "^0.2.0", 25 | "prop-types": "^15.7.2", 26 | "react": "17.0.1", 27 | "react-dom": "17.0.1", 28 | "sass": "^1.29.0", 29 | "stripe": "^8.148.0", 30 | "uuid": "^8.3.1", 31 | "validator": "^13.1.17" 32 | }, 33 | "devDependencies": { 34 | "install": "^0.13.0", 35 | "postcss-flexbugs-fixes": "^5.0.1", 36 | "postcss-import": "^13.0.0", 37 | "postcss-preset-env": "^6.7.0", 38 | "precss": "^4.0.0", 39 | "tailwindcss": "^1.9.6" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import "../src/styles/style.scss"; 2 | import "../src/styles/main.scss"; 3 | 4 | import Router from 'next/router'; 5 | import NProgress from 'nprogress'; 6 | 7 | NProgress.configure({ showSpinner: false }); 8 | Router.events.on('routeChangeStart', () => NProgress.start()); 9 | Router.events.on('routeChangeComplete', () => NProgress.done()); 10 | Router.events.on('routeChangeError', () => NProgress.done()); 11 | 12 | function MyApp({ Component, pageProps }) { 13 | return 14 | } 15 | 16 | export default MyApp 17 | 18 | -------------------------------------------------------------------------------- /pages/api/create-order.js: -------------------------------------------------------------------------------- 1 | const WooCommerceRestApi = require("@woocommerce/woocommerce-rest-api").default; 2 | import {isEmpty} from 'lodash' 3 | 4 | const api = new WooCommerceRestApi({ 5 | url: process.env.NEXT_PUBLIC_WORDPRESS_URL, 6 | consumerKey: process.env.WC_CONSUMER_KEY, 7 | consumerSecret: process.env.WC_CONSUMER_SECRET, 8 | version: "wc/v3" 9 | }); 10 | 11 | /** 12 | * Create order endpoint. 13 | * 14 | * @see http://woocommerce.github.io/woocommerce-rest-api-docs/?javascript#create-an-order 15 | * 16 | * @param {Object} req Request. 17 | * @param {Object} res Response. 18 | * 19 | * @return {Promise<{orderId: string, success: boolean, error: string}>} 20 | */ 21 | export default async function handler(req, res) { 22 | 23 | const responseData = { 24 | success: false, 25 | orderId: '', 26 | total: '', 27 | currency: '', 28 | error: '' 29 | } 30 | 31 | if ( isEmpty(req.body) ) { 32 | responseData.error = 'Required data not sent'; 33 | return responseData 34 | } 35 | 36 | const data = req.body; 37 | data.status = 'pending'; 38 | data.set_paid = false; 39 | 40 | try { 41 | const {data} = await api.post( 42 | 'orders', 43 | req.body 44 | ); 45 | 46 | responseData.success = true; 47 | responseData.orderId = data.number; 48 | responseData.total = data.total; 49 | responseData.currency = data.currency; 50 | 51 | res.json(responseData) 52 | 53 | } catch (error) { 54 | /** 55 | * Request usually fails if the data in req.body is not sent in the format required. 56 | * 57 | * @see Data shape expected: https://stackoverflow.com/questions/49349396/create-an-order-with-coupon-lines-in-woocomerce-rest-api 58 | */ 59 | responseData.error = error.message; 60 | res.status(500).json(responseData); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /pages/api/get-stripe-session.js: -------------------------------------------------------------------------------- 1 | const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); 2 | 3 | module.exports = async (req, res) => { 4 | 5 | const { session_id } = req.query; 6 | 7 | const session = await stripe.checkout.sessions.retrieve(session_id); 8 | 9 | res.status(200).json(session); 10 | }; 11 | -------------------------------------------------------------------------------- /pages/api/stripe-web-hook.js: -------------------------------------------------------------------------------- 1 | import { buffer } from "micro"; 2 | const Stripe = require('stripe'); 3 | const WooCommerceRestApi = require("@woocommerce/woocommerce-rest-api").default; 4 | 5 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { 6 | apiVersion: '2020-08-27' 7 | }); 8 | const webhookSecret = process.env.STRIPE_WEBHOOK_ENDPOINT_SECRET; 9 | 10 | export const config = { 11 | api: { 12 | bodyParser: false, 13 | }, 14 | }; 15 | 16 | const api = new WooCommerceRestApi({ 17 | url: process.env.NEXT_PUBLIC_WORDPRESS_URL, 18 | consumerKey: process.env.WC_CONSUMER_KEY, 19 | consumerSecret: process.env.WC_CONSUMER_SECRET, 20 | version: "wc/v3" 21 | }); 22 | 23 | /** 24 | * Update Order. 25 | * 26 | * Once payment is successful or failed, 27 | * Update Order Status to 'Processing' or 'Failed' and set the transaction id. 28 | * 29 | * @param {String} newStatus Order Status to be updated. 30 | * @param {String} orderId Order id 31 | * @param {String} transactionId Transaction id. 32 | * 33 | * @returns {Promise} 34 | */ 35 | const updateOrder = async ( newStatus, orderId, transactionId = '' ) => { 36 | 37 | let newOrderData = { 38 | status: newStatus 39 | } 40 | 41 | if ( transactionId ) { 42 | newOrderData.transaction_id = transactionId 43 | } 44 | 45 | try { 46 | const {data} = await api.put( `orders/${ orderId }`, newOrderData ); 47 | console.log( '✅ Order updated data', data ); 48 | } catch (ex) { 49 | console.error('Order creation error', ex); 50 | throw ex; 51 | } 52 | } 53 | 54 | const handler = async (req, res) => { 55 | if (req.method === "POST") { 56 | const buf = await buffer(req); 57 | const sig = req.headers["stripe-signature"]; 58 | 59 | let stripeEvent; 60 | 61 | try { 62 | stripeEvent = stripe.webhooks.constructEvent(buf, sig, webhookSecret); 63 | console.log( 'stripeEvent', stripeEvent ); 64 | } catch (err) { 65 | res.status(400).send(`Webhook Error: ${err.message}`); 66 | return; 67 | } 68 | 69 | if ( 'checkout.session.completed' === stripeEvent.type ) { 70 | const session = stripeEvent.data.object; 71 | console.log( 'sessionsession', session ); 72 | console.log( '✅ session.metadata.orderId', session.metadata.orderId, session.id ); 73 | // Payment Success. 74 | try { 75 | await updateOrder( 'processing', session.metadata.orderId, session.id ); 76 | } catch (error) { 77 | await updateOrder( 'failed', session.metadata.orderId ); 78 | console.error('Update order error', error); 79 | } 80 | } 81 | 82 | res.json({ received: true }); 83 | } else { 84 | res.setHeader("Allow", "POST"); 85 | res.status(405).end("Method Not Allowed"); 86 | } 87 | }; 88 | 89 | export default handler; 90 | -------------------------------------------------------------------------------- /pages/api/stripe/[...nextstripe].js: -------------------------------------------------------------------------------- 1 | import NextStripe from 'next-stripe' 2 | 3 | export default NextStripe({ 4 | stripe_key: process.env.STRIPE_SECRET_KEY 5 | }) 6 | -------------------------------------------------------------------------------- /pages/cart.js: -------------------------------------------------------------------------------- 1 | 2 | import Layout from "../src/components/Layout"; 3 | import CartItemsContainer from "../src/components/cart/cart-page/CartItemsContainer"; 4 | 5 | const Cart = () => { 6 | return ( 7 | 8 | 9 | 10 | ) 11 | }; 12 | 13 | export default Cart; 14 | -------------------------------------------------------------------------------- /pages/categories.js: -------------------------------------------------------------------------------- 1 | import Layout from "../src/components/Layout"; 2 | import client from '../src/components/ApolloClient'; 3 | import ParentCategoriesBlock from "../src/components/category/category-block/ParentCategoriesBlock"; 4 | import GET_CATEGORIES_QUERY from "../src/queries/get-categories"; 5 | 6 | export default function Categories ( props ) { 7 | 8 | const { productCategories } = props; 9 | 10 | return ( 11 | 12 | {/*Categories*/} 13 |
14 |

Categories

15 | 16 |
17 |
18 | ) 19 | }; 20 | 21 | export async function getStaticProps() { 22 | 23 | const {data} = await client.query({ 24 | query: GET_CATEGORIES_QUERY, 25 | }); 26 | 27 | return { 28 | props: { 29 | productCategories: data?.productCategories?.nodes || [] 30 | }, 31 | revalidate: 1 32 | } 33 | 34 | }; 35 | -------------------------------------------------------------------------------- /pages/category/[slug].js: -------------------------------------------------------------------------------- 1 | import Layout from "../../src/components/Layout"; 2 | import client from "../../src/components/ApolloClient"; 3 | import Product from "../../src/components/Product"; 4 | import {PRODUCT_BY_CATEGORY_SLUG, PRODUCT_CATEGORIES_SLUGS} from "../../src/queries/product-by-category"; 5 | import {isEmpty} from "lodash"; 6 | import {useRouter} from "next/router"; 7 | 8 | export default function CategorySingle( props ) { 9 | 10 | const router = useRouter() 11 | 12 | // If the page is not yet generated, this will be displayed 13 | // initially until getStaticProps() finishes running 14 | if (router.isFallback) { 15 | return
Loading...
16 | } 17 | 18 | const { categoryName, products } = props; 19 | 20 | return ( 21 | 22 |
23 | { categoryName ?

{ categoryName }

: '' } 24 |
25 | { undefined !== products && products?.length ? ( 26 | products.map( product => ) 27 | ) : ''} 28 |
29 |
30 |
31 | ) 32 | }; 33 | 34 | export async function getStaticProps(context) { 35 | 36 | const {params: { slug }} = context 37 | 38 | const {data} = await client.query(({ 39 | query: PRODUCT_BY_CATEGORY_SLUG, 40 | variables: { slug } 41 | })); 42 | 43 | return { 44 | props: { 45 | categoryName: data?.productCategory?.name ?? '', 46 | products: data?.productCategory?.products?.nodes ?? [] 47 | }, 48 | revalidate: 1 49 | } 50 | 51 | } 52 | 53 | export async function getStaticPaths () { 54 | const { data } = await client.query({ 55 | query: PRODUCT_CATEGORIES_SLUGS 56 | }) 57 | 58 | const pathsData = [] 59 | 60 | data?.productCategories?.nodes && data?.productCategories?.nodes.map((productCategory) => { 61 | if (!isEmpty(productCategory?.slug)) { 62 | pathsData.push({ params: { slug: productCategory?.slug } }) 63 | } 64 | }) 65 | 66 | return { 67 | paths: pathsData, 68 | fallback: true 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /pages/checkout.js: -------------------------------------------------------------------------------- 1 | import Layout from "../src/components/Layout"; 2 | import CheckoutForm from "../src/components/checkout/CheckoutForm"; 3 | import GET_COUNTRIES from "../src/queries/get-countries"; 4 | import client from "../src/components/ApolloClient"; 5 | 6 | const Checkout = ({data}) => ( 7 | 8 |
9 |

Checkout Page

10 | 11 |
12 |
13 | ); 14 | 15 | export default Checkout; 16 | 17 | export async function getStaticProps() { 18 | const { data } = await client.query({ 19 | query: GET_COUNTRIES 20 | }); 21 | 22 | return { 23 | props: { 24 | data: data || {} 25 | }, 26 | revalidate: 1 27 | }; 28 | 29 | } 30 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import Layout from "../src/components/Layout"; 2 | import Product from "../src/components/Product"; 3 | import client from '../src/components/ApolloClient'; 4 | import ParentCategoriesBlock from "../src/components/category/category-block/ParentCategoriesBlock"; 5 | import PRODUCTS_AND_CATEGORIES_QUERY from "../src/queries/product-and-categories"; 6 | import HeroCarousel from "../src/components/home/hero-carousel"; 7 | 8 | export default function Home (props) { 9 | 10 | const { products, productCategories, heroCarousel } = props || {}; 11 | 12 | return ( 13 | 14 | {/*Hero Carousel*/} 15 | 16 | {/*Categories*/ } 17 |
18 |

Categories

19 | 20 |
21 | {/*Products*/ } 22 |
23 |

Products

24 |
25 | { products.length ? ( 26 | products.map( product => ) 27 | ) : '' } 28 |
29 |
30 | 31 |
32 | ) 33 | }; 34 | 35 | export async function getStaticProps () { 36 | 37 | const { data } = await client.query( { 38 | query: PRODUCTS_AND_CATEGORIES_QUERY, 39 | } ); 40 | 41 | return { 42 | props: { 43 | productCategories: data?.productCategories?.nodes ? data.productCategories.nodes : [], 44 | products: data?.products?.nodes ? data.products.nodes : [], 45 | heroCarousel: data?.heroCarousel?.nodes[0]?.children?.nodes ? data.heroCarousel.nodes[0].children.nodes : [] 46 | }, 47 | revalidate: 1 48 | } 49 | 50 | }; 51 | -------------------------------------------------------------------------------- /pages/product/[slug].js: -------------------------------------------------------------------------------- 1 | import Layout from '../../src/components/Layout'; 2 | import { useRouter } from 'next/router'; 3 | import client from '../../src/components/ApolloClient'; 4 | import AddToCartButton from '../../src/components/cart/AddToCartButton'; 5 | import {PRODUCT_BY_SLUG_QUERY, PRODUCT_SLUGS} from '../../src/queries/product-by-slug'; 6 | import { isEmpty } from 'lodash'; 7 | import GalleryCarousel from "../../src/components/single-product/gallery-carousel"; 8 | import Price from "../../src/components/single-product/price"; 9 | 10 | export default function Product(props) { 11 | const { product } = props; 12 | 13 | const router = useRouter() 14 | 15 | // If the page is not yet generated, this will be displayed 16 | // initially until getStaticProps() finishes running 17 | if (router.isFallback) { 18 | return
Loading...
19 | } 20 | 21 | return ( 22 | 23 | { product ? ( 24 |
25 |
26 |
27 | 28 | { !isEmpty( product?.galleryImages?.nodes ) ? ( 29 | 30 | ) : !isEmpty( product.image ) ? ( 31 | Product Image 38 | ) : null } 39 |
40 |
41 |

{ product.name }

42 |
49 | 50 | 51 |
52 |
53 | 54 |
55 | ) : ( 56 | '' 57 | ) } 58 | 59 | ); 60 | }; 61 | 62 | 63 | export async function getStaticProps(context) { 64 | 65 | const {params: { slug }} = context 66 | 67 | const {data} = await client.query({ 68 | query: PRODUCT_BY_SLUG_QUERY, 69 | variables: { slug } 70 | }) 71 | 72 | return { 73 | props: { 74 | product: data?.product || {}, 75 | }, 76 | revalidate: 1 77 | }; 78 | } 79 | 80 | export async function getStaticPaths () { 81 | const { data } = await client.query({ 82 | query: PRODUCT_SLUGS 83 | }) 84 | 85 | const pathsData = [] 86 | 87 | data?.products?.nodes && data?.products?.nodes.map((product) => { 88 | if (!isEmpty(product?.slug)) { 89 | pathsData.push({ params: { slug: product?.slug } }) 90 | } 91 | }) 92 | 93 | return { 94 | paths: pathsData, 95 | fallback: true 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /pages/thank-you.js: -------------------------------------------------------------------------------- 1 | import {useState, useEffect, useContext} from "react"; 2 | import Router from "next/router"; 3 | import Link from 'next/link'; 4 | import axios from "axios"; 5 | import Layout from "../src/components/Layout"; 6 | import {AppContext} from "../src/components/context/AppContext"; 7 | import Loading from "../src/components/icons/Loading"; 8 | import ShoppingCart from "../src/components/icons/ShoppingCart"; 9 | 10 | const ThankYouContent = () => { 11 | const [cart, setCart] = useContext(AppContext); 12 | const [isSessionFetching, setSessionFetching] = useState(false); 13 | const [sessionData, setSessionData] = useState({}); 14 | const session_id = process.browser ? Router.query.session_id : null; 15 | 16 | useEffect(() => { 17 | setSessionFetching(true); 18 | if (process.browser) { 19 | localStorage.removeItem('woo-next-cart'); 20 | setCart(null); 21 | 22 | if (session_id) { 23 | axios.get(`/api/get-stripe-session/?session_id=${session_id}`) 24 | .then((response) => { 25 | setSessionData(response?.data ?? {}); 26 | setSessionFetching(false); 27 | }) 28 | .catch((error) => { 29 | console.log(error); 30 | setSessionFetching(false); 31 | }); 32 | } 33 | } 34 | 35 | }, [session_id]); 36 | 37 | return ( 38 |
39 |
40 | {isSessionFetching ? : ( 41 | <> 42 |

Thank you for placing the order.

43 |

Your payment is successful and your order details are:

44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
NameDetails
Order#{sessionData?.metadata?.orderId}
Email{sessionData?.customer_email}
62 | 63 | Shop more 64 | 65 | 66 | )} 67 |
68 |
69 | ) 70 | } 71 | 72 | const ThankYou = () => { 73 | return ( 74 | 75 | 76 | 77 | ) 78 | } 79 | 80 | export default ThankYou; 81 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'postcss-import': {}, 4 | autoprefixer: {}, 5 | tailwindcss: {}, 6 | 'postcss-flexbugs-fixes': {}, 7 | 'postcss-preset-env': { 8 | autoprefixer: { 9 | flexbox: 'no-2009' 10 | }, 11 | stage: 3, 12 | features: { 13 | 'custom-properties': false 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /public/cart-spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imranhsayed/woo-next/9a69b5710e9b5b470832d20f106e16d2d75f60ae/public/cart-spinner.gif -------------------------------------------------------------------------------- /src/components/ApolloClient.js: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | 3 | import { ApolloClient, ApolloLink, InMemoryCache, createHttpLink } from "@apollo/client"; 4 | 5 | /** 6 | * Middleware operation 7 | * If we have a session token in localStorage, add it to the GraphQL request as a Session header. 8 | */ 9 | export const middleware = new ApolloLink( ( operation, forward ) => { 10 | /** 11 | * If session data exist in local storage, set value as session header. 12 | */ 13 | const session = ( process.browser ) ? localStorage.getItem( "woo-session" ) : null; 14 | 15 | if ( session ) { 16 | operation.setContext( ( { headers = {} } ) => ( { 17 | headers: { 18 | "woocommerce-session": `Session ${ session }` 19 | } 20 | } ) ); 21 | } 22 | 23 | return forward( operation ); 24 | 25 | } ); 26 | 27 | /** 28 | * Afterware operation. 29 | * 30 | * This catches the incoming session token and stores it in localStorage, for future GraphQL requests. 31 | */ 32 | export const afterware = new ApolloLink( ( operation, forward ) => { 33 | 34 | return forward( operation ).map( response => { 35 | 36 | if ( !process.browser ) { 37 | return response; 38 | } 39 | 40 | /** 41 | * Check for session header and update session in local storage accordingly. 42 | */ 43 | const context = operation.getContext(); 44 | const { response: { headers } } = context; 45 | const session = headers.get( "woocommerce-session" ); 46 | 47 | if ( session ) { 48 | 49 | // Remove session data if session destroyed. 50 | if ( "false" === session ) { 51 | 52 | localStorage.removeItem( "woo-session" ); 53 | 54 | // Update session new data if changed. 55 | } else if ( localStorage.getItem( "woo-session" ) !== session ) { 56 | 57 | localStorage.setItem( "woo-session", headers.get( "woocommerce-session" ) ); 58 | 59 | } 60 | } 61 | 62 | return response; 63 | 64 | } ); 65 | } ); 66 | 67 | // Apollo GraphQL client. 68 | const client = new ApolloClient({ 69 | link: middleware.concat( afterware.concat( createHttpLink({ 70 | uri: `${process.env.NEXT_PUBLIC_WORDPRESS_URL}/graphql`, 71 | fetch: fetch 72 | }) ) ), 73 | cache: new InMemoryCache(), 74 | }); 75 | 76 | export default client; 77 | -------------------------------------------------------------------------------- /src/components/Footer.js: -------------------------------------------------------------------------------- 1 | import {Facebook, Instagram, Twitter, Youtube} from "./icons"; 2 | 3 | const Footer = () => ( 4 |
5 |
6 |
7 |

© Codeytek Academy 2020

8 |

Learn the latest tech skills

9 | Follow on social links to support the work 10 |
11 |
    12 |
  • 13 |
  • 14 |
  • 15 |
  • 16 |
17 |
18 |
19 | ); 20 | 21 | export default Footer; 22 | -------------------------------------------------------------------------------- /src/components/Header.js: -------------------------------------------------------------------------------- 1 | import Nav from "./Nav"; 2 | 3 | const Header = () => { 4 | return ( 5 |
6 |
8 | ) 9 | }; 10 | 11 | export default Header; 12 | -------------------------------------------------------------------------------- /src/components/Layout.js: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import { AppProvider } from "./context/AppContext"; 3 | import Header from "./Header"; 4 | import Footer from "./Footer"; 5 | import client from "./ApolloClient"; 6 | import Router from "next/router"; 7 | import NProgress from "nprogress"; 8 | import { ApolloProvider } from "@apollo/client"; 9 | 10 | Router.events.on("routeChangeStart", () => NProgress.start()); 11 | Router.events.on("routeChangeComplete", () => NProgress.done()); 12 | Router.events.on("routeChangeError", () => NProgress.done()); 13 | 14 | const Layout = (props) => { 15 | return ( 16 | 17 | 18 |
19 | 20 | Woocommerce React Theme 21 | 22 |
23 | {props.children} 24 |
25 |
26 |
27 |
28 | ); 29 | }; 30 | 31 | export default Layout; 32 | -------------------------------------------------------------------------------- /src/components/Nav.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import CartIcon from "./cart/CartIcon"; 3 | import { useState } from 'react'; 4 | 5 | const Nav = () => { 6 | 7 | const [ isMenuVisible, setMenuVisibility ] = useState(false); 8 | 9 | return ( 10 | 74 | ) 75 | }; 76 | 77 | export default Nav; 78 | -------------------------------------------------------------------------------- /src/components/Product.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import AddToCartButton from '../components/cart/AddToCartButton'; 3 | import Price from "./single-product/price"; 4 | import Image from "../image"; 5 | import {DEFAULT_PRODUCT_HOME_IMG_URL} from "../constants/urls"; 6 | 7 | const Product = ( props ) => { 8 | const { product } = props; 9 | 10 | return ( 11 | // @TODO Need to handle Group products differently. 12 | undefined !== product && 'GroupProduct' !== product.__typename ? ( 13 |
14 | 15 | 16 | 17 | 18 | 27 | 28 | 29 |
30 |

31 | { product.name ? product.name : '' } 32 |

33 |
34 | 35 | 36 |
37 | 38 |
39 | ) : ( 40 | '' 41 | ) 42 | ); 43 | }; 44 | 45 | export default Product; 46 | -------------------------------------------------------------------------------- /src/components/cart/AddToCartButton.js: -------------------------------------------------------------------------------- 1 | import {useState, useContext} from "react"; 2 | import {useQuery, useMutation} from '@apollo/client'; 3 | import Link from "next/link"; 4 | import {v4} from 'uuid'; 5 | import cx from 'classnames'; 6 | 7 | import {AppContext} from "../context/AppContext"; 8 | import {getFormattedCart} from "../../functions"; 9 | import GET_CART from "../../queries/get-cart"; 10 | import ADD_TO_CART from "../../mutations/add-to-cart"; 11 | 12 | const AddToCart = (props) => { 13 | 14 | const {product} = props; 15 | 16 | const productQryInput = { 17 | clientMutationId: v4(), // Generate a unique id. 18 | productId: product.productId, 19 | }; 20 | 21 | const [cart, setCart] = useContext(AppContext); 22 | const [showViewCart, setShowViewCart] = useState(false); 23 | const [requestError, setRequestError] = useState(null); 24 | 25 | // Get Cart Data. 26 | const {data, refetch} = useQuery(GET_CART, { 27 | notifyOnNetworkStatusChange: true, 28 | onCompleted: () => { 29 | 30 | // Update cart in the localStorage. 31 | const updatedCart = getFormattedCart(data); 32 | localStorage.setItem('woo-next-cart', JSON.stringify(updatedCart)); 33 | 34 | // Update cart data in React Context. 35 | setCart(updatedCart); 36 | } 37 | }); 38 | 39 | // Add to Cart Mutation. 40 | const [addToCart, { 41 | data: addToCartRes, 42 | loading: addToCartLoading, 43 | error: addToCartError 44 | }] = useMutation(ADD_TO_CART, { 45 | variables: { 46 | input: productQryInput, 47 | }, 48 | onCompleted: () => { 49 | // On Success: 50 | // 1. Make the GET_CART query to update the cart with new values in React context. 51 | refetch(); 52 | 53 | // 2. Show View Cart Button 54 | setShowViewCart(true) 55 | }, 56 | onError: (error) => { 57 | if (error) { 58 | setRequestError(error?.graphQLErrors?.[0]?.message ?? ''); 59 | } 60 | } 61 | }); 62 | 63 | const handleAddToCartClick = async () => { 64 | setRequestError(null); 65 | await addToCart(); 66 | }; 67 | 68 | return ( 69 |
70 | {/* Check if its an external product then put its external buy link */} 71 | {"ExternalProduct" === product.__typename ? ( 72 | 74 | Buy now 75 | 76 | ) : 77 | 88 | } 89 | {showViewCart ? ( 90 | 91 | 95 | 96 | ) : ''} 97 |
98 | ); 99 | }; 100 | 101 | export default AddToCart; 102 | -------------------------------------------------------------------------------- /src/components/cart/CartIcon.js: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { AppContext } from "../context/AppContext"; 3 | import Link from 'next/link'; 4 | 5 | const CartIcon = () => { 6 | 7 | const [ cart ] = useContext( AppContext ); 8 | const productsCount = ( null !== cart && Object.keys( cart ).length ) ? cart.totalProductsCount : ''; 9 | const totalPrice = ( null !== cart && Object.keys( cart ).length ) ? cart.totalProductsPrice : ''; 10 | 11 | return ( 12 | 13 | 14 | 15 | Bag 16 | { productsCount ? ({ productsCount }) : '' } 17 | {/*{ totalPrice ? { totalPrice } : '' }*/} 18 | 19 | 20 | 21 | ) 22 | }; 23 | 24 | export default CartIcon; 25 | -------------------------------------------------------------------------------- /src/components/cart/cart-page/CartItem.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { v4 } from "uuid"; 3 | import { getUpdatedItems } from "../../../functions"; 4 | import {Cross, Loading} from "../../icons"; 5 | 6 | const CartItem = ( { 7 | item, 8 | products, 9 | updateCartProcessing, 10 | handleRemoveProductClick, 11 | updateCart, 12 | } ) => { 13 | 14 | const [productCount, setProductCount] = useState( item.qty ); 15 | 16 | /* 17 | * When user changes the qty from product input update the cart in localStorage 18 | * Also update the cart in global context 19 | * 20 | * @param {Object} event event 21 | * 22 | * @return {void} 23 | */ 24 | const handleQtyChange = ( event, cartKey ) => { 25 | 26 | if ( process.browser ) { 27 | 28 | event.stopPropagation(); 29 | 30 | // If the previous update cart mutation request is still processing, then return. 31 | if ( updateCartProcessing ) { 32 | return; 33 | } 34 | 35 | // If the user tries to delete the count of product, set that to 1 by default ( This will not allow him to reduce it less than zero ) 36 | const newQty = ( event.target.value ) ? parseInt( event.target.value ) : 1; 37 | 38 | // Set the new qty in state. 39 | setProductCount( newQty ); 40 | 41 | if ( products.length ) { 42 | 43 | const updatedItems = getUpdatedItems( products, newQty, cartKey ); 44 | 45 | updateCart( { 46 | variables: { 47 | input: { 48 | clientMutationId: v4(), 49 | items: updatedItems 50 | } 51 | }, 52 | } ); 53 | } 54 | 55 | } 56 | }; 57 | 58 | 59 | return ( 60 | 61 | 62 | {/* Remove item */} 63 | handleRemoveProductClick( event, item.cartKey, products ) }> 65 | 66 | 67 | 68 | 69 | { 70 | 71 | { item.name } 72 | { ( 'string' !== typeof item.price ) ? item.price.toFixed( 2 ) : item.price } 73 | 74 | {/* Qty Input */ } 75 | 76 | {/* @TODO Need to update this with graphQL query */ } 77 | handleQtyChange( event, item.cartKey ) } 84 | /> 85 | 86 | 87 | { ( 'string' !== typeof item.totalPrice ) ? item.totalPrice.toFixed( 2 ) : item.totalPrice } 88 | 89 | 90 | ) 91 | }; 92 | 93 | export default CartItem; 94 | -------------------------------------------------------------------------------- /src/components/cart/cart-page/CartItemsContainer.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { useContext, useState } from 'react'; 3 | import { AppContext } from "../../context/AppContext"; 4 | import { getFormattedCart, getUpdatedItems } from '../../../functions'; 5 | import CartItem from "./CartItem"; 6 | import { v4 } from 'uuid'; 7 | import { useMutation, useQuery } from '@apollo/client'; 8 | import UPDATE_CART from "../../../mutations/update-cart"; 9 | import GET_CART from "../../../queries/get-cart"; 10 | import CLEAR_CART_MUTATION from "../../../mutations/clear-cart"; 11 | import {isEmpty} from 'lodash' 12 | 13 | 14 | const CartItemsContainer = () => { 15 | 16 | 17 | // @TODO wil use it in future variations of the project. 18 | const [ cart, setCart ] = useContext( AppContext ); 19 | const [requestError, setRequestError] = useState( null ); 20 | 21 | // Get Cart Data. 22 | const { loading, error, data, refetch } = useQuery( GET_CART, { 23 | notifyOnNetworkStatusChange: true, 24 | onCompleted: () => { 25 | 26 | // Update cart in the localStorage. 27 | const updatedCart = getFormattedCart( data ); 28 | localStorage.setItem( 'woo-next-cart', JSON.stringify( updatedCart ) ); 29 | 30 | // Update cart data in React Context. 31 | setCart( updatedCart ); 32 | } 33 | } ); 34 | 35 | // Update Cart Mutation. 36 | const [updateCart, { data: updateCartResponse, loading: updateCartProcessing, error: updateCartError }] = useMutation( UPDATE_CART, { 37 | onCompleted: () => { 38 | refetch(); 39 | }, 40 | onError: ( error ) => { 41 | if ( error ) { 42 | const errorMessage = error?.graphQLErrors?.[ 0 ]?.message ? error.graphQLErrors[ 0 ].message : ''; 43 | setRequestError( errorMessage ); 44 | } 45 | } 46 | } ); 47 | 48 | // Update Cart Mutation. 49 | const [clearCart, { data: clearCartRes, loading: clearCartProcessing, error: clearCartError }] = useMutation( CLEAR_CART_MUTATION, { 50 | onCompleted: () => { 51 | refetch(); 52 | }, 53 | onError: ( error ) => { 54 | if ( error ) { 55 | const errorMessage = ! isEmpty(error?.graphQLErrors?.[ 0 ]) ? error.graphQLErrors[ 0 ]?.message : ''; 56 | setRequestError( errorMessage ); 57 | } 58 | } 59 | } ); 60 | 61 | /* 62 | * Handle remove product click. 63 | * 64 | * @param {Object} event event 65 | * @param {Integer} Product Id. 66 | * 67 | * @return {void} 68 | */ 69 | const handleRemoveProductClick = ( event, cartKey, products ) => { 70 | 71 | event.stopPropagation(); 72 | if ( products.length ) { 73 | 74 | // By passing the newQty to 0 in updateCart Mutation, it will remove the item. 75 | const newQty = 0; 76 | const updatedItems = getUpdatedItems( products, newQty, cartKey ); 77 | 78 | updateCart( { 79 | variables: { 80 | input: { 81 | clientMutationId: v4(), 82 | items: updatedItems 83 | } 84 | }, 85 | } ); 86 | } 87 | }; 88 | 89 | // Clear the entire cart. 90 | const handleClearCart = ( event ) => { 91 | 92 | event.stopPropagation(); 93 | 94 | if ( clearCartProcessing ) { 95 | return; 96 | } 97 | 98 | clearCart( { 99 | variables: { 100 | input: { 101 | clientMutationId: v4(), 102 | all: true 103 | } 104 | }, 105 | } ); 106 | } 107 | 108 | return ( 109 |
110 | { cart ? ( 111 |
112 |
113 |

Cart

114 | {/*Clear entire cart*/} 115 |
116 | 120 | { clearCartProcessing ?

Clearing...

: '' } 121 | { updateCartProcessing ?

Updating...

: null } 122 |
123 |
124 |
125 | 126 | 127 | 128 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | { cart.products.length && ( 138 | cart.products.map( item => ( 139 | 147 | ) ) 148 | ) } 149 | 150 |
129 | 130 | ProductPriceQuantityTotal
151 | 152 | {/*Cart Total*/ } 153 |
154 |
155 | {/*

Cart Total

*/} 156 | 157 | 158 | 159 | 160 | 161 | 162 | {/* 163 | 164 | 165 | */} 166 | 167 |
Subtotal{ ( 'string' !== typeof cart.totalProductsPrice ) ? cart.totalProductsPrice.toFixed(2) : cart.totalProductsPrice }
Total{ ( 'string' !== typeof cart.totalProductsPrice ) ? cart.totalProductsPrice.toFixed(2) : cart.totalProductsPrice }
168 | 169 | 173 | 174 |
175 |
176 |
177 | 178 | {/* Display Errors if any */} 179 | { requestError ?
{ requestError }
: '' } 180 |
181 | ) : ( 182 |
183 |

No items in the cart

184 | 185 | 189 | 190 |
191 | ) } 192 |
193 | 194 | ); 195 | }; 196 | 197 | export default CartItemsContainer; 198 | -------------------------------------------------------------------------------- /src/components/category/category-block/ParentCategoriesBlock.js: -------------------------------------------------------------------------------- 1 | import ProductCategoryBlock from "./ParentCategoryBlock"; 2 | 3 | const ParentCategoriesBlock = ( props ) => { 4 | 5 | const { productCategories } = props || {}; 6 | 7 | return ( 8 |
9 | { productCategories.length ? ( 10 | productCategories.map( ( productCategory, index ) => ) 11 | ) : null } 12 |
13 | ) 14 | 15 | }; 16 | 17 | export default ParentCategoriesBlock; 18 | -------------------------------------------------------------------------------- /src/components/category/category-block/ParentCategoryBlock.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import Image from "../../../image"; 3 | import {DEFAULT_CATEGORY_IMG_URL} from "../../../constants/urls"; 4 | 5 | const ParentCategoryBlock = ( props ) => { 6 | 7 | const { category } = props; 8 | 9 | return ( 10 | 28 | ); 29 | } 30 | 31 | export default ParentCategoryBlock; 32 | -------------------------------------------------------------------------------- /src/components/checkout/Address.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import CountrySelection from "./CountrySelection"; 3 | import StateSelection from "./StatesSelection"; 4 | import InputField from "./form-elements/InputField"; 5 | 6 | const Address = ({input, countries, states, handleOnChange, isFetchingStates, isShipping}) => { 7 | 8 | const {errors} = input || {}; 9 | 10 | return ( 11 | <> 12 |
13 | 23 | 33 |
34 | 43 | {/* Country Selection*/} 44 | 50 | 61 | 71 | 81 | {/* State */} 82 | 89 |
90 | 100 | 110 |
111 | 122 | {/* @TODO Create an Account */} 123 | {/*
*/} 124 | {/* */} 128 | {/*
*/} 129 | {/*

Additional Information

*/} 130 | {/* @TODO Order Notes */} 131 | {/*
*/} 132 | {/* */} 133 | {/*