├── .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 | [](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 | 
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 |
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 |
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 |
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 |
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 |
25 |
26 |
27 |
{product?.name}
28 |
29 |
30 | {product?.price}
31 |
32 |
33 |
34 |
35 |
36 |
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 |
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 |
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 | |
32 | |
33 |
34 | Product
35 | |
36 |
37 | Price
38 | |
39 |
40 | Quantity
41 | |
42 |
43 | Total
44 | |
45 |
46 |
47 |
48 | {cart.products.length &&
49 | cart.products.map((item) => (
50 |
56 | ))}
57 |
58 |
59 |
60 | {/*Cart Total*/}
61 |
62 |
63 |
Cart Totals
64 |
65 |
66 |
67 | Subtotal |
68 |
69 | ${cart.totalProductsPrice.toFixed(2)}
70 | |
71 |
72 |
73 | Total |
74 |
75 | ${cart.totalProductsPrice.toFixed(2)}
76 | |
77 |
78 |
79 |
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 |
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 |
229 |
230 |
231 |
232 | );
233 | };
234 |
235 | export default Billing;
236 |
--------------------------------------------------------------------------------
/src/components/checkout/CheckoutCartItem.js:
--------------------------------------------------------------------------------
1 | import Image from '../Image';
2 |
3 | const CheckoutCartItem = ({ item }) => {
4 | return (
5 |
6 |
7 |
13 | |
14 | {item.name} |
15 | ${item.totalPrice.toFixed(2)} |
16 |
17 | );
18 | };
19 |
20 | export default CheckoutCartItem;
21 |
--------------------------------------------------------------------------------
/src/components/checkout/CheckoutForm.js:
--------------------------------------------------------------------------------
1 | import { useState, useContext, Fragment } from 'react';
2 | import Billing from './Billing';
3 | import YourOrder from './YourOrder';
4 | import PaymentModes from './PaymentModes';
5 | import { AppContext } from '../context/AppContext';
6 | import validateAndSanitizeCheckoutForm from '../../validator/checkout';
7 |
8 | const CheckoutForm = () => {
9 | const [cart] = useContext(AppContext);
10 |
11 | const initialState = {
12 | firstName: '',
13 | lastName: '',
14 | companyName: '',
15 | country: '',
16 | streetAddressOne: '',
17 | streetAddressTwo: '',
18 | city: '',
19 | county: '',
20 | postCode: '',
21 | phone: '',
22 | email: '',
23 | createAccount: false,
24 | orderNotes: '',
25 | paymentMode: '',
26 | errors: null
27 | };
28 |
29 | const [input, setInput] = useState(initialState);
30 |
31 | /*
32 | * Handle form submit.
33 | *
34 | * @param {Object} event Event Object.
35 | *
36 | * @return {void}
37 | */
38 | const handleFormSubmit = (event) => {
39 | event.preventDefault();
40 | const result = validateAndSanitizeCheckoutForm(input);
41 | if (!result.isValid) {
42 | setInput({ ...input, errors: result.errors });
43 | }
44 | };
45 |
46 | /*
47 | * Handle onchange input.
48 | *
49 | * @param {Object} event Event Object.
50 | *
51 | * @return {void}
52 | */
53 | const handleOnChange = (event) => {
54 | if ('createAccount' === event.target.name) {
55 | const newState = { ...input, [event.target.name]: !input.createAccount };
56 | setInput(newState);
57 | } else {
58 | const newState = { ...input, [event.target.name]: event.target.value };
59 | setInput(newState);
60 | }
61 | };
62 |
63 | return (
64 |
65 | {cart ? (
66 |
91 | ) : (
92 | ''
93 | )}
94 |
95 | );
96 | };
97 |
98 | export default CheckoutForm;
99 |
--------------------------------------------------------------------------------
/src/components/checkout/Error.js:
--------------------------------------------------------------------------------
1 | const Error = ({ errors, fieldName }) => {
2 | return errors && errors.hasOwnProperty(fieldName) ? (
3 | {errors[fieldName]}
4 | ) : (
5 | ''
6 | );
7 | };
8 |
9 | export default Error;
10 |
--------------------------------------------------------------------------------
/src/components/checkout/PaymentModes.js:
--------------------------------------------------------------------------------
1 | import Error from './Error';
2 |
3 | const PaymentModes = ({ input, handleOnChange }) => {
4 | return (
5 |
6 |
7 | {/*Pay with Paypal*/}
8 |
9 |
19 |
20 | {/*Pay with Stripe*/}
21 |
22 |
32 |
33 | {/* Payment Instructions*/}
34 |
35 | Please send a check to Store Name, Store Street, Store Town, Store State / County,
36 | Store Postcode.
37 |
38 |
39 | );
40 | };
41 |
42 | export default PaymentModes;
43 |
--------------------------------------------------------------------------------
/src/components/checkout/YourOrder.js:
--------------------------------------------------------------------------------
1 | import { Fragment } from 'react';
2 | import CheckoutCartItem from './CheckoutCartItem';
3 |
4 | const YourOrder = ({ cart }) => {
5 | return (
6 |
7 | {cart ? (
8 |
9 | {/*Product Listing*/}
10 |
11 |
12 |
13 | |
14 |
15 | Product
16 | |
17 |
18 | Total
19 | |
20 |
21 |
22 |
23 | {cart.products.length &&
24 | cart.products.map((item) => (
25 |
26 | ))}
27 | {/*Total*/}
28 |
29 | |
30 | Subtotal |
31 |
32 | ${cart.totalProductsPrice.toFixed(2)}
33 | |
34 |
35 |
36 | |
37 | Total |
38 |
39 | ${cart.totalProductsPrice.toFixed(2)}
40 | |
41 |
42 |
43 |
44 |
45 | ) : (
46 | ''
47 | )}
48 |
49 | );
50 | };
51 |
52 | export default YourOrder;
53 |
--------------------------------------------------------------------------------
/src/components/checkout/country-list.js:
--------------------------------------------------------------------------------
1 | const countryList = [
2 | 'Afghanistan',
3 | 'Albania',
4 | 'Algeria',
5 | 'Andorra',
6 | 'Angola',
7 | 'Anguilla',
8 | 'Antigua & Barbuda',
9 | 'Argentina',
10 | 'Armenia',
11 | 'Aruba',
12 | 'Australia',
13 | 'Austria',
14 | 'Azerbaijan',
15 | 'Bahamas',
16 | 'Bahrain',
17 | 'Bangladesh',
18 | 'Barbados',
19 | 'Belarus',
20 | 'Belgium',
21 | 'Belize',
22 | 'Benin',
23 | 'Bermuda',
24 | 'Bhutan',
25 | 'Bolivia',
26 | 'Bosnia & Herzegovina',
27 | 'Botswana',
28 | 'Brazil',
29 | 'British Virgin Islands',
30 | 'Brunei',
31 | 'Bulgaria',
32 | 'Burkina Faso',
33 | 'Burundi',
34 | 'Cambodia',
35 | 'Cameroon',
36 | 'Canada',
37 | 'Cape Verde',
38 | 'Cayman Islands',
39 | 'Chad',
40 | 'Chile',
41 | 'China',
42 | 'Colombia',
43 | 'Congo',
44 | 'Cook Islands',
45 | 'Costa Rica',
46 | 'Cote D Ivoire',
47 | 'Croatia',
48 | 'Cruise Ship',
49 | 'Cuba',
50 | 'Cyprus',
51 | 'Czech Republic',
52 | 'Denmark',
53 | 'Djibouti',
54 | 'Dominica',
55 | 'Dominican Republic',
56 | 'Ecuador',
57 | 'Egypt',
58 | 'El Salvador',
59 | 'Equatorial Guinea',
60 | 'Estonia',
61 | 'Ethiopia',
62 | 'Falkland Islands',
63 | 'Faroe Islands',
64 | 'Fiji',
65 | 'Finland',
66 | 'France',
67 | 'French Polynesia',
68 | 'French West Indies',
69 | 'Gabon',
70 | 'Gambia',
71 | 'Georgia',
72 | 'Germany',
73 | 'Ghana',
74 | 'Gibraltar',
75 | 'Greece',
76 | 'Greenland',
77 | 'Grenada',
78 | 'Guam',
79 | 'Guatemala',
80 | 'Guernsey',
81 | 'Guinea',
82 | 'Guinea Bissau',
83 | 'Guyana',
84 | 'Haiti',
85 | 'Honduras',
86 | 'Hong Kong',
87 | 'Hungary',
88 | 'Iceland',
89 | 'India',
90 | 'Indonesia',
91 | 'Iran',
92 | 'Iraq',
93 | 'Ireland',
94 | 'Isle of Man',
95 | 'Israel',
96 | 'Italy',
97 | 'Jamaica',
98 | 'Japan',
99 | 'Jersey',
100 | 'Jordan',
101 | 'Kazakhstan',
102 | 'Kenya',
103 | 'Kuwait',
104 | 'Kyrgyz Republic',
105 | 'Laos',
106 | 'Latvia',
107 | 'Lebanon',
108 | 'Lesotho',
109 | 'Liberia',
110 | 'Libya',
111 | 'Liechtenstein',
112 | 'Lithuania',
113 | 'Luxembourg',
114 | 'Macau',
115 | 'Macedonia',
116 | 'Madagascar',
117 | 'Malawi',
118 | 'Malaysia',
119 | 'Maldives',
120 | 'Mali',
121 | 'Malta',
122 | 'Mauritania',
123 | 'Mauritius',
124 | 'Mexico',
125 | 'Moldova',
126 | 'Monaco',
127 | 'Mongolia',
128 | 'Montenegro',
129 | 'Montserrat',
130 | 'Morocco',
131 | 'Mozambique',
132 | 'Namibia',
133 | 'Nepal',
134 | 'Netherlands',
135 | 'Netherlands Antilles',
136 | 'New Caledonia',
137 | 'New Zealand',
138 | 'Nicaragua',
139 | 'Niger',
140 | 'Nigeria',
141 | 'Norway',
142 | 'Oman',
143 | 'Pakistan',
144 | 'Palestine',
145 | 'Panama',
146 | 'Papua New Guinea',
147 | 'Paraguay',
148 | 'Peru',
149 | 'Philippines',
150 | 'Poland',
151 | 'Portugal',
152 | 'Puerto Rico',
153 | 'Qatar',
154 | 'Reunion',
155 | 'Romania',
156 | 'Russia',
157 | 'Rwanda',
158 | 'Saint Pierre & Miquelon',
159 | 'Samoa',
160 | 'San Marino',
161 | 'Satellite',
162 | 'Saudi Arabia',
163 | 'Senegal',
164 | 'Serbia',
165 | 'Seychelles',
166 | 'Sierra Leone',
167 | 'Singapore',
168 | 'Slovakia',
169 | 'Slovenia',
170 | 'South Africa',
171 | 'South Korea',
172 | 'Spain',
173 | 'Sri Lanka',
174 | 'St Kitts & Nevis',
175 | 'St Lucia',
176 | 'St Vincent',
177 | 'St. Lucia',
178 | 'Sudan',
179 | 'Suriname',
180 | 'Swaziland',
181 | 'Sweden',
182 | 'Switzerland',
183 | 'Syria',
184 | 'Taiwan',
185 | 'Tajikistan',
186 | 'Tanzania',
187 | 'Thailand',
188 | "Timor L'Este",
189 | 'Togo',
190 | 'Tonga',
191 | 'Trinidad & Tobago',
192 | 'Tunisia',
193 | 'Turkey',
194 | 'Turkmenistan',
195 | 'Turks & Caicos',
196 | 'Uganda',
197 | 'Ukraine',
198 | 'United Arab Emirates',
199 | 'United Kingdom',
200 | 'United States',
201 | 'United States Minor Outlying Islands',
202 | 'Uruguay',
203 | 'Uzbekistan',
204 | 'Venezuela',
205 | 'Vietnam',
206 | 'Virgin Islands (US)',
207 | 'Yemen',
208 | 'Zambia',
209 | 'Zimbabwe'
210 | ];
211 |
212 | export default countryList;
213 |
--------------------------------------------------------------------------------
/src/components/context/AppContext.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | export const AppContext = React.createContext([{}, () => {}]);
3 |
4 | export const AppProvider = (props) => {
5 | const [cart, setCart] = useState(null);
6 |
7 | useEffect(() => {
8 | if (process.browser) {
9 | let cartData = localStorage.getItem('wpd-cart');
10 | cartData = null !== cartData ? JSON.parse(cartData) : '';
11 | setCart(cartData);
12 | }
13 | }, []);
14 |
15 | return {props.children};
16 | };
17 |
--------------------------------------------------------------------------------
/src/components/gallery/index.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import Image from '../Image';
3 |
4 | const Gallery = ({edges}) => {
5 |
6 | const [active, setActive] = useState(0);
7 |
8 | const lastImage = edges.length - 1;
9 |
10 | const prevIndex = active - 1 < 0 ? lastImage : active - 1;
11 | const nextIndex = active + 1 > lastImage ? 0 : active + 1;
12 |
13 | let galleryContent;
14 |
15 | edges.length ? edges.map( ({ node }, i ) => {
16 | let className = '';
17 | if ( active === i ) {
18 | className = 'active';
19 | galleryContent =
20 |
21 |
27 |
28 | }
29 | }) : null;
30 |
31 | return (
32 | <>
33 | {
34 | edges.length > 0 &&
35 |
36 | setActive(prevIndex)}>
37 | { galleryContent }
38 | setActive(nextIndex)}>
39 |
40 | }
41 | >
42 | )
43 | }
44 | export default Gallery
--------------------------------------------------------------------------------
/src/components/home/Categories.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 |
3 | const Categories = () => {
4 | return (
5 |
6 |
Shop by Category
7 |
59 |
60 | );
61 | };
62 |
63 | export default Categories;
64 |
--------------------------------------------------------------------------------
/src/components/home/Hero.js:
--------------------------------------------------------------------------------
1 | const Hero = () => (
2 |
3 |
4 |
Welcome
5 |
Welcome to the WP Decoupled Demo page.
6 |
7 | This is a frontend for a WooCommerce Store created with Next.js and WPGraphQL.
8 |
9 |
10 |
11 | );
12 |
13 | export default Hero;
14 |
--------------------------------------------------------------------------------
/src/components/layouts/Footer.js:
--------------------------------------------------------------------------------
1 | const Footer = () => (
2 |
55 | );
56 |
57 | export default Footer;
58 |
--------------------------------------------------------------------------------
/src/components/layouts/Header.js:
--------------------------------------------------------------------------------
1 | import Router from 'next/router';
2 | import NProgress from 'nprogress';
3 | import Nav from './Nav';
4 |
5 | Router.onRouteChangeStart = () => {
6 | NProgress.start();
7 | };
8 | Router.onRouteChangeComplete = () => {
9 | NProgress.done();
10 | };
11 |
12 | Router.onRouteChangeError = () => {
13 | NProgress.done();
14 | };
15 |
16 | const Header = () => ;
17 |
18 | export default Header;
19 |
--------------------------------------------------------------------------------
/src/components/layouts/Layout.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { AppProvider } from '../context/AppContext';
3 | import '../../styles/sass/styles.scss';
4 | import '../../styles/vendor/bootstrap.min.css';
5 | import Head from 'next/head';
6 | import Header from './Header';
7 | import Footer from './Footer';
8 |
9 | const Layout = (props) => {
10 | useEffect(() => {
11 | if ('serviceWorker' in navigator) {
12 | window.addEventListener('load', function () {
13 | navigator.serviceWorker
14 | .register('/service-worker.js', { scope: '/' })
15 | .then(function (registration) {
16 | // SW registered
17 | })
18 | .catch(function (registrationError) {
19 | // SW registration failed
20 | });
21 | });
22 | }
23 | }, []);
24 |
25 | return (
26 |
27 |
28 |
29 |
WP Decoupled
30 |
31 |
32 |
36 |
37 |
38 |
39 |
40 |
{props.children}
41 |
42 |
43 |
44 | );
45 | };
46 |
47 | export default Layout;
48 |
--------------------------------------------------------------------------------
/src/components/layouts/Menu.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import { isUserValidated, logoutUser } from '../../utils/auth-functions';
3 | import { useEffect, useState } from 'react';
4 | import isEmpty from '../../validator/isEmpty';
5 |
6 | const Menu = () => {
7 | const [loggedIn, setLoggedIn] = useState(false);
8 |
9 | const handleLogout = () => {
10 | if (process.browser) {
11 | logoutUser('/login');
12 | }
13 | };
14 |
15 | useEffect(() => {
16 | if (process.browser) {
17 | const userValidated = isUserValidated();
18 |
19 | // If user is not validated, then logout button should be shown.
20 | if (!isEmpty(userValidated)) {
21 | setLoggedIn(true);
22 | }
23 | }
24 | });
25 |
26 | return (
27 |
28 | -
29 |
30 | Login
31 |
32 |
33 | -
34 |
35 | Register
36 |
37 |
38 | {loggedIn ? (
39 | -
40 |
41 | Logout
42 |
43 |
44 | ) : (
45 | ''
46 | )}
47 |
48 | );
49 | };
50 |
51 | export default Menu;
52 |
--------------------------------------------------------------------------------
/src/components/layouts/Nav.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import CartIcon from '../cart/CartIcon';
3 | import Menu from './Menu';
4 |
5 | const Nav = () => {
6 | return (
7 |
27 | );
28 | };
29 |
30 | export default Nav;
31 |
--------------------------------------------------------------------------------
/src/components/message-alert/Loading.js:
--------------------------------------------------------------------------------
1 | const Loading = ({ message }) => {message}
;
2 |
3 | export default Loading;
4 |
--------------------------------------------------------------------------------
/src/components/message-alert/MessageAlert.js:
--------------------------------------------------------------------------------
1 | import DOMPurify from 'dompurify';
2 | import { wpdDecodeHtml } from '../../utils/commmon-functions';
3 |
4 | const MessageAlert = ({ message, success, onCloseButtonClick }) => {
5 | return (
6 |
7 |
14 |
18 |
19 | );
20 | };
21 |
22 | export default MessageAlert;
23 |
--------------------------------------------------------------------------------
/src/queries/auth/login.js:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 | import UserFragment from '../fragments/user';
3 | /**
4 | * Login user mutation query.
5 | */
6 | export default gql`
7 | mutation LoginUser($username: String!, $password: String!) {
8 | login(input: { clientMutationId: "uniqueId", username: $username, password: $password }) {
9 | authToken
10 | user {
11 | ...UserFragment
12 | userId
13 | nicename
14 | }
15 | }
16 | }
17 | ${UserFragment}
18 | `;
--------------------------------------------------------------------------------
/src/queries/auth/register.js:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 | import UserFragment from '../fragments/user';
3 |
4 | /**
5 | * Register user mutation query.
6 | */
7 | export default gql`
8 | mutation RegisterMyUser($username: String!, $email: String!, $password: String!) {
9 | registerUser(
10 | input: {
11 | clientMutationId: "CreateUser"
12 | username: $username
13 | email: $email
14 | password: $password
15 | }
16 | ) {
17 | user {
18 | ...UserFragment
19 | nicename
20 | }
21 | }
22 | }
23 | ${UserFragment}
24 | `;
--------------------------------------------------------------------------------
/src/queries/fragments/image.js:
--------------------------------------------------------------------------------
1 | const ImageFragment = `
2 | fragment ImageFragment on MediaItem {
3 | id
4 | uri
5 | title
6 | srcSet
7 | sourceUrl
8 | altText
9 | }
10 | `;
11 | export default ImageFragment;
--------------------------------------------------------------------------------
/src/queries/fragments/product.js:
--------------------------------------------------------------------------------
1 | import ImageFragment from './image';
2 |
3 | const ProductFragment = `
4 | fragment ProductFragment on Product {
5 | id
6 | databaseId
7 | averageRating
8 | slug
9 | description
10 | image {
11 | ...ImageFragment
12 | }
13 | name
14 | ... on SimpleProduct {
15 | price
16 | id
17 | }
18 | ... on VariableProduct {
19 | price
20 | id
21 | }
22 | ... on ExternalProduct {
23 | price
24 | id
25 | }
26 | ... on GroupProduct {
27 | products {
28 | nodes {
29 | ... on SimpleProduct {
30 | price
31 | }
32 | }
33 | }
34 | id
35 | }
36 | galleryImages {
37 | edges {
38 | node {
39 | ...ImageFragment
40 | }
41 | }
42 | }
43 | }
44 | ${ImageFragment}
45 | `;
46 | export default ProductFragment;
--------------------------------------------------------------------------------
/src/queries/fragments/user.js:
--------------------------------------------------------------------------------
1 | const UserFragment = `
2 | fragment UserFragment on User {
3 | id
4 | name
5 | email
6 | }
7 | `;
8 | export default UserFragment;
--------------------------------------------------------------------------------
/src/queries/get-product-slug.js:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 | import ProductFragment from './fragments/product';
3 |
4 | export default gql`
5 | query GET_PRODUCT_SLUGS {
6 | products: products {
7 | edges {
8 | node {
9 | ...ProductFragment
10 | }
11 | }
12 | }
13 | }
14 | ${ProductFragment}
15 | `;
--------------------------------------------------------------------------------
/src/queries/get-product.js:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 | import ProductFragment from './fragments/product';
3 |
4 | export default gql`
5 | query Product($slug: ID!) {
6 | product(id: $slug, idType: SLUG) {
7 | ...ProductFragment
8 | }
9 | }
10 | ${ProductFragment}
11 | `;
--------------------------------------------------------------------------------
/src/queries/get-products.js:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 | import ProductFragment from './fragments/product';
3 |
4 | export default gql`
5 | query {
6 | products(first: 50) {
7 | nodes {
8 | ...ProductFragment
9 | }
10 | }
11 | }
12 | ${ProductFragment}
13 | `;
--------------------------------------------------------------------------------
/src/queries/index.js:
--------------------------------------------------------------------------------
1 | export { default as PRODUCTS_QUERY } from './get-products';
2 | export { default as PRODUCT_QUERY } from './get-product';
3 | export { default as LOGIN_USER } from './auth/login';
4 | export { default as REGISTER_USER } from './auth/register';
5 | export { default as PRODUCT_SLUGS } from './get-product-slug';
--------------------------------------------------------------------------------
/src/styles/sass/cart.scss:
--------------------------------------------------------------------------------
1 | .wd-cart-wrapper {
2 |
3 | overflow: scroll;
4 |
5 | & .wd-cart-heading {
6 | padding-bottom: 16px;
7 | }
8 | }
9 |
10 | .wd-cart-head-container {
11 |
12 | line-height: 3.2;
13 | background-color: #f8f8f8;
14 |
15 | & .wd-cart-heading-el {
16 | font-size: 16px;
17 | }
18 | }
19 |
20 | .wd-cart-item {
21 | & .wd-cart-element {
22 | vertical-align: middle;
23 | font-size: 14px;
24 | }
25 |
26 | & .wd-cart-el-close {
27 | text-align: center;
28 | font-size: 22px;
29 |
30 | & .wd-cart-close-icon {
31 | cursor: pointer;
32 | }
33 | }
34 | }
35 |
36 | .wd-cart-total-container {
37 | display: flex;
38 | justify-content: flex-end;
39 | margin-top: 48px;
40 | }
41 |
42 | .wd-cart-total-container {
43 | & .wd-cart-element-total {
44 | background-color: #f8f8f8;
45 | font-size: 16px;
46 | font-weight: 600;
47 | padding-left: 20px;
48 | }
49 |
50 | & .wd-cart-element-amt {
51 | background-color: #fdfdfd;
52 | font-size: 16px;
53 | padding-left: 20px;
54 | }
55 |
56 | & .wd-cart-checkout-txt {
57 | padding-right: 12px;
58 | }
59 | }
60 |
61 | .wd-cart-item {
62 | & .wd-cart-qty-input {
63 | padding: 9px 5px;
64 | width: 60px;
65 | text-align: center;
66 | background-color: #f2f2f2;
67 | color: #43454b;
68 | outline: 0;
69 | border: 0;
70 | -webkit-appearance: none;
71 | box-sizing: border-box;
72 | font-weight: 400;
73 | box-shadow: inset 0 1px 1px rgba(0,0,0,.125);
74 |
75 | &:focus {
76 | box-shadow: none;
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/styles/sass/checkout.scss:
--------------------------------------------------------------------------------
1 | .required {
2 | color: #ff0000;
3 | padding-left: 5px;
4 | }
5 |
6 | .wd-checkout-form {
7 | & .wd-checkout-input {
8 | background-color: #f2f2f2;
9 | height: 42px;
10 | padding: 0 16px;
11 | }
12 |
13 | & .wd-checkout-textarea {
14 | background-color: #f2f2f2;
15 | padding: 0 16px;
16 | }
17 |
18 | & .wd-checkout-total {
19 | font-size: 18px;
20 | }
21 |
22 | & .wd-payment-input-container {
23 | background-color: #f5f5f5;
24 | padding: 20px 48px;
25 |
26 | & .wd-payment-content {
27 | font-size: 16px;
28 | padding-left: 6px;
29 | }
30 | }
31 |
32 | & .wd-checkout-payment-instructions {
33 | padding: 24px 28px;
34 | background-color: #fafafa;
35 | font-size: 14px;
36 | }
37 |
38 | & .wd-place-order-btn-wrap {
39 |
40 | padding: 16px;
41 | background-color: #fafafa;
42 |
43 | & .wd-place-order-btn {
44 | padding: 18px 0;
45 | font-size: 22px;
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/styles/sass/common.scss:
--------------------------------------------------------------------------------
1 | a:focus,
2 | button:focus {
3 | user-select: none;
4 | outline: none !important;
5 | }
6 |
7 | body, div.wd-content {
8 | display: flex;
9 | flex-direction: column;
10 | min-height: 60vh;
11 | padding: 0;
12 | }
13 |
14 | ul.row {
15 | list-style: none;
16 | }
17 |
18 |
19 | .btn.wd-large-black-btn {
20 | width: 100%;
21 | background-color: #2c2d33;
22 | border-color: #2c2d33;
23 | color: #fff;
24 | font-size: 18px;
25 | font-weight: 800;
26 |
27 | &:hover {
28 | background-color: #3d3d46;
29 | color: #fff;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/styles/sass/core/_colors.scss:
--------------------------------------------------------------------------------
1 | $white: #fff;
2 | $light_gray: #f8f8f8;
3 | $light_gray-secondary: #818181;
4 |
5 | $color__border-light-gray: #dee2e6;
6 |
--------------------------------------------------------------------------------
/src/styles/sass/gallery.scss:
--------------------------------------------------------------------------------
1 | .product-gallery {
2 | position: relative;
3 | display: flex;
4 | justify-content: center;
5 |
6 | & div {
7 | display: none;
8 | }
9 |
10 | & .active {
11 | display: block;
12 | }
13 |
14 | & .arrowIcon {
15 | position: absolute;
16 | top:50%;
17 | font-size: 28px;
18 | cursor: pointer;
19 | }
20 |
21 | & .nextImage {
22 | right: 0;
23 | }
24 |
25 | & .prevImage {
26 | left: 0;
27 | }
28 | }
29 |
30 | .ImageAnimation {
31 | animation: galleryFadeIn 0.8s;
32 | }
33 |
34 | @keyframes galleryFadeIn {
35 | 0% {
36 | opacity: 0;
37 | }
38 | 100% {
39 | opacity: 1;
40 | }
41 | }
42 |
43 | .galleryFadeIn {
44 | -webkit-animation-name: galleryFadeIn;
45 | animation-name: galleryFadeIn;
46 | }
47 |
48 |
--------------------------------------------------------------------------------
/src/styles/sass/home.scss:
--------------------------------------------------------------------------------
1 | .woocommerce-loop-category__title {
2 | font-size: 1.1em;
3 | line-height: 1.214;
4 | font-weight: 400;
5 | text-align: center;
6 | color: #333333;
7 | margin-bottom: .5em;
8 | margin-top: .5em;
9 | }
10 |
11 |
12 | .hero {
13 | background: url("/static/hero.jpg");
14 | height: 420px;
15 | background-size: cover;
16 | display: flex;
17 | justify-content: center;
18 | background-position: center;
19 | }
20 |
21 | .wp-block-cover__inner-container {
22 | width: calc(6 * (100vw / 12));
23 | max-width: calc(6 * (100vw / 12));
24 | padding-top: 6.8535260698em;
25 | padding-bottom: 6.8535260698em;
26 | h1 {
27 | font-size: 3.706325903em;
28 | margin-bottom: .2360828548em;
29 | }
30 | p.has-text-color {
31 | font-size: 1.25em;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/styles/sass/layouts/_forms.scss:
--------------------------------------------------------------------------------
1 | .wd-content .wd-form {
2 | width: 600px;
3 | }
4 |
--------------------------------------------------------------------------------
/src/styles/sass/layouts/_my-account.scss:
--------------------------------------------------------------------------------
1 | .wpd-my-account {
2 |
3 | position: relative;
4 |
5 | // Sidebar
6 | .wpd-my-account-sidebar {
7 | position: absolute;
8 | width: 180px;
9 | height: auto;
10 | max-height: 400px;
11 | z-index: 1;
12 | top: 0;
13 | left: 0;
14 | background-color: $light_gray;
15 | overflow-x: hidden;
16 | padding-top: 16px;
17 |
18 | .wpd-my-account-sidebar__link {
19 | padding: 6px 8px 6px 16px;
20 | text-decoration: none;
21 | font-size: 16px;
22 | color: $light_gray-secondary;
23 | display: block;
24 | transition: 0.3s;
25 |
26 | &:hover {
27 | opacity: 0.8;
28 | }
29 | }
30 |
31 | }
32 |
33 | .wpd-my-account__main {
34 | margin-left: 180px; // same as width of .wpd-my-account-sidebar
35 |
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/src/styles/sass/layouts/_nav.scss:
--------------------------------------------------------------------------------
1 | .wpd-main-nav {
2 | margin: 0;
3 | padding: 0;
4 | display: flex;
5 |
6 | .wpd-main-nav__list {
7 |
8 | list-style: none;
9 |
10 | .wpd-main-nav__link {
11 | text-decoration: none;
12 | color: $white;
13 | padding: 5px 10px;
14 | font-size: 16px;
15 | transition: 0.3s;
16 |
17 | &:hover {
18 | opacity: 0.8;
19 | color: $white;
20 | cursor: pointer;
21 | }
22 |
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/styles/sass/layouts/_nprogress.scss:
--------------------------------------------------------------------------------
1 | #nprogress {
2 |
3 | // Background bar
4 | .bar {
5 | height: 2px !important;
6 | border-top: 4px solid $light_gray !important;
7 |
8 | .peg {
9 | box-shadow: 0 0 2px $white, 0 0 5px $white !important;
10 | }
11 | }
12 |
13 | // Spinner
14 | .spinner {
15 | .spinner-icon {
16 | border-top-color: $light_gray !important;
17 | border-left-color: $white !important;
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/styles/sass/navbar.scss:
--------------------------------------------------------------------------------
1 | .wd-navbar-wrap {
2 | background-color: #2196F3;
3 |
4 | & .wd-navbar {
5 | max-width: 1147px;
6 | margin: auto;
7 | box-shadow: none;
8 | }
9 | }
10 |
11 | .bar {
12 | border-top: 10px solid #000000;
13 | display: grid;
14 | grid-template-columns: auto 1fr;
15 | justify-content: soace-space-between;
16 | align-items: stretch;
17 | @media (max-width: 1300px) {
18 | grid-template-columns: 1fr;
19 | justify-content: center;
20 | }
21 | position: fixed;
22 | width: 100%;
23 | }
24 | .sub-bar {
25 | display: grid;
26 | grid-template-columns: 1fr auto;
27 | border-bottom: 1px solid #777777;
28 | }
--------------------------------------------------------------------------------
/src/styles/sass/products.scss:
--------------------------------------------------------------------------------
1 | .wd-content {
2 | padding: 24px 10px 10px 14px;
3 | }
4 |
5 | .product-container {
6 | text-align: center;
7 | }
8 |
9 | @media screen and ( max-width: 500px ) {
10 | .product-container {
11 | min-width: 400px;
12 | }
13 | }
14 |
15 | @media screen and ( max-width: 400px ) {
16 | .product-container {
17 | min-width: 300px;
18 | }
19 | }
20 |
21 | .product-link {
22 | cursor: pointer;
23 | color: #333333;
24 | }
25 |
26 |
27 | .product-name {
28 | margin: 16px;
29 | }
30 | .product-price {
31 | margin: 0 0 20px 0;
32 | }
33 | .product-view-link {
34 | text-decoration: none;
35 | color: #555;
36 | border: 1px solid #555;
37 | padding: 10px 16px;
38 | background-color: #fff;
39 | margin-top: 10px;
40 | }
41 |
42 | .product-description {
43 | max-width: 500px;
44 | margin-top: 24px;
45 | text-align: left;
46 | }
47 |
48 | .wd-view-cart-btn {
49 | margin-left: 10px;
50 | }
51 |
52 | p.price {
53 | font-size: 1.41575em;
54 | margin: 1.41575em 0;
55 | }
--------------------------------------------------------------------------------
/src/styles/sass/styles.scss:
--------------------------------------------------------------------------------
1 | @import "core/colors";
2 | @import "common";
3 | @import "layouts/nprogress";
4 | @import "layouts/forms";
5 | @import "layouts/nav";
6 | @import "navbar";
7 | @import "home";
8 | @import "products";
9 | @import "cart";
10 | @import "checkout";
11 | @import "layouts/my-account";
12 | @import "gallery";
13 |
--------------------------------------------------------------------------------
/src/utils/auth-functions.js:
--------------------------------------------------------------------------------
1 | import isEmpty from '../validator/isEmpty';
2 | import Router from 'next/router';
3 |
4 | /**
5 | * Check if user is logged in.
6 | *
7 | * @return {object} Auth Object containing token and user data, false on failure.
8 | */
9 | export const isUserValidated = () => {
10 | let authTokenData = localStorage.getItem(process.env.RT_WP_DECOUPLED_USER_TOKEN);
11 | let userLoggedInData = '';
12 |
13 | if (!isEmpty(authTokenData)) {
14 | authTokenData = JSON.parse(authTokenData);
15 |
16 | if (!isEmpty(authTokenData.authToken)) {
17 | userLoggedInData = authTokenData;
18 | }
19 | }
20 |
21 | return userLoggedInData;
22 | };
23 |
24 | /**
25 | * Logout the user.
26 | *
27 | * @param {string} urlToRedirect URL where user needs to be redirected after logout.
28 | *
29 | * @return {void}
30 | */
31 | export const logoutUser = (urlToRedirect) => {
32 | // Set auth data value in localStorage to empty.
33 | localStorage.setItem(process.env.RT_WP_DECOUPLED_USER_TOKEN, '');
34 |
35 | // Redirect the user to the given url.
36 | Router.push(urlToRedirect);
37 | };
38 |
--------------------------------------------------------------------------------
/src/utils/cart-functions.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Extracts and returns float value from a string.
3 | *
4 | * @param {string} string String
5 | * @return {any}
6 | */
7 | export const getFloatVal = (string) => {
8 | let floatValue = string.match(/[+-]?\d+(\.\d+)?/g)[0];
9 | return null !== floatValue ? parseFloat(parseFloat(floatValue).toFixed(2)) : '';
10 | };
11 |
12 | /**
13 | * Create a new product object.
14 | *
15 | * @param {Object} product Product
16 | * @param {Integer} productPrice Product Price
17 | * @param {Integer} qty Quantity
18 | * @return {{image: *, databaseId: *, totalPrice: number, price: *, qty: *, name: *}}
19 | */
20 | export const createNewProduct = (product, productPrice, qty) => {
21 | return {
22 | databaseId: product.databaseId,
23 | image: product.image,
24 | name: product.name,
25 | price: productPrice,
26 | qty,
27 | totalPrice: parseFloat((productPrice * qty).toFixed(2))
28 | };
29 | };
30 |
31 | /**
32 | * Add first product.
33 | *
34 | * @param {Object} product Product
35 | * @return {{totalProductsCount: number, totalProductsPrice: any, products: Array}}
36 | */
37 | export const addFirstProduct = (product) => {
38 | let productPrice = getFloatVal(product.price);
39 |
40 | let newCart = {
41 | products: [],
42 | totalProductsCount: 1,
43 | totalProductsPrice: parseFloat(productPrice.toFixed(2))
44 | };
45 |
46 | const newProduct = createNewProduct(product, productPrice, 1);
47 | newCart.products.push(newProduct);
48 |
49 | localStorage.setItem('wpd-cart', JSON.stringify(newCart));
50 |
51 | return newCart;
52 | };
53 |
54 | /**
55 | * Get updated products array
56 | * Update the product if it exists else,
57 | * add the new product to existing cart,
58 | *
59 | * @param {Object} existingProductsInCart Existing product in cart
60 | * @param {Object} product Product
61 | * @param {Integer} qtyToBeAdded Quantity
62 | * @param {Integer} newQty New qty of the product (optional)
63 | * @return {*[]}
64 | */
65 | export const getUpdatedProducts = (
66 | existingProductsInCart,
67 | product,
68 | qtyToBeAdded,
69 | newQty = false
70 | ) => {
71 | // Check if the product already exits in the cart.
72 | const productExitsIndex = isProductInCart(existingProductsInCart, product.databaseId);
73 |
74 | // If product exits ( index of that product found in the array ), update the product quantity and totalPrice
75 | if (-1 < productExitsIndex) {
76 | let updatedProducts = existingProductsInCart;
77 | let updatedProduct = updatedProducts[productExitsIndex];
78 |
79 | // If have new qty of the product available, set that else add the qtyToBeAdded
80 | updatedProduct.qty = newQty
81 | ? parseInt(newQty)
82 | : parseInt(updatedProduct.qty + qtyToBeAdded);
83 | updatedProduct.totalPrice = parseFloat(
84 | (updatedProduct.price * updatedProduct.qty).toFixed(2)
85 | );
86 |
87 | return updatedProducts;
88 | } else {
89 | // If product not found push the new product to the existing product array.
90 | let productPrice = getFloatVal(product.price);
91 | const newProduct = createNewProduct(product, productPrice, qtyToBeAdded);
92 | existingProductsInCart.push(newProduct);
93 |
94 | return existingProductsInCart;
95 | }
96 | };
97 |
98 | /**
99 | * Updates the existing cart with new item.
100 | *
101 | * @param {Object} existingCart Existing Cart.
102 | * @param {Object} product Product.
103 | * @param {Integer} qtyToBeAdded Quantity.
104 | * @param {Integer} newQty New Qty to be updated.
105 | * @return {{totalProductsCount: *, totalProductsPrice: *, products: *}}
106 | */
107 | export const updateCart = (existingCart, product, qtyToBeAdded, newQty = false) => {
108 | const updatedProducts = getUpdatedProducts(
109 | existingCart.products,
110 | product,
111 | qtyToBeAdded,
112 | newQty
113 | );
114 |
115 | const addPrice = (total, item) => {
116 | total.totalPrice += item.totalPrice;
117 | total.qty += item.qty;
118 |
119 | return total;
120 | };
121 |
122 | // Loop through the updated product array and add the totalPrice of each item to get the totalPrice
123 | let total = updatedProducts.reduce(addPrice, { totalPrice: 0, qty: 0 });
124 |
125 | const updatedCart = {
126 | products: updatedProducts,
127 | totalProductsCount: parseInt(total.qty),
128 | totalProductsPrice: parseFloat(total.totalPrice)
129 | };
130 |
131 | localStorage.setItem('wpd-cart', JSON.stringify(updatedCart));
132 |
133 | return updatedCart;
134 | };
135 |
136 | /**
137 | * Returns index of the product if it exists.
138 | *
139 | * @param {Object} existingProductsInCart Existing Products.
140 | * @param {Integer} databaseId Product id.
141 | * @return {number | *} Index Returns -1 if product does not exist in the array, index number otherwise
142 | */
143 | const isProductInCart = (existingProductsInCart, databaseId) => {
144 | const returnItemThatExits = (item, index) => {
145 | if (databaseId === item.databaseId) {
146 | return item;
147 | }
148 | };
149 |
150 | // This new array will only contain the product which is matched.
151 | const newArray = existingProductsInCart.filter(returnItemThatExits);
152 |
153 | return existingProductsInCart.indexOf(newArray[0]);
154 | };
155 |
156 | /**
157 | * Remove Item from the cart.
158 | *
159 | * @param {Integer} databaseId Product Id.
160 | * @return {any | string} Updated cart
161 | */
162 | export const removeItemFromCart = (databaseId) => {
163 | let existingCart = localStorage.getItem('wpd-cart');
164 | existingCart = JSON.parse(existingCart);
165 |
166 | // If there is only one item in the cart, delete the cart.
167 | if (1 === existingCart.products.length) {
168 | localStorage.removeItem('wpd-cart');
169 | return null;
170 | }
171 |
172 | // Check if the product already exits in the cart.
173 | const productExitsIndex = isProductInCart(existingCart.products, databaseId);
174 |
175 | // If product to be removed exits
176 | if (-1 < productExitsIndex) {
177 | const productTobeRemoved = existingCart.products[productExitsIndex];
178 | const qtyToBeRemovedFromTotal = productTobeRemoved.qty;
179 | const priceToBeDeductedFromTotal = productTobeRemoved.totalPrice;
180 |
181 | // Remove that product from the array and update the total price and total quantity of the cart
182 | let updatedCart = existingCart;
183 | updatedCart.products.splice(productExitsIndex, 1);
184 | updatedCart.totalProductsCount = updatedCart.totalProductsCount - qtyToBeRemovedFromTotal;
185 | updatedCart.totalProductsPrice =
186 | updatedCart.totalProductsPrice - priceToBeDeductedFromTotal;
187 |
188 | localStorage.setItem('wpd-cart', JSON.stringify(updatedCart));
189 | return updatedCart;
190 | } else {
191 | return existingCart;
192 | }
193 | };
194 |
--------------------------------------------------------------------------------
/src/utils/commmon-functions.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Converts the html entities to html.
3 | *
4 | * @param {string} html HTML with entities.
5 | *
6 | * @return {string} html with converted html entities.
7 | */
8 | export const wpdDecodeHtml = (html) => {
9 | const txt = document.createElement('textarea');
10 | txt.innerHTML = html;
11 | return txt.value;
12 | };
13 |
--------------------------------------------------------------------------------
/src/validator/checkout.js:
--------------------------------------------------------------------------------
1 | import validator from 'validator';
2 | import isEmpty from './isEmpty';
3 |
4 | const validateAndSanitizeCheckoutForm = (data) => {
5 | let errors = {};
6 | let sanitizedData = {};
7 |
8 | /**
9 | * Set the firstName value equal to an empty string if user has not entered the firstName, otherwise the Validator.isEmpty() wont work down below.
10 | * Note that the isEmpty() here is our custom function defined in is-empty.js and
11 | * Validator.isEmpty() down below comes from validator library.
12 | * Similarly we do it for for the rest of the fields
13 | */
14 | data.firstName = !isEmpty(data.firstName) ? data.firstName : '';
15 | data.lastName = !isEmpty(data.lastName) ? data.lastName : '';
16 | data.companyName = !isEmpty(data.companyName) ? data.companyName : '';
17 | data.country = !isEmpty(data.country) ? data.country : '';
18 | data.streetAddressOne = !isEmpty(data.streetAddressOne) ? data.streetAddressOne : '';
19 | data.streetAddressTwo = !isEmpty(data.streetAddressTwo) ? data.streetAddressTwo : '';
20 | data.city = !isEmpty(data.city) ? data.city : '';
21 | data.county = !isEmpty(data.county) ? data.county : '';
22 | data.postCode = !isEmpty(data.postCode) ? data.postCode : '';
23 | data.phone = !isEmpty(data.phone) ? data.phone : '';
24 | data.email = !isEmpty(data.email) ? data.email : '';
25 | data.createAccount = !isEmpty(data.createAccount) ? data.createAccount : '';
26 | data.orderNotes = !isEmpty(data.orderNotes) ? data.orderNotes : '';
27 | data.paymentMode = !isEmpty(data.paymentMode) ? data.paymentMode : '';
28 |
29 | /**
30 | * Checks for error if required is true
31 | * and adds Error and Sanitized data to the errors and sanitizedData object
32 | *
33 | * @param {String} fieldName Field name e.g. First name, last name
34 | * @param {String} errorContent Error Content to be used in showing error e.g. First Name, Last Name
35 | * @param {Integer} min Minimum characters required
36 | * @param {Integer} max Maximum characters required
37 | * @param {String} type Type e.g. email, phone etc.
38 | * @param {boolean} required Required if required is passed as false, it will not validate error and just do sanitization.
39 | */
40 | const addErrorAndSanitizedData = (fieldName, errorContent, min, max, type = '', required) => {
41 | const postCodeLocale = process?.env?.POST_CODE_LOCALE ?? '';
42 | /**
43 | * Please note that this isEmpty() belongs to validator and not our custom function defined above.
44 | *
45 | * Check for error and if there is no error then sanitize data.
46 | */
47 | if (!validator.isLength(data[fieldName], { min, max })) {
48 | errors[fieldName] = `${errorContent} must be ${min} to ${max} characters`;
49 | }
50 |
51 | if ('email' === type && !validator.isEmail(data[fieldName])) {
52 | errors[fieldName] = `${errorContent} is not valid`;
53 | }
54 |
55 | if ('phone' === type && !validator.isMobilePhone(data[fieldName])) {
56 | errors[fieldName] = `${errorContent} is not valid`;
57 | }
58 |
59 | if (
60 | 'postCode' === type &&
61 | postCodeLocale &&
62 | !validator.isPostalCode(data[fieldName], postCodeLocale)
63 | ) {
64 | errors[fieldName] = `${errorContent} is not valid`;
65 | }
66 |
67 | if (required && validator.isEmpty(data[fieldName])) {
68 | errors[fieldName] = `${errorContent} is required`;
69 | }
70 |
71 | // If no errors
72 | if (!errors[fieldName]) {
73 | sanitizedData[fieldName] = validator.trim(data[fieldName]);
74 | sanitizedData[fieldName] =
75 | 'email' === type
76 | ? validator.normalizeEmail(sanitizedData[fieldName])
77 | : sanitizedData[fieldName];
78 | sanitizedData[fieldName] = validator.escape(sanitizedData[fieldName]);
79 | }
80 | };
81 |
82 | addErrorAndSanitizedData('firstName', 'First name', 2, 35, 'string', true);
83 | addErrorAndSanitizedData('lastName', 'Last name', 2, 35, 'string', true);
84 | addErrorAndSanitizedData('companyName', 'Company Name', 0, 35, 'string', false);
85 | addErrorAndSanitizedData('country', 'Country name', 2, 55, 'string', true);
86 | addErrorAndSanitizedData('streetAddressOne', 'Street address line 1', 35, 100, 'string', true);
87 | addErrorAndSanitizedData('streetAddressTwo', '', 0, 254, 'string', false);
88 | addErrorAndSanitizedData('city', 'City field', 3, 25, 'string', true);
89 | addErrorAndSanitizedData('county', '', false);
90 | addErrorAndSanitizedData('postCode', 'Post code', 2, 9, 'postCode', true);
91 | addErrorAndSanitizedData('phone', 'Phone number', 10, 15, 'phone', true);
92 | addErrorAndSanitizedData('email', 'Email', 11, 254, 'email', true);
93 |
94 | // The data.createAccount is a boolean value.
95 | sanitizedData.createAccount = data.createAccount;
96 | addErrorAndSanitizedData('orderNotes', '', 0, 254, 'string', false);
97 | addErrorAndSanitizedData('paymentMode', 'Payment mode field', 2, 20, 'string', true);
98 |
99 | return {
100 | sanitizedData,
101 | errors,
102 | isValid: isEmpty(errors)
103 | };
104 | };
105 |
106 | export default validateAndSanitizeCheckoutForm;
107 |
--------------------------------------------------------------------------------
/src/validator/isEmpty.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns true if the value is undefined/null/empty object/empty string.
3 | *
4 | * @param value
5 | * @return {boolean}
6 | */
7 | const isEmpty = (value) =>
8 | value === undefined ||
9 | value === null ||
10 | (typeof value === 'object' && Object.keys(value).length === 0) ||
11 | (typeof value === 'string' && value.trim().length === 0);
12 |
13 | export default isEmpty;
14 |
--------------------------------------------------------------------------------
/src/validator/login.js:
--------------------------------------------------------------------------------
1 | import validator from 'validator';
2 | import isEmpty from './isEmpty';
3 |
4 | const validateAndSanitizeLoginForm = (data) => {
5 | let errors = {};
6 | let sanitizedData = {};
7 |
8 | /**
9 | * Set the username value equal to an empty string if user has not entered the username, otherwise the Validator.isEmpty() wont work down below.
10 | * Note that the isEmpty() here is our custom function defined in is-empty.js and
11 | * Validator.isEmpty() down below comes from validator library.
12 | * Similarly we do it for for the rest of the fields
13 | */
14 | data.username = !isEmpty(data.username) ? data.username : '';
15 | data.password = !isEmpty(data.password) ? data.password : '';
16 |
17 | /**
18 | * Checks for error if required is true
19 | * and adds Error and Sanitized data to the errors and sanitizedData object respectively.
20 | *
21 | * @param {String} fieldName Field name e.g. First name, last name
22 | * @param {String} errorContent Error Content to be used in showing error e.g. First Name, Last Name
23 | * @param {Integer} min Minimum characters required
24 | * @param {Integer} max Maximum characters required
25 | * @param {String} type Type e.g. email, phone etc.
26 | * @param {boolean} required Required if required is passed as false, it will not validate error and just do sanitization.
27 | */
28 | const addErrorAndSanitizedData = (fieldName, errorContent, min, max, type = '', required) => {
29 | /**
30 | * Please note that this isEmpty() belongs to validator and not our custom function defined above.
31 | *
32 | * Check for error and if there is no error then sanitize data.
33 | */
34 | if (!validator.isLength(data[fieldName], { min, max })) {
35 | errors[fieldName] = `${errorContent} must be ${min} to ${max} characters`;
36 | }
37 |
38 | if (required && validator.isEmpty(data[fieldName])) {
39 | errors[fieldName] = `${errorContent} is required`;
40 | }
41 |
42 | // If no errors
43 | if (!errors[fieldName]) {
44 | sanitizedData[fieldName] = validator.trim(data[fieldName]);
45 | sanitizedData[fieldName] = validator.escape(sanitizedData[fieldName]);
46 | }
47 | };
48 |
49 | addErrorAndSanitizedData('username', 'Username', 2, 35, 'string', true);
50 | addErrorAndSanitizedData('password', 'Password', 2, 35, 'string', true);
51 |
52 | return {
53 | sanitizedData,
54 | errors,
55 | isValid: isEmpty(errors)
56 | };
57 | };
58 |
59 | export default validateAndSanitizeLoginForm;
60 |
--------------------------------------------------------------------------------
/src/validator/register.js:
--------------------------------------------------------------------------------
1 | import validator from 'validator';
2 | import isEmpty from './isEmpty';
3 |
4 | const validateAndSanitizeRegisterForm = (data) => {
5 | let errors = {};
6 | let sanitizedData = {};
7 |
8 | /**
9 | * Set the username value equal to an empty string if user has not entered the username, otherwise the Validator.isEmpty() wont work down below.
10 | * Note that the isEmpty() here is our custom function defined in is-empty.js and
11 | * Validator.isEmpty() down below comes from validator library.
12 | * Similarly we do it for for the rest of the fields
13 | */
14 | data.username = !isEmpty(data.username) ? data.username : '';
15 | data.email = !isEmpty(data.email) ? data.email : '';
16 | data.password = !isEmpty(data.password) ? data.password : '';
17 |
18 | /**
19 | * Checks for error if required is true
20 | * and adds Error and Sanitized data to the errors and sanitizedData object respectively.
21 | *
22 | * @param {String} fieldName Field name e.g. First name, last name
23 | * @param {String} errorContent Error Content to be used in showing error e.g. First Name, Last Name
24 | * @param {Integer} min Minimum characters required
25 | * @param {Integer} max Maximum characters required
26 | * @param {String} type Type e.g. email, phone etc.
27 | * @param {boolean} required Required if required is passed as false, it will not validate error and just do sanitization.
28 | */
29 | const addErrorAndSanitizedData = (fieldName, errorContent, min, max, type = '', required) => {
30 | /**
31 | * Please note that this isEmpty() belongs to validator and not our custom function defined above.
32 | *
33 | * Check for error and if there is no error then sanitize data.
34 | */
35 | if (!validator.isLength(data[fieldName], { min, max })) {
36 | errors[fieldName] = `${errorContent} must be ${min} to ${max} characters`;
37 | }
38 |
39 | if ('email' === type && !validator.isEmail(data[fieldName])) {
40 | errors[fieldName] = `${errorContent} is not valid`;
41 | }
42 |
43 | if (required && validator.isEmpty(data[fieldName])) {
44 | errors[fieldName] = `${errorContent} is required`;
45 | }
46 |
47 | // If no errors
48 | if (!errors[fieldName]) {
49 | sanitizedData[fieldName] = validator.trim(data[fieldName]);
50 | sanitizedData[fieldName] =
51 | 'email' === type
52 | ? validator.normalizeEmail(sanitizedData[fieldName])
53 | : sanitizedData[fieldName];
54 | sanitizedData[fieldName] = validator.escape(sanitizedData[fieldName]);
55 | }
56 | };
57 |
58 | addErrorAndSanitizedData('username', 'Username', 2, 35, 'string', true);
59 | addErrorAndSanitizedData('email', 'Email', 11, 50, 'email', true);
60 | addErrorAndSanitizedData('password', 'Password', 2, 35, 'string', true);
61 |
62 | return {
63 | sanitizedData,
64 | errors,
65 | isValid: isEmpty(errors)
66 | };
67 | };
68 |
69 | export default validateAndSanitizeRegisterForm;
70 |
--------------------------------------------------------------------------------