├── .env.example ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .nowignore ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── LICENCE.md ├── README.md ├── cypress.json ├── cypress └── integration │ └── homeProduct.spec.js ├── demo.gif ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── cart.js ├── checkout.js ├── index.js ├── login.js ├── my-account.js ├── product │ └── [slug].js └── register.js ├── public └── static │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── favicon.ico │ ├── hero.jpg │ ├── manifest │ └── manifest.json │ ├── nprogress.css │ └── placeholder.png ├── service-worker.js ├── src ├── apollo │ └── ApolloClient.js ├── components │ ├── Image.js │ ├── cart │ │ ├── AddToCartButton.js │ │ ├── CartIcon.js │ │ └── cart-page │ │ │ ├── CartBlocks.js │ │ │ └── CartItem.js │ ├── checkout │ │ ├── Billing.js │ │ ├── CheckoutCartItem.js │ │ ├── CheckoutForm.js │ │ ├── Error.js │ │ ├── PaymentModes.js │ │ ├── YourOrder.js │ │ └── country-list.js │ ├── context │ │ └── AppContext.js │ ├── gallery │ │ └── index.js │ ├── home │ │ ├── Categories.js │ │ └── Hero.js │ ├── layouts │ │ ├── Footer.js │ │ ├── Header.js │ │ ├── Layout.js │ │ ├── Menu.js │ │ └── Nav.js │ └── message-alert │ │ ├── Loading.js │ │ └── MessageAlert.js ├── queries │ ├── auth │ │ ├── login.js │ │ └── register.js │ ├── fragments │ │ ├── image.js │ │ ├── product.js │ │ └── user.js │ ├── get-product-slug.js │ ├── get-product.js │ ├── get-products.js │ └── index.js ├── styles │ ├── sass │ │ ├── cart.scss │ │ ├── checkout.scss │ │ ├── common.scss │ │ ├── core │ │ │ └── _colors.scss │ │ ├── gallery.scss │ │ ├── home.scss │ │ ├── layouts │ │ │ ├── _forms.scss │ │ │ ├── _my-account.scss │ │ │ ├── _nav.scss │ │ │ └── _nprogress.scss │ │ ├── navbar.scss │ │ ├── products.scss │ │ └── styles.scss │ └── vendor │ │ └── bootstrap.min.css ├── utils │ ├── auth-functions.js │ ├── cart-functions.js │ └── commmon-functions.js └── validator │ ├── checkout.js │ ├── isEmpty.js │ ├── login.js │ └── register.js └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | SITE_URL=http://localhost:3000 2 | NEXT_PUBLIC_WOO_SITE_URL=http://yourwocommercesite.com 3 | WOO_CONSUMER_KEY=xxxxx 4 | WOO_SECRET=xxxxx -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.scss 2 | **/*.md 3 | **/*.txt 4 | **/*.min.js 5 | **/node_modules/** 6 | **/vendor/** 7 | **/build/** 8 | **/src/components/**/vendor-* -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, // Make sure eslint picks up the config at the root of the directory 3 | parserOptions: { 4 | ecmaVersion: 2020, // Use the latest ecmascript standard 5 | sourceType: 'module', // Allows using import/export statements 6 | ecmaFeatures: { 7 | jsx: true // Enable JSX since we're using React 8 | } 9 | }, 10 | settings: { 11 | react: { 12 | version: 'detect' // Automatically detect the react version 13 | } 14 | }, 15 | env: { 16 | browser: true, // Enables browser globals like window and document 17 | amd: true, // Enables require() and define() as global variables as per the amd spec. 18 | node: true // Enables Node.js global variables and Node.js scoping. 19 | }, 20 | extends: [ 21 | 'eslint:recommended', 22 | 'plugin:react/recommended', 23 | 'plugin:jsx-a11y/recommended', 24 | 'plugin:prettier/recommended' // Make this the last element so prettier config overrides other formatting rules 25 | ], 26 | rules: { 27 | 'prettier/prettier': ['error', {}, { usePrettierrc: true }], // Use our .prettierrc file as source 28 | 'react/react-in-jsx-scope': 'off', 29 | 'jsx-a11y/anchor-is-valid': [ 30 | 'error', 31 | { 32 | components: ['Link'], 33 | specialLink: ['hrefLeft', 'hrefRight'], 34 | aspects: ['invalidHref', 'preferButton'] 35 | } 36 | ], 37 | 'react/prop-types': 'off' 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /.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 | .env 11 | client-config.js 12 | .next 13 | wooConfig.js 14 | 15 | .now -------------------------------------------------------------------------------- /.nowignore: -------------------------------------------------------------------------------- 1 | .next 2 | .cache -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 14.15.0 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | public 3 | src/styles/vendor/ 4 | *.md 5 | .next 6 | .eslintrc.js 7 | next.config.js 8 | package-lock.json 9 | package.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "tabWidth": 4, 4 | "printWidth": 100, 5 | "singleQuote": true, 6 | "trailingComma": "none", 7 | "jsxBracketSameLine": true 8 | } 9 | -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2019-present rtCamp. https://rtCamp.com 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WP Decoupled :zap: 2 | [![Project Status: WIP – Initial development is in progress, but there has not yet been a stable, usable release suitable for the public.](https://www.repostatus.org/badges/latest/wip.svg)](https://www.repostatus.org/#wip) 3 | 4 | This is a React theme boilerplate for WordPress, built with Next JS, Webpack, Babel, Node, Express. 5 | 6 | ## Live demo site. 7 | [Live Demo](https://wp-decoupled-git-master.rtcamp.vercel.app/) 8 | 9 | ## Demo :movie_camera: 10 | 11 | ![](demo.gif) 12 | 13 | ## Getting Started :surfer: 14 | 15 | These instructions will get you a copy of the project up and running on your local machine for development purposes. 16 | 17 | 18 | ### Installing :wrench: 19 | 20 | 1. Clone this repo in `git@github.com:rtCamp/wp-decoupled.git` 21 | 2. `cd wp-decoupled` 22 | 3. `nvm use` 23 | 4. `npm install` 24 | 25 | ## Configure Backend( WordPress site ) :wrench: 26 | 27 | ### 1. Add GraphQl support on WordPress 28 | 29 | 1. Clone and activate the following plugins, in your WordPress plugin directory: 30 | * [wp-graphql](https://github.com/wp-graphql/wp-graphql) Exposes graphql for WordPress 31 | * [wp-graphql-jwt-authentication](https://github.com/wp-graphql/wp-graphql-jwt-authentication) This plugin extends the [wp-graphql](https://github.com/wp-graphql/wp-graphql) plugin to provide authentication using JWT. 32 | * [wp-graphiql](https://github.com/wp-graphql/wp-graphiql) Provides GraphiQL IDE (playground) to the WP-Admin 33 | * [wp-graphql-woocommerce](https://github.com/wp-graphql/wp-graphql-woocommerce) Adds Woocommerce functionality to a WPGraphQL schema( Tested upto [v0.7.0](https://github.com/wp-graphql/wp-graphql-woocommerce/releases/tag/v0.7.0) of wp-graphql-woocommerce) 34 | 35 | 36 | 2. 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 ) 37 | WP Dashboard > Tools > WooCommerce products(CSV) : The WooCommerce default products csv file is available at `wp-content/plugins/woocommerce/sample-data/sample_products.csv` 38 | 39 | ### 2. For Authentication :lock: 40 | 41 | a. You can use any secret token of your choice, however it would be best if you generate one using WordPress Salt generator (https://api.wordpress.org/secret-key/1.1/salt/) to generate a Secret. 42 | And just pick up any one of the token and add it in place of 'your-secret-token' below: 43 | 44 | Define a Secret in `wp-config.php` of your WordPress directory: 45 | ``` 46 | define( 'GRAPHQL_JWT_AUTH_SECRET_KEY', 'your-secret-token' ); 47 | ``` 48 | 49 | b. Depending on your particular environment, you may have to research how to enable these headers, but in Apache, you can do the following in your `.htaccess`: 50 | 51 | ``` 52 | SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1 53 | ``` 54 | 55 | ## Configure Front End :wrench: 56 | 57 | * Rename `.env.example` to `.env` and update your details 58 | 59 | ``` 60 | SITE_URL=http://localhost:3000 61 | NEXT_PUBLIC_WOO_SITE_URL=http://yourwocommercesite.com 62 | WOO_CONSUMER_KEY=xxxxx 63 | WOO_SECRET=xxxxx 64 | ``` 65 | 66 | 67 | ## Commands :computer: 68 | 69 | * `npm run dev` Runs the node server in development mode 70 | * `npm run dev:inspect` Runs the dev server with **Inspector** 71 | * `npm run server` Runs the **NEXT** produciton server 72 | * `npm run lint` Runs the linter 73 | * `npm run format` Runs the formatter 74 | * `npm run build` Creates the **NEXT** build 75 | 76 | ## Using PWA on mobile :iphone: 77 | 78 | * Open the site in Chrome on your mobile and then click on add to home screen. 79 | * It will be downloaded and saved as a Progressive Web App on your mobile. 80 | * Once added Look `WP Decoupled` app on your mobile. 81 | * This PWA will work even when you are offline. 82 | 83 | ## Branches Information :seedling: 84 | 85 | 1. [master](https://github.com/rtCamp/wp-decoupled/tree/master) Main React WooCommerce theme 86 | 2. [develop](https://github.com/rtCamp/wp-decoupled/tree/develop) For testing 87 | 2. [wp-docoupled-boilerplate](https://github.com/rtCamp/wp-decoupled/tree/wp-decoupled-boilerplate) Boilerplate to start a new React theme project with PWA implementation ( Work in Progress ) 88 | 89 | ## Author 90 | 91 | * **[rtCamp](https://rtcamp.com)** 92 | 93 | ## Contributors :bust_in_silhouette: 94 | 95 | * **[Imran Sayed](https://github.com/imranhsayed)** 96 | * **[Muhammad Muhsin](https://github.com/m-muhsin)** 97 | * **[Divyaraj Masani](https://github.com/divyarajmasani)** 98 | * **[Sayed Taqui](https://github.com/sayedtaqui)** 99 | * **[Vipin Kumar Dinkar](https://github.com/nicestrudeguy)** 100 | * **[Belal Dif](https://github.com/bilouStrike)** 101 | 102 | ## License :page_with_curl: 103 | 104 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details 105 | 106 | ## Does this interest you? 107 | 108 | Join us at rtCamp, we specialize in providing high performance enterprise WordPress solutions 109 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:3000" 3 | } 4 | -------------------------------------------------------------------------------- /cypress/integration/homeProduct.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | describe('Visit home , if products exists, check the links by clicking', () => { 4 | it('visit and check first product', () => { 5 | cy.visit('http://localhost:3000'); 6 | cy.get('.products-wrapper').find('.product-container').first().click() 7 | cy.url().should('include','/product/'); 8 | }); 9 | 10 | it('visit and check last product', () => { 11 | cy.visit('http://localhost:3000'); 12 | cy.get('.products-wrapper').find('.product-container').last().click() 13 | cy.url().should('include','/product/'); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtCamp/wp-decoupled/be03e99a4738179025739f5e1074a69d97504dd5/demo.gif -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const withCss = require('@zeit/next-css'); 2 | const path = require('path'); 3 | 4 | const withOffline = require('next-offline'); 5 | const withSass = require('@zeit/next-sass'); 6 | 7 | const workBoxOptions = { 8 | workboxOpts: { 9 | swSrc: 'service-worker.js', 10 | swDest: 'static/service-worker.js', 11 | exclude: [/.+error\.js$/, /\.map$/] 12 | } 13 | }; 14 | 15 | const backend_hostname = new URL(process.env.NEXT_PUBLIC_WOO_SITE_URL).hostname; 16 | 17 | module.exports = withOffline( 18 | withCss( 19 | withSass({ 20 | workboxOpts: workBoxOptions.workboxOpts, 21 | generateInDevMode: true, 22 | dontAutoRegisterSw: true, 23 | generateSw: false, 24 | globPatterns: ['static/**/*'], 25 | globDirectory: '.', 26 | target: 'serverless', 27 | images: { 28 | domains: [ 29 | backend_hostname, 30 | 'https://via.placeholder.com' 31 | ], 32 | }, 33 | }) 34 | ) 35 | ); 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wp-decoupled", 3 | "version": "1.0.0", 4 | "description": "A decoupled WordPress application in React built with Next.js", 5 | "main": "server.js", 6 | "scripts": { 7 | "dev": "next dev", 8 | "dev:inspect": "NODE_OPTIONS='--inspect' next", 9 | "build": "next build", 10 | "start": "next start", 11 | "cypress:open": "cypress open", 12 | "cypress:run": "cypress run", 13 | "lint": "eslint --fix .", 14 | "format": "prettier --write './**/*.{js,jsx,ts,tsx,css,md,json}' --config ./.prettierrc" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/rtCamp/wp-decoupled.git" 19 | }, 20 | "keywords": [ 21 | "decoupled", 22 | "react", 23 | "next.js" 24 | ], 25 | "author": "Imran Sayed, Sayed Taqui, Divyaraj Masani, Muhammad Muhsin", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/rtCamp/wp-decoupled/issues" 29 | }, 30 | "engine": { 31 | "node": "v8.12.0", 32 | "npm": "6.4.1" 33 | }, 34 | "homepage": "https://github.com/rtCamp/wp-decoupled#readme", 35 | "dependencies": { 36 | "@apollo/client": "^3.3.6", 37 | "@zeit/next-css": "^1.0.1", 38 | "cookie-parser": "^1.4.5", 39 | "dompurify": "^2.2.6", 40 | "dotenv": "^8.2.0", 41 | "express": "^4.17.1", 42 | "graphql": "^14.7.0", 43 | "graphql-tag": "^2.11.0", 44 | "idb-keyval": "^3.2.0", 45 | "isomorphic-unfetch": "^3.1.0", 46 | "next": "^10.0.5", 47 | "next-offline": "^4.0.6", 48 | "node-fetch": "^2.6.1", 49 | "nprogress": "^0.2.0", 50 | "prop-types": "^15.7.2", 51 | "react": "^17.0.1", 52 | "react-bootstrap": "^1.4.3", 53 | "react-dom": "^17.0.1", 54 | "url-loader": "^2.3.0", 55 | "validator": "^11.1.0", 56 | "woocommerce-api": "^1.5.0" 57 | }, 58 | "devDependencies": { 59 | "@zeit/next-sass": "^1.0.1", 60 | "cypress": "^6.3.0", 61 | "eslint": "^7.17.0", 62 | "eslint-config-airbnb": "^18.2.1", 63 | "eslint-config-prettier": "^7.1.0", 64 | "eslint-plugin-import": "^2.22.1", 65 | "eslint-plugin-jsx-a11y": "^6.4.1", 66 | "eslint-plugin-prettier": "^3.3.1", 67 | "eslint-plugin-react": "^7.22.0", 68 | "eslint-plugin-react-hooks": "^4.2.0", 69 | "next-compose-plugins": "^2.2.1", 70 | "node-sass": "^4.14.1", 71 | "prettier": "^2.2.1", 72 | "serialize-javascript": "^2.1.2" 73 | } 74 | } -------------------------------------------------------------------------------- /pages/cart.js: -------------------------------------------------------------------------------- 1 | import Layout from '../src/components/layouts/Layout'; 2 | import CartBlocks from '../src/components/cart/cart-page/CartBlocks'; 3 | 4 | const Cart = () => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default Cart; 13 | -------------------------------------------------------------------------------- /pages/checkout.js: -------------------------------------------------------------------------------- 1 | import Layout from '../src/components/layouts/Layout'; 2 | import CheckoutForm from '../src/components/checkout/CheckoutForm'; 3 | 4 | const Checkout = () => { 5 | return ( 6 | 7 |
8 |

Checkout Page.

9 | 10 |
11 |
12 | ); 13 | }; 14 | 15 | export default Checkout; 16 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import Layout from '../src/components/layouts/Layout'; 2 | import Link from 'next/link'; 3 | import client from '../src/apollo/ApolloClient'; 4 | import AddToCartButton from '../src/components/cart/AddToCartButton'; 5 | import Hero from '../src/components/home/Hero'; 6 | import Image from '../src/components/Image'; 7 | import { PRODUCTS_QUERY } from '../src/queries'; 8 | 9 | const NewProducts = ({ products }) => { 10 | return ( 11 |
12 |

Products

13 | {products.length ? ( 14 |
15 |
16 | {products.map((item) => 17 | // @TODO Need to add support for Group product. 18 | undefined !== item && 'GroupProduct' !== item.__typename ? ( 19 |
20 | {/* @TODO need to get rid of using databseId here. */} 21 | 22 | 23 | 24 | {item?.image?.altText 28 |
{item.name}
29 |

{item.price}

30 |
31 |
32 | 33 | 34 |
35 | ) : ( 36 | '' 37 | ) 38 | )} 39 |
40 |
41 | ) : ( 42 | '' 43 | )} 44 |
45 | ); 46 | }; 47 | 48 | const Index = (props) => { 49 | const { products } = props; 50 | 51 | return ( 52 | 53 | 54 | {/**/} 55 | 56 | 57 | ); 58 | }; 59 | 60 | export async function getStaticProps() { 61 | const { data } = await client.query({ 62 | query: PRODUCTS_QUERY 63 | }); 64 | return { 65 | props: { 66 | products: data.products.nodes 67 | }, 68 | revalidate: 1 69 | }; 70 | }; 71 | 72 | export default Index; 73 | -------------------------------------------------------------------------------- /pages/login.js: -------------------------------------------------------------------------------- 1 | import Layout from '../src/components/layouts/Layout'; 2 | import { useState } from 'react'; 3 | import client from '../src/apollo/ApolloClient'; 4 | import { useMutation } from '@apollo/client' 5 | import MessageAlert from '../src/components/message-alert/MessageAlert'; 6 | import Loading from '../src/components/message-alert/Loading'; 7 | import Router from 'next/router'; 8 | import { isUserValidated } from '../src/utils/auth-functions'; 9 | import isEmpty from '../src/validator/isEmpty'; 10 | import Link from 'next/link'; 11 | import validateAndSanitizeLoginForm from '../src/validator/login'; 12 | import { LOGIN_USER } from '../src/queries'; 13 | /** 14 | * Login functional component. 15 | * 16 | * @return {object} Login form. 17 | */ 18 | const Login = () => { 19 | const [username, setUsername] = useState(''); 20 | const [password, setPassword] = useState(''); 21 | const [errorMessage, setErrorMessage] = useState(''); 22 | const [showAlertBar, setShowAlertBar] = useState(true); 23 | 24 | // Check if the user is validated already. 25 | if (process.browser) { 26 | const userValidated = isUserValidated(); 27 | 28 | // If user is already validated, redirect user to My Account page. 29 | if (!isEmpty(userValidated)) { 30 | Router.push('/my-account'); 31 | } 32 | } 33 | 34 | /** 35 | * Hide the Status bar on cross button click. 36 | * 37 | * @return {void} 38 | */ 39 | const onCloseButtonClick = () => { 40 | setShowAlertBar(false); 41 | setErrorMessage(''); 42 | }; 43 | 44 | /** 45 | * Handles user login. 46 | * 47 | * @param {object} event Event Object. 48 | * @param {object} login login function from login mutation query. 49 | * @return {void} 50 | */ 51 | const handleLogin = async (event, login) => { 52 | if (process.browser) { 53 | event.preventDefault(); 54 | 55 | // Validation and Sanitization. 56 | const validationResult = validateAndSanitizeLoginForm({ username, password }); 57 | 58 | // If the data is valid. 59 | if (validationResult.isValid) { 60 | await login({ 61 | variables: { 62 | username: validationResult.sanitizedData.username, 63 | password: validationResult.sanitizedData.password 64 | } 65 | }) 66 | .then((response) => handleLoginSuccess(response)) 67 | .catch((err) => handleLoginFail(err.graphQLErrors[0].message)); 68 | } else { 69 | setClientSideError(validationResult); 70 | } 71 | } 72 | }; 73 | 74 | /** 75 | * Sets client side error. 76 | * 77 | * Sets error data to result received from our client side validation function, 78 | * and statusbar to true so that its visible to show the error. 79 | * 80 | * @param {Object} validationResult Validation Data result. 81 | */ 82 | const setClientSideError = (validationResult) => { 83 | if (validationResult.errors.password) { 84 | setErrorMessage(validationResult.errors.password); 85 | } 86 | 87 | if (validationResult.errors.username) { 88 | setErrorMessage(validationResult.errors.username); 89 | } 90 | 91 | setShowAlertBar(true); 92 | }; 93 | 94 | /** 95 | * Set server side error. 96 | * 97 | * Sets error data received as a response of our query from the server 98 | * and sets statusbar to true so that its visible to show our error. 99 | * 100 | * @param {String} error Error 101 | * 102 | * @return {void} 103 | */ 104 | const setServerSideError = (error) => { 105 | setErrorMessage(error); 106 | setShowAlertBar(true); 107 | }; 108 | 109 | /** 110 | * Handle Login Fail. 111 | * 112 | * Set the error message text and validated to false. 113 | * 114 | * @param {String} err Error message received 115 | * @return {void} 116 | */ 117 | const handleLoginFail = (err) => { 118 | const error = err.split('_').join(' ').toUpperCase(); 119 | 120 | setServerSideError(error); 121 | }; 122 | 123 | /** 124 | * Handle Login success. 125 | * 126 | * @param {Object} response Response received 127 | * 128 | * @return {void} 129 | */ 130 | const handleLoginSuccess = (response) => { 131 | if (response.data.login.authToken) { 132 | // Set the authtoken, user id and username in the localStorage. 133 | localStorage.setItem( 134 | process.env.RT_WP_DECOUPLED_USER_TOKEN, 135 | JSON.stringify(response.data.login) 136 | ); 137 | 138 | // Set form field vaues to empty. 139 | setErrorMessage(''); 140 | setUsername(''); 141 | setPassword(''); 142 | 143 | // Send the user to My Account page on successful login. 144 | Router.push('/my-account'); 145 | } 146 | }; 147 | 148 | const [ 149 | login, 150 | { 151 | data: data, 152 | loading: loading, 153 | error: error, 154 | }, 155 | ] = useMutation(LOGIN_USER, { client }) 156 | 157 | return ( 158 | 159 |
160 | {/* Title */} 161 |

Login

162 | 163 | {/* Error Message */} 164 | {'' !== errorMessage 165 | ? showAlertBar && ( 166 | 171 | ) 172 | : ''} 173 | 174 | {/* Login Form */} 175 |
handleLogin(event, login)}> 176 | {/* Username or email */} 177 |
178 | 181 | setUsername(event.target.value)} 188 | /> 189 |
190 | 191 | {/* Password */} 192 |
193 | 196 | setPassword(event.target.value)} 203 | /> 204 |
205 | 206 | {/* Submit Button */} 207 |
208 | 214 | 215 | Register 216 | 217 |
218 | 219 | {/* Loading */} 220 | {loading ? : ''} 221 | 222 |
223 |
224 | ); 225 | }; 226 | 227 | export default Login; 228 | -------------------------------------------------------------------------------- /pages/my-account.js: -------------------------------------------------------------------------------- 1 | import Layout from '../src/components/layouts/Layout'; 2 | import { useState, useEffect } from 'react'; 3 | import { isUserValidated } from '../src/utils/auth-functions'; 4 | import isEmpty from '../src/validator/isEmpty'; 5 | import Router from 'next/router'; 6 | 7 | /** 8 | * MyAccount functional component. 9 | * 10 | * @return {object} MyAccount content. 11 | */ 12 | const MyAccount = () => { 13 | const [showContent, setShowContent] = useState(false); 14 | const [userData, setUserData] = useState(''); 15 | 16 | useEffect(() => { 17 | const userValidatedData = isUserValidated(); 18 | 19 | if (!isEmpty(userValidatedData)) { 20 | setUserData(userValidatedData); 21 | setShowContent(true); 22 | } else { 23 | // If user is not logged in send the user back to login page. 24 | Router.push('/login'); 25 | } 26 | }, []); 27 | 28 | return ( 29 | 30 | {/* Only Show Content if user is logged in */} 31 | {showContent ? ( 32 |
33 |

My Account

34 |
35 | 36 | Dashboard 37 | 38 | 39 | Orders 40 | 41 | 42 | Addresesses 43 | 44 | 45 | Account Details 46 | 47 |
48 | 49 |
50 |
51 | {userData.user.nicename ?
Howdy {userData.user.nicename}!
: ''} 52 |
Account Details
53 | {userData.user.email ?

Email: {userData.user.email}

: ''} 54 |
55 |
56 |
57 | ) : ( 58 | '' 59 | )} 60 |
61 | ); 62 | }; 63 | 64 | export default MyAccount; 65 | -------------------------------------------------------------------------------- /pages/product/[slug].js: -------------------------------------------------------------------------------- 1 | import Layout from '../../src/components/layouts/Layout'; 2 | import AddToCartButton from '../../src/components/cart/AddToCartButton'; 3 | import client from '../../src/apollo/ApolloClient'; 4 | import Image from '../../src/components/Image'; 5 | import { 6 | PRODUCT_QUERY, 7 | PRODUCT_SLUGS 8 | } from '../../src/queries'; 9 | import Gallery from '../../src/components/gallery'; 10 | 11 | const Product = ({data}) => { 12 | 13 | const { product } = data || {} 14 | 15 | return ( 16 | 17 | {product ? ( 18 |
19 |
20 |
21 | {product?.image?.altText 25 |
26 |
27 |

{product?.name}

28 |

29 | 30 | {product?.price} 31 | 32 |

33 | 34 |
35 |
36 |
37 |
41 |
42 | 43 |
44 | ) : ( 45 | null 46 | )} 47 | 48 | ); 49 | }; 50 | 51 | export async function getStaticProps({ params }) { 52 | let { slug } = params; 53 | 54 | const { data } = await client.query({ 55 | query: PRODUCT_QUERY, 56 | variables: { slug } 57 | }); 58 | 59 | return { 60 | props: { 61 | data: { 62 | product: data?.product 63 | } 64 | } 65 | }; 66 | } 67 | 68 | export async function getStaticPaths() { 69 | 70 | const { data } = await client.query({ 71 | query: PRODUCT_SLUGS 72 | }); 73 | 74 | const pathsData = []; 75 | 76 | data.products.edges.map((product) => { 77 | pathsData.push({ params: { slug: `${product.node.slug}` } }); 78 | }); 79 | 80 | return { 81 | paths: pathsData, 82 | fallback: true 83 | }; 84 | } 85 | 86 | export default Product; 87 | -------------------------------------------------------------------------------- /pages/register.js: -------------------------------------------------------------------------------- 1 | import Layout from '../src/components/layouts/Layout'; 2 | import { useState } from 'react'; 3 | import client from '../src/apollo/ApolloClient'; 4 | import { useMutation } from '@apollo/client' 5 | import MessageAlert from '../src/components/message-alert/MessageAlert'; 6 | import Loading from '../src/components/message-alert/Loading'; 7 | import Router from 'next/router'; 8 | import { isUserValidated } from '../src/utils/auth-functions'; 9 | import isEmpty from '../src/validator/isEmpty'; 10 | import Link from 'next/link'; 11 | import validateAndSanitizeRegisterForm from '../src/validator/register'; 12 | import { REGISTER_USER } from '../src/queries'; 13 | /** 14 | * Register Functional Component. 15 | * 16 | * @return {object} Register form. 17 | */ 18 | const Register = () => { 19 | const [username, setUsername] = useState(''); 20 | const [email, setEmail] = useState(''); 21 | const [password, setPassword] = useState(''); 22 | const [errorMessage, setErrorMessage] = useState(''); 23 | const [successMessage, setSuccessMessage] = useState(''); 24 | const [showAlertBar, setShowAlertBar] = useState(true); 25 | 26 | // Check if the user is validated already. 27 | if (process.browser) { 28 | const userValidated = isUserValidated(); 29 | 30 | // Redirect the user to My Account page if user is already validated. 31 | if (!isEmpty(userValidated)) { 32 | Router.push('/my-account'); 33 | } 34 | } 35 | 36 | /** 37 | * Hide the Status bar on cross button click. 38 | */ 39 | const onCloseButtonClick = () => { 40 | setErrorMessage(''); 41 | setShowAlertBar(false); 42 | }; 43 | 44 | /** 45 | * Sets client side error. 46 | * 47 | * Sets error data to result of our client side validation, 48 | * and statusbars to true so that its visible. 49 | * 50 | * @param {Object} validationResult Validation result data. 51 | */ 52 | const setClientSideError = (validationResult) => { 53 | if (validationResult.errors.password) { 54 | setErrorMessage(validationResult.errors.password); 55 | } 56 | 57 | if (validationResult.errors.email) { 58 | setErrorMessage(validationResult.errors.email); 59 | } 60 | 61 | if (validationResult.errors.username) { 62 | setErrorMessage(validationResult.errors.username); 63 | } 64 | 65 | setShowAlertBar(true); 66 | }; 67 | 68 | /** 69 | * Set server side error. 70 | * 71 | * Sets error data received as a response of our query from the server 72 | * and set statusbar to true so that its visible. 73 | * 74 | * @param {String} error Error 75 | * 76 | * @return {void} 77 | */ 78 | const setServerSideError = (error) => { 79 | setErrorMessage(error); 80 | setShowAlertBar(true); 81 | }; 82 | 83 | /** 84 | * Handles user registration. 85 | * 86 | * @param {object} event Event Object. 87 | * @param {object} registerUser registerUser function from REGISTER_USER mutation query. 88 | * @return {void} 89 | */ 90 | const handleRegister = async (event, registerUser) => { 91 | if (process.browser) { 92 | event.preventDefault(); 93 | 94 | // Validation and Sanitization. 95 | const validationResult = validateAndSanitizeRegisterForm({ username, email, password }); 96 | 97 | // If the data is valid. 98 | if (validationResult.isValid) { 99 | await registerUser({ 100 | variables: { 101 | username: validationResult.sanitizedData.username, 102 | email: validationResult.sanitizedData.email, 103 | password: validationResult.sanitizedData.password 104 | } 105 | }) 106 | .then((response) => handleRegisterSuccess(response)) 107 | .catch((err) => handleRegisterFail(err.graphQLErrors[0].message)); 108 | } else { 109 | setClientSideError(validationResult); 110 | } 111 | } 112 | }; 113 | 114 | /** 115 | * Handle Registration Fail. 116 | * 117 | * Set the error message text and validated to false. 118 | * 119 | * @param {String} err Error message received 120 | * @return {void} 121 | */ 122 | const handleRegisterFail = (err) => { 123 | const error = err.split('_').join(' ').toUpperCase(); 124 | 125 | setServerSideError(error); 126 | }; 127 | 128 | /** 129 | * Handle Register success. 130 | * 131 | * @param {Object} response Response received. 132 | * @return {void} 133 | */ 134 | const handleRegisterSuccess = (response) => { 135 | if (response.data.registerUser.user.email) { 136 | // Set form fields value to empty. 137 | setErrorMessage(''); 138 | setUsername(''); 139 | setPassword(''); 140 | 141 | localStorage.setItem('registration-success', 'yes'); 142 | 143 | // Add a message. 144 | setSuccessMessage( 145 | 'Registration Successful! . You will be redirected to login page now...' 146 | ); 147 | 148 | setTimeout(() => { 149 | // Send the user to Login page. 150 | Router.push('/login?registered=true'); 151 | }, 3000); 152 | } 153 | }; 154 | 155 | const [ 156 | registerUser, 157 | { 158 | data: data, 159 | loading: loading, 160 | error: error, 161 | }, 162 | ] = useMutation(REGISTER_USER, { client }) 163 | 164 | return ( 165 | 166 |
167 | {/* Title */} 168 |

Register

169 | 170 | {/* Error Message */} 171 | {'' !== errorMessage 172 | ? showAlertBar && ( 173 | 178 | ) 179 | : ''} 180 | 181 | {'' !== successMessage 182 | ? showAlertBar && ( 183 | 188 | ) 189 | : ''} 190 | 191 | {/* Login Form */} 192 |
handleRegister(event, registerUser)}> 195 | {/* Username */} 196 |
197 | 200 | setUsername(event.target.value)} 207 | /> 208 |
209 | 210 | {/* Username */} 211 |
212 | 215 | setEmail(event.target.value)} 222 | /> 223 |
224 | 225 | {/* Password */} 226 |
227 | 230 | setPassword(event.target.value)} 237 | /> 238 |
239 | 240 | {/* Submit Button */} 241 |
242 | 248 | 249 | Login 250 | 251 |
252 | 253 | {/* Loading */} 254 | {loading ? : ''} 255 | 256 |
257 |
258 | ); 259 | }; 260 | 261 | export default Register; 262 | -------------------------------------------------------------------------------- /public/static/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtCamp/wp-decoupled/be03e99a4738179025739f5e1074a69d97504dd5/public/static/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/static/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtCamp/wp-decoupled/be03e99a4738179025739f5e1074a69d97504dd5/public/static/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtCamp/wp-decoupled/be03e99a4738179025739f5e1074a69d97504dd5/public/static/favicon.ico -------------------------------------------------------------------------------- /public/static/hero.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtCamp/wp-decoupled/be03e99a4738179025739f5e1074a69d97504dd5/public/static/hero.jpg -------------------------------------------------------------------------------- /public/static/manifest/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "WP Decoupled", 3 | "name": "WP Decoupled", 4 | "dir": "ltr", 5 | "lang": "en", 6 | "icons": [ 7 | { 8 | "src": "/static/favicon.ico", 9 | "sizes": "64x64 32x32 24x24 16x16", 10 | "type": "image/x-icon" 11 | }, 12 | { 13 | "src": "/static/android-chrome-192x192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "/static/android-chrome-512x512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | } 22 | ], 23 | "start_url": "/", 24 | "display": "standalone", 25 | "theme_color": "#2196F3", 26 | "background_color": "#2196F3" 27 | } 28 | -------------------------------------------------------------------------------- /public/static/nprogress.css: -------------------------------------------------------------------------------- 1 | /* Make clicks pass-through */ 2 | #nprogress { 3 | pointer-events: none; 4 | } 5 | 6 | #nprogress .bar { 7 | background: red; 8 | position: fixed; 9 | z-index: 1031; 10 | top: 0; 11 | left: 0; 12 | 13 | width: 100%; 14 | height: 5px; 15 | } 16 | 17 | /* Fancy blur effect */ 18 | #nprogress .peg { 19 | display: block; 20 | position: absolute; 21 | right: 0px; 22 | width: 100px; 23 | height: 100%; 24 | box-shadow: 0 0 10px red, 0 0 5px red; 25 | opacity: 1.0; 26 | 27 | -webkit-transform: rotate(3deg) translate(0px, -4px); 28 | -ms-transform: rotate(3deg) translate(0px, -4px); 29 | transform: rotate(3deg) translate(0px, -4px); 30 | } 31 | 32 | /* Remove these to get rid of the spinner */ 33 | #nprogress .spinner { 34 | display: block; 35 | position: fixed; 36 | z-index: 1031; 37 | top: 15px; 38 | right: 15px; 39 | } 40 | 41 | #nprogress .spinner-icon { 42 | width: 18px; 43 | height: 18px; 44 | box-sizing: border-box; 45 | 46 | border: solid 2px transparent; 47 | border-top-color: red; 48 | border-left-color: red; 49 | border-radius: 50%; 50 | 51 | -webkit-animation: nprogress-spinner 400ms linear infinite; 52 | animation: nprogress-spinner 400ms linear infinite; 53 | } 54 | 55 | .nprogress-custom-parent { 56 | overflow: hidden; 57 | position: relative; 58 | } 59 | 60 | .nprogress-custom-parent #nprogress .spinner, 61 | .nprogress-custom-parent #nprogress .bar { 62 | position: absolute; 63 | } 64 | 65 | @-webkit-keyframes nprogress-spinner { 66 | 0% { -webkit-transform: rotate(0deg); } 67 | 100% { -webkit-transform: rotate(360deg); } 68 | } 69 | @keyframes nprogress-spinner { 70 | 0% { transform: rotate(0deg); } 71 | 100% { transform: rotate(360deg); } 72 | } -------------------------------------------------------------------------------- /public/static/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtCamp/wp-decoupled/be03e99a4738179025739f5e1074a69d97504dd5/public/static/placeholder.png -------------------------------------------------------------------------------- /service-worker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Service worker scripts. 3 | */ 4 | 5 | importScripts('https://cdn.jsdelivr.net/npm/idb-keyval@3/dist/idb-keyval-iife.min.js'); 6 | 7 | const store = new idbKeyval.Store('GraphQL-Cache', 'PostResponses'); 8 | 9 | workbox.core.skipWaiting(); 10 | workbox.core.clientsClaim(); 11 | 12 | self.addEventListener('fetch', function (event) { 13 | if (event.request.method === 'POST') { 14 | event.waitUntil(event.respondWith(staleWhileRevalidate(event))); 15 | } 16 | }); 17 | 18 | workbox.routing.registerRoute( 19 | new RegExp('https://tekskools.com'), 20 | new workbox.strategies.StaleWhileRevalidate() 21 | ); 22 | 23 | // @todo Temporary, needs to work on routes. 24 | workbox.routing.registerRoute( 25 | new RegExp(/\/product.+/), 26 | new workbox.strategies.StaleWhileRevalidate() 27 | ); 28 | 29 | const preCacheFiles = self.__precacheManifest || []; 30 | 31 | preCacheFiles.push({ 32 | url: '/' 33 | }); 34 | 35 | workbox.precaching.precacheAndRoute(preCacheFiles); 36 | 37 | async function staleWhileRevalidate(event) { 38 | let cachedResponse = await getCache(event.request.clone()); 39 | let fetchPromise = fetch(event.request.clone()) 40 | .then((response) => { 41 | setCache(event.request.clone(), response.clone()); 42 | return response; 43 | }) 44 | .catch((err) => { 45 | // Handle error 46 | }); 47 | return cachedResponse ? Promise.resolve(cachedResponse) : fetchPromise; 48 | } 49 | 50 | async function serializeResponse(response) { 51 | let serializedHeaders = {}; 52 | for (var entry of response.headers.entries()) { 53 | serializedHeaders[entry[0]] = entry[1]; 54 | } 55 | let serialized = { 56 | headers: serializedHeaders, 57 | status: response.status, 58 | statusText: response.statusText 59 | }; 60 | serialized.body = await response.json(); 61 | return serialized; 62 | } 63 | 64 | async function setCache(request, response) { 65 | let body = await request.json(); 66 | let id = body.query.toString() + JSON.stringify(body.variables); 67 | 68 | var entry = { 69 | query: body.query, 70 | response: await serializeResponse(response), 71 | timestamp: Date.now() 72 | }; 73 | idbKeyval.set(id, entry, store); 74 | } 75 | 76 | async function getCache(request) { 77 | let data; 78 | try { 79 | let body = await request.json(); 80 | let id = body.query.toString() + JSON.stringify(body.variables); 81 | 82 | data = await idbKeyval.get(id, store); 83 | if (!data) return null; 84 | 85 | // Check cache max age. 86 | let cacheControl = request.headers.get('Cache-Control'); 87 | let maxAge = cacheControl ? parseInt(cacheControl.split('=')[1]) : 3600; 88 | if (Date.now() - data.timestamp > maxAge * 1000) { 89 | // Cache expired. Load from API endpoint 90 | return null; 91 | } 92 | 93 | // Load response from cache. 94 | return new Response(JSON.stringify(data.response.body), data.response); 95 | } catch (err) { 96 | return null; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/apollo/ApolloClient.js: -------------------------------------------------------------------------------- 1 | import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client'; 2 | 3 | const defaultOptions = { 4 | watchQuery: { 5 | fetchPolicy: 'no-cache', 6 | errorPolicy: 'ignore' 7 | }, 8 | query: { 9 | fetchPolicy: 'no-cache', 10 | errorPolicy: 'all' 11 | } 12 | }; 13 | 14 | const cache = new InMemoryCache({ 15 | resultCaching: false 16 | }); 17 | 18 | const link = createHttpLink({ 19 | uri: `${process.env.NEXT_PUBLIC_WOO_SITE_URL}/graphql` 20 | }); 21 | 22 | // Apollo GraphQL client. 23 | const client = new ApolloClient({ 24 | link, 25 | cache, 26 | defaultOptions, 27 | connectToDevTools: true 28 | }); 29 | 30 | export default client; 31 | -------------------------------------------------------------------------------- /src/components/Image.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import Img from 'next/image'; 3 | import PropTypes from 'prop-types'; 4 | 5 | const Image = (props) => { 6 | 7 | const [error, setError] = useState(false); 8 | 9 | const { 10 | src, 11 | width, 12 | height, 13 | alt, 14 | ...otherProps 15 | } = props; 16 | 17 | // URL to use if the actual image fails to load. 18 | const fallBackUrl = '/static/placeholder.png'; 19 | 20 | /** 21 | * Handles any error when loading the image. 22 | * 23 | * @return {void} 24 | */ 25 | const errorHandler = () => { 26 | setError(true); 27 | } 28 | 29 | return ( 30 | {alt} 37 | ); 38 | }; 39 | 40 | Image.propTypes = { 41 | src: PropTypes.string.isRequired, 42 | width: PropTypes.number, 43 | height: PropTypes.number, 44 | alt: PropTypes.string, 45 | layout: PropTypes.oneOf(['fixed', 'intrinsic', 'responsive', 'fill']), 46 | sizes: PropTypes.string, 47 | quality: PropTypes.number, 48 | priority: PropTypes.bool, 49 | objectFit: PropTypes.string, 50 | objectPosition: PropTypes.string, 51 | className: PropTypes.string, 52 | id: PropTypes.string 53 | } 54 | 55 | Image.defaultProps = { 56 | width: 240, 57 | height: 240, 58 | } 59 | 60 | export default Image; 61 | -------------------------------------------------------------------------------- /src/components/cart/AddToCartButton.js: -------------------------------------------------------------------------------- 1 | import { useState, useContext } from 'react'; 2 | import { AppContext } from '../context/AppContext'; 3 | import { addFirstProduct, updateCart } from '../../utils/cart-functions'; 4 | import Link from 'next/link'; 5 | 6 | const AddToCartButton = (props) => { 7 | const { product } = props; 8 | const [cart, setCart] = useContext(AppContext); 9 | const [showViewCart, setShowViewCart] = useState(false); 10 | 11 | /** 12 | * Handles adding items to the cart. 13 | * 14 | * @return {void} 15 | */ 16 | const handleAddToCartClick = () => { 17 | // If component is rendered client side. 18 | if (process.browser) { 19 | let existingCart = localStorage.getItem('wpd-cart'); 20 | 21 | // If cart has item(s) already, update existing or add new item. 22 | if (existingCart) { 23 | existingCart = JSON.parse(existingCart); 24 | 25 | const qtyToBeAdded = 1; 26 | 27 | const updatedCart = updateCart(existingCart, product, qtyToBeAdded); 28 | 29 | setCart(updatedCart); 30 | } else { 31 | /** 32 | * If No Items in the cart, create an empty array and add one. 33 | * @type {Array} 34 | */ 35 | const newCart = addFirstProduct(product); 36 | setCart(newCart); 37 | } 38 | 39 | // Show View Cart Button 40 | setShowViewCart(true); 41 | } 42 | }; 43 | 44 | return ( 45 | <> 46 | 49 | {showViewCart ? ( 50 | 51 | 52 | 53 | ) : ( 54 | '' 55 | )} 56 | 57 | ); 58 | }; 59 | 60 | export default AddToCartButton; 61 | -------------------------------------------------------------------------------- /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 | const [cart] = useContext(AppContext); 7 | const productsCount = null !== cart ? cart.totalProductsCount : ''; 8 | const totalPrice = null !== cart ? cart.totalProductsPrice : ''; 9 | 10 | return ( 11 | <> 12 | 13 | 14 |
15 | {totalPrice ? ( 16 | ${totalPrice.toFixed(2)} 17 | ) : ( 18 | '' 19 | )} 20 | 21 | 22 | {productsCount ? ( 23 | {productsCount} 24 | ) : ( 25 | '' 26 | )} 27 | 28 |
29 |
30 | 31 | 64 | 65 | ); 66 | }; 67 | 68 | export default CartIcon; 69 | -------------------------------------------------------------------------------- /src/components/cart/cart-page/CartBlocks.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { useContext } from 'react'; 3 | import { AppContext } from '../../context/AppContext'; 4 | import { removeItemFromCart } from '../../../utils/cart-functions'; 5 | import CartItem from './CartItem'; 6 | 7 | const CartBlocks = () => { 8 | const [cart, setCart] = useContext(AppContext); 9 | 10 | /* 11 | * Handle remove product click. 12 | * 13 | * @param {Object} event event 14 | * @param {Integer} Product Id. 15 | * 16 | * @return {void} 17 | */ 18 | const handleRemoveProductClick = (event, databaseId) => { 19 | const updatedCart = removeItemFromCart(databaseId); 20 | setCart(updatedCart); 21 | }; 22 | 23 | return ( 24 |
25 | {cart ? ( 26 |
27 |

Cart

28 | 29 | 30 | 31 | 36 | 39 | 42 | 45 | 46 | 47 | 48 | {cart.products.length && 49 | cart.products.map((item) => ( 50 | 56 | ))} 57 | 58 |
32 | 33 | 34 | Product 35 | 37 | Price 38 | 40 | Quantity 41 | 43 | Total 44 |
59 | 60 | {/*Cart Total*/} 61 |
62 |
63 |

Cart Totals

64 | 65 | 66 | 67 | 68 | 71 | 72 | 73 | 74 | 77 | 78 | 79 |
Subtotal 69 | ${cart.totalProductsPrice.toFixed(2)} 70 |
Total 75 | ${cart.totalProductsPrice.toFixed(2)} 76 |
80 | 81 | 87 | 88 |
89 |
90 |
91 | ) : ( 92 | '' 93 | )} 94 |
95 | ); 96 | }; 97 | 98 | export default CartBlocks; 99 | -------------------------------------------------------------------------------- /src/components/cart/cart-page/CartItem.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { updateCart } from '../../../utils/cart-functions'; 3 | import Image from '../../Image'; 4 | const CartItem = ({ item, handleRemoveProductClick, setCart }) => { 5 | const [productCount, setProductCount] = useState(item.qty); 6 | 7 | /* 8 | * When user changes the qty from product input update the cart in localStorage 9 | * Also update the cart in global context 10 | * 11 | * @param {Object} event event 12 | * 13 | * @return {void} 14 | */ 15 | const handleQtyChange = (event) => { 16 | if (process.browser) { 17 | const newQty = event.target.value; 18 | 19 | // Set the new qty in State 20 | setProductCount(newQty); 21 | 22 | let existingCart = localStorage.getItem('wpd-cart'); 23 | existingCart = JSON.parse(existingCart); 24 | 25 | // Update the cart in localStorage. 26 | const updatedCart = updateCart(existingCart, item, false, newQty); 27 | 28 | // Update the cart in global context 29 | setCart(updatedCart); 30 | } 31 | }; 32 | 33 | return ( 34 | 35 | 36 | handleRemoveProductClick(event, item.databaseId)}> 39 | 40 | 41 | 42 | 43 | {item?.image?.altText 49 | 50 | {item.name} 51 | ${item.price.toFixed(2)} 52 | 53 | {/* Qty Input */} 54 | 55 | 62 | 63 | {item.totalPrice.toFixed(2)} 64 | 65 | ); 66 | }; 67 | 68 | export default CartItem; 69 | -------------------------------------------------------------------------------- /src/components/checkout/Billing.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import countryList from './country-list'; 3 | import Error from './Error'; 4 | 5 | const Billing = ({ input, handleOnChange }) => { 6 | return ( 7 | 8 | {/*Name*/} 9 |
10 |
11 |
12 | 18 | 26 | 27 |
28 |
29 |
30 |
31 | 37 | 45 | 46 |
47 |
48 |
49 | {/* Company Name */} 50 |
51 | 52 | 60 | 61 |
62 | {/* Country */} 63 |
64 | 70 | 84 | 85 |
86 | {/* Street Address */} 87 |
88 | 94 | 103 | 104 |
105 | 114 |
115 | {/* Town/City */} 116 |
117 | 123 | 131 | 132 |
133 | {/* County */} 134 |
135 | 136 | 144 | 145 |
146 | {/* Post Code */} 147 |
148 | 154 | 162 | 163 |
164 | {/*Phone & Email*/} 165 |
166 |
167 |
168 | 174 | 182 | 183 |
184 |
185 |
186 |
187 | 193 | 201 | 202 |
203 |
204 |
205 | {/* Create an Account */} 206 |
207 | 216 |
217 |

Additional Information

218 | {/* Order Notes */} 219 |
220 | 221 |