🫢Sử dụng Nextjs kết nối store woocomerce, blog, category post, category product (ok)
Last updated
Was this helpful?
Last updated
Was this helpful?
Đây là bản gốc
Đây là bản typescript
Chú ý: lấy trường cụ thể cần thiết dùng _fields
https://dev-lionel1.pantheonsite.io/wp-json/wp/v2/posts/?_fields=author,id,excerpt,title,link,featured_media_src_url,featured_media&categories=1
Kết nối online
wooConfig.js
const wooConfig = {
siteUrl: 'https://dev-lionel1.pantheonsite.io',
consumerKey: 'ck_a07b018d4bc7fca36953f4d151ad1a3db40e5355',
consumerSecret: 'cs_fd29f1ab86335a85fec504bd6671fa68e9690875',
};
export default wooConfig;
package.json
{
"name": "app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@woocommerce/woocommerce-rest-api": "^1.0.1",
"isomorphic-unfetch": "^4.0.2",
"next": "15.1.6",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"postcss": "^8",
"tailwindcss": "^3.4.1"
}
}
client-config.js
const clientConfig = {
siteUrl: 'http://localhost:3000'
};
export default clientConfig;
.env
NEXT_PUBLIC_WORDPRESS_URL=https://dev-lionel1.pantheonsite.io
NEXT_PUBLIC_SITE_URL=http://localhost:3000
NODE_TLS_REJECT_UNAUTHORIZED=0
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxx
STRIPE_SECRET_KEY=sk_test_xxx
STRIPE_WEBHOOK_ENDPOINT_SECRET=whsec_xxxx
#WC_CONSUMER_KEY=ck_f5105208271bd7770348fa592b160233260e8c20
#WC_CONSUMER_KEY=ck_a4d07c9f2ac8cb1e02779810a35644591dc4a7b6
WC_CONSUMER_KEY=ck_a07b018d4bc7fca36953f4d151ad1a3db40e5355
#WC_CONSUMER_SECRET=cs_aaad5023a5b20f206b2ab8f14776845bd6997df8
#WC_CONSUMER_SECRET=cs_89ba76c650eac0aff8baf1623dc4074147c2e2b8
WC_CONSUMER_SECRET=cs_fd29f1ab86335a85fec504bd6671fa68e9690875
Kết nối local
wooConfig.js
const wooConfig = {
// siteUrl: 'https://dev-lionel1.pantheonsite.io',
// consumerKey: 'ck_a07b018d4bc7fca36953f4d151ad1a3db40e5355',
// consumerSecret: 'cs_fd29f1ab86335a85fec504bd6671fa68e9690875',
siteUrl: 'https://wpclidemo.dev',
consumerKey: 'ck_f5105208271bd7770348fa592b160233260e8c20',
consumerSecret: 'cs_aaad5023a5b20f206b2ab8f14776845bd6997df8',
};
export default wooConfig;
client-config.js
const clientConfig = {
siteUrl: 'http://localhost:3000'
};
export default clientConfig;
.env
#NEXT_PUBLIC_WORDPRESS_URL=https://dev-lionel1.pantheonsite.io
NEXT_PUBLIC_WORDPRESS_URL=https://wpclidemo.dev
NEXT_PUBLIC_SITE_URL=http://localhost:3000
NODE_TLS_REJECT_UNAUTHORIZED=0
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxx
STRIPE_SECRET_KEY=sk_test_xxx
STRIPE_WEBHOOK_ENDPOINT_SECRET=whsec_xxxx
WC_CONSUMER_KEY=ck_f5105208271bd7770348fa592b160233260e8c20
WC_CONSUMER_SECRET=cs_aaad5023a5b20f206b2ab8f14776845bd6997df8
#WC_CONSUMER_KEY=ck_a4d07c9f2ac8cb1e02779810a35644591dc4a7b6
#WC_CONSUMER_SECRET=cs_89ba76c650eac0aff8baf1623dc4074147c2e2b8
#WC_CONSUMER_KEY=ck_a07b018d4bc7fca36953f4d151ad1a3db40e5355
#WC_CONSUMER_SECRET=cs_fd29f1ab86335a85fec504bd6671fa68e9690875
pages\index.js
import Layout from "@/components/Layout";
import fetch from 'isomorphic-unfetch';
import Product from "@/components/Product";
import clientConfig from "@/client-config";
import { useEffect, useState } from "react";
export default function Home(props) {
const [listproducts, setProduct] = useState([]);
useEffect(() => {
fetch(`${clientConfig.siteUrl}/api/getproducts`)
.then((r) => r.json())
.then((data) => {
setProduct(data);
});
},[]);
return (
<Layout>
<div className="container m-auto grid grid-cols-4 gap-2 mt-6">
{listproducts.length ? (listproducts.map(product => <Product key={product.id} product={product} />)) : ''}
</div>
</Layout>
);
}
pages\api\getproducts.js
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import wooConfig from "@/wooConfig";
const WooCommerceRestApi = require("@woocommerce/woocommerce-rest-api").default;
const WooCommerce = new WooCommerceRestApi({
url: wooConfig.siteUrl,
consumerKey: wooConfig.consumerKey,
consumerSecret: wooConfig.consumerSecret,
version: 'wc/v3',
queryStringAuth: true
});
export default function handler(req, res) {
WooCommerce.get("products")
.then((response) => {
res.json(response.data)
})
.catch((error) => {
console.log(error);
});
}
components\Footer.js
import Nav from "./Nav";
const Footer = () => {
return (
<div className="text-center p-2 bg-slate-300 text-black mt-5">
<p>Copyright© Lionel</p>
</div>
)
};
export default Footer;
components\Header.js
import Nav from "./Nav";
const Header = () => {
return (
<div>
<Nav/>
</div>
)
};
export default Header;
components\Layout.js
import Head from 'next/head';
import Header from "./Header";
import Footer from "./Footer";
const Layout = ( props ) => {
return (
<div>
<Head>
<title>Woocommerce React Theme</title>
</Head>
<Header/>
{ props.children }
<Footer />
</div>
);
};
export default Layout;
components\Nav.js
const Nav = () => {
return (
<nav className="navbar navbar-expand-lg navbar-dark bg-black">
<ul className="flex items-center">
<li className="p-2 nav-item active">
<a className="nav-link text-white" href="#">WooNext <span className="sr-only">(current)</span></a>
</li>
<li className="p-2 nav-item">
<a className="nav-link text-white" href="#">Categories</a>
</li>
<li className="p-2 nav-item">
<a className="nav-link text-white" href="#">My Account</a>
</li>
</ul>
</nav>
)
};
export default Nav;
components\Product.js
const Product = ( props ) => {
const { product } = props;
return (
<div className="shadow border text-center">
<h3 className="card-header">{product.name}</h3>
<img src={product.images[0]?.src ?? '/23.jpg'} className="h-[465px] w-full object-cover" alt="Product image"/>
<div className="card-body p-1">
<h6 className="card-subtitle mb-3 text-black">Price{ product.price }</h6>
</div>
<a href="#" className=" bg-black text-white hover:text-blue-600 p-4 shadow-lg block ">View</a>
</div>
);
}
export default Product;
pages\blog\index.js
/**
* External Dependencies.
*/
import axios from 'axios';
/**
* Internal Dependencies.
*/
import Layout from '@/components/Layout';
import Posts from '@/components/posts';
import Pagination from '@/components/pagination';
import { HEADER_FOOTER_ENDPOINT } from '@/utils/constants/endpoints';
import { getPosts } from '@/utils/blog';
/**
* Blog Component.
*
* @param {Object} headerFooter Header Footer Data.
* @param {Object} postsData Post Data.
*/
const Blog = ( { headerFooter, postsData } ) => {
const seo = {
title: 'Blog Page',
description: 'Blog Page',
og_image: [],
og_site_name: 'React WooCommerce Theme',
robots: {
index: 'index',
follow: 'follow',
},
}
return (
<Layout headerFooter={ headerFooter || {} } seo={ seo }>
<h1>Blog</h1>
<Posts posts={ postsData?.posts_data ?? [] }/>
<Pagination pagesCount={ postsData?.page_count ?? 0 } postName="blog"/>
</Layout>
);
};
export default Blog;
export async function getStaticProps() {
const { data: headerFooterData } = await axios.get( HEADER_FOOTER_ENDPOINT );
const { data: postsData } = await getPosts();
return {
props: {
headerFooter: headerFooterData?.data ?? {},
postsData: postsData || {},
},
/**
* Revalidate means that if a new request comes to server, then every 1 sec it will check
* if the data is changed, if it is changed then it will update the
* static file inside .next folder with the new data, so that any 'SUBSEQUENT' requests should have updated data.
*/
revalidate: 1,
};
}
pages\blog\[slug].js
/**
* External Dependencies.
*/
import { isEmpty } from 'lodash';
import { useRouter } from 'next/router';
import axios from 'axios';
/**
* Internal Dependencies.
*/
import Layout from '@/components/Layout';
import { handleRedirectsAndReturnData, FALLBACK } from './../../utils/slug';
import { getFormattedDate, sanitize } from '@/utils/miscellaneous';
import { HEADER_FOOTER_ENDPOINT } from '@/utils/constants/endpoints';
import { getComments, getPost, getPosts } from '@/utils/blog';
import Image from '@/components/image';
import PostMeta from '@/components/post-meta';
import Comments from '@/components/comments';
const Post = ( { headerFooter, postData, commentsData } ) => {
const router = useRouter();
/**
* If the page is not yet generated, this will be displayed
* initially until getStaticProps() finishes running
*/
if ( router.isFallback ) {
return <div>Loading...</div>;
}
return (
<Layout headerFooter={ headerFooter || {} } seo={ postData?.yoast_head_json ?? {} }>
<div className="w-4/5 m-auto mb-8">
<figure className="mb-4 overflow-hidden">
<Image
sourceUrl={ postData?._embedded[ 'wp:featuredmedia' ]?.[ 0 ]?.source_url ?? '/12.jpg' }
title={ postData?.title?.rendered ?? '' }
width="600"
height="400"
containerClassNames="w-full h-600px"
/>
</figure>
<PostMeta date={ getFormattedDate( postData?.date ?? '' ) } authorName={ postData?._embedded?.author?.[0]?.name ?? '' }/>
<h1 dangerouslySetInnerHTML={ { __html: sanitize( postData?.title?.rendered ?? '' ) } }/>
<div dangerouslySetInnerHTML={ { __html: sanitize( postData?.content?.rendered ?? '' ) } }/>
<Comments comments={ commentsData } postId={ postData?.id ?? '' }/>
</div>
</Layout>
);
};
export default Post;
export async function getStaticProps( { params } ) {
const { data: headerFooterData } = await axios.get( HEADER_FOOTER_ENDPOINT );
const postData = await getPost( params?.slug ?? '' );
const commentsData = await getComments( postData?.[0]?.id ?? 0 );
const defaultProps = {
props: {
headerFooter: headerFooterData?.data ?? {},
postData: postData?.[0] ?? {},
commentsData: commentsData || []
},
/**
* Revalidate means that if a new request comes to server, then every 1 sec it will check
* if the data is changed, if it is changed then it will update the
* static file inside .next folder with the new data, so that any 'SUBSEQUENT' requests should have updated data.
*/
revalidate: 1,
};
return handleRedirectsAndReturnData( defaultProps, postData );
}
/**
* Since the page name 'does not' use catch-all routes,
* for example [slug],
* that's why params would contain just slug and not an array of slugs , unlike [...slug].
* For example, If we need to have dynamic route '/foo/'
* Then we would add paths: [ params: { slug: 'foo' } } ]
* Here slug will be 'foo', then Next.js will statically generate the page at /foo/
*
* At build time next js will make an api call get the data and
* generate a page bar.js inside .next/foo directory, so when the page is served on browser
* data is already present, unlike getInitialProps which gets the page at build time but makes an api
* call after page is served on the browser.
*
* @see https://nextjs.org/docs/basic-features/data-fetching#the-paths-key-required
*
* @returns {Promise<{paths: [], fallback: boolean}>}
*/
export async function getStaticPaths() {
const { data: postsData } = await getPosts();
const pathsData = [];
postsData?.posts_data?.length && postsData?.posts_data?.map( post => {
if ( ! isEmpty( post?.slug ) ) {
pathsData.push( { params: { slug: post?.slug } } );
}
} );
return {
paths: pathsData,
fallback: FALLBACK,
};
}
pages\blog\page\[pageNo].js
/**
* External Dependencies.
*/
import { useRouter } from 'next/router';
import axios from 'axios';
/**
* Internal Dependencies.
*/
import { handleRedirectsAndReturnData } from '@/utils/slug';
import { getPosts } from '@/utils/blog';
import { HEADER_FOOTER_ENDPOINT } from '@/utils/constants/endpoints';
import Layout from '@/components/Layout';
import Posts from '@/components/posts';
import Pagination from '@/components/pagination';
const Page = ( { headerFooter, postsData } ) => {
const router = useRouter();
// Redirecting to /blog if we are on page 1
const pageNo = router?.query?.pageNo ?? 1;
if ( 'undefined' !== typeof window && '1' === pageNo ) {
router.push( '/blog' );
}
const seo = {
title: `Blog Page No ${pageNo}`,
description: 'Blog Page',
og_image: [],
og_site_name: 'React WooCommerce Theme',
robots: {
index: 'index',
follow: 'follow',
},
}
return (
<Layout headerFooter={ headerFooter || {} } seo={ seo }>
<h1>Blog</h1>
<Posts posts={ postsData?.posts_data ?? [] }/>
<Pagination pagesCount={ postsData?.page_count ?? 0 } postName="blog"/>
</Layout>
);
};
export default Page;
export async function getStaticProps( { params } ) {
// Note: pageNo data type is string
const { pageNo } = params || {};
const { data: headerFooterData } = await axios.get( HEADER_FOOTER_ENDPOINT );
const { data: postsData } = await getPosts( pageNo );
const defaultProps = {
props: {
headerFooter: headerFooterData?.data ?? {},
postsData: postsData || {},
},
/**
* Revalidate means that if a new request comes to server, then every 1 sec it will check
* if the data is changed, if it is changed then it will update the
* static file inside .next folder with the new data, so that any 'SUBSEQUENT' requests should have updated data.
*/
revalidate: 1,
};
return handleRedirectsAndReturnData( defaultProps, postsData, 'posts_data' );
}
export async function getStaticPaths() {
const { data: postsData } = await getPosts();
const pagesCount = postsData?.page_count ?? 0;
const paths = new Array(pagesCount)?.fill('')?.map( ( _, index ) => ( {
params: {
pageNo: ( index + 1 ).toString(),
},
} ) );
return {
paths: [ ...paths ],
fallback: false,
};
}
utils\constants\endpoints.js
export const HEADER_FOOTER_ENDPOINT = `${process.env.NEXT_PUBLIC_WORDPRESS_URL}/wp-json/rae/v1/header-footer?header_location_id=hcms-menu-header&footer_location_id=hcms-menu-footer`;
export const GET_POSTS_ENDPOINT = `${process.env.NEXT_PUBLIC_WORDPRESS_URL }/wp-json/rae/v1/posts`;
export const GET_POST_ENDPOINT = `${process.env.NEXT_PUBLIC_WORDPRESS_URL }/wp-json/wp/v2/posts`;
export const GET_PAGES_ENDPOINT = `${process.env.NEXT_PUBLIC_WORDPRESS_URL }/wp-json/wp/v2/pages`;
export const COMMENTS_ENDPOINT = `${process.env.NEXT_PUBLIC_WORDPRESS_URL }/wp-json/wp/v2/comments`;
utils\constants\images.js
export const DEFAULT_IMG_URL = 'https://placeholder.pics/svg/380x380';
utils\blog.js
/**
* External Dependencies.
*/
import axios from 'axios';
/**
* Internal Dependencies.
*/
import { COMMENTS_ENDPOINT, GET_PAGES_ENDPOINT, GET_POST_ENDPOINT, GET_POSTS_ENDPOINT } from './constants/endpoints';
/**
* Get Posts.
*
* @return {Promise<void>}
*/
export const getPosts = async (pageNo = 1) => {
return await axios.get(`${GET_POSTS_ENDPOINT}?page_no=${pageNo}`)
.then(res => {
if (200 === res.data.status) {
return res;
} else {
return {
posts_data: {},
error: 'Post not found',
};
}
})
.catch(err => {
return {
posts_data: {},
error: err?.response?.data?.message
};
});
};
/**
* Get Post By Slug.
*
* @return {Promise<void>}
*/
export const getPost = async (postSlug = '') => {
return await axios.get(`${GET_POST_ENDPOINT}?slug=${postSlug}&_embed`)
.then(res => {
if (200 === res.status) {
return res.data;
} else {
return [];
}
})
.catch(err => {
return [];
});
};
/**
* Get Pages.
*
* @return {Promise<void>}
*/
export const getPages = async () => {
return await axios.get(`${GET_PAGES_ENDPOINT}?_embed`)
.then(res => {
if (200 === res.status) {
return res.data;
} else {
return [];
}
})
.catch(err => {
return [];
});
};
/**
* Get Page By Slug.
*
* @return {Promise<void>}
*/
export const getPage = async (pageSlug = '') => {
return await axios.get(`${GET_PAGES_ENDPOINT}?slug=${pageSlug}&_embed`)
.then(res => {
if (200 === res.status) {
return res.data;
} else {
return [];
}
})
.catch(err => {
return [];
});
};
/**
* Get Post By Slug.
*
* @return {Promise<void>}
*/
export const getComments = async (postID = '') => {
return await axios.get(`${COMMENTS_ENDPOINT}?post=${postID}`)
.then(res => {
if (200 === res.status) {
return res.data;
} else {
return [];
}
})
.catch(err => {
return [];
});
};
/**
* Post a Comment
*
* POST Request.
*
* @return {Promise<void>}
*/
export const postComment = async (postID = '', data = {}) => {
return await axios.post(`${COMMENTS_ENDPOINT}`, {
post: data?.postId ?? 0,
parent: data?.parent ?? 0,
content: data?.comment ?? '',
author_email: data?.email ?? '',
date: data?.date ?? '',
author_url: data?.url ?? '',
author_name: data?.author ?? '',
},)
.then(res => {
if (200 === res.status || 201 === res.status) {
return {
success: true,
data: res.data,
error: '',
};
} else {
return {
success: false,
error: 'Failed. Please try again.',
};
}
})
.catch(err => {
return {
success: false,
error: err?.response?.data?.message,
};
});
};
utils\miscellaneous.js
import DOMPurify from 'dompurify';
/**
* Sanitize markup or text when used inside dangerouslysetInnerHTML
*
* @param {string} content Plain or html string.
*
* @return {string} Sanitized string
*/
export const sanitize = (content) => {
return 'undefined' !== typeof window ? DOMPurify.sanitize(content) : content;
};
/**
* Replace backend url with front-end url.
*
* @param {String} data Data.
*
* @return formattedData Formatted data.
*/
export const replaceBackendWithFrontendUrl = (data) => {
if (!data || 'string' !== typeof data) {
return '';
}
// First replace all the backend-url with front-end url
let formattedData = data.replaceAll(process.env.NEXT_PUBLIC_WORDPRESS_SITE_URL, process.env.NEXT_PUBLIC_SITE_URL);
// Replace only the upload urls for images to back-end url, since images are hosted in the backend.
return formattedData.replaceAll(`${process.env.NEXT_PUBLIC_SITE_URL}/wp-content/uploads`, `${process.env.NEXT_PUBLIC_WORDPRESS_SITE_URL}/wp-content/uploads`);
}
/**
* Get Formatted Date.
* @param {String} theDate The date to be formatted.
* @param {String} locales locales.
*
* @return {string} Formatted Date.
*/
export const getFormattedDate = (theDate = '', locales = 'en-us') => {
const options = { weekday: 'long', year: 'numeric', month: 'short', day: 'numeric' };
return new Date(theDate).toLocaleDateString(locales, options);
};
/**
* Get path name from url.
*
* @param {String} url URL.
*
* @return {String} URL pathname.
*/
export const getPathNameFromUrl = (url = '') => {
if (!url) {
return '';
}
const theURL = new URL(url);
return theURL.pathname;
}
/**
* Smooth Scroll.
*
* Scrolls the given element to the top of the screen
* minus the topOffset( when provided ), smoothly(with animation).
*
* @param {Object} targetEl element to be scrolled.
* @param {number} topOffset When target is clicked it will move up until this offset
* from the top of the screen.
* @param {number} duration Duration of scroll in milliseconds.
*
* @return {null|void} Null.
*/
export const smoothScroll = (targetEl, topOffset = 0, duration = 500) => {
if (!targetEl) {
return null;
}
const targetPosition = targetEl.getBoundingClientRect().top - topOffset;
const startPosition = window.scrollY; // Current height of the window.
let startTime = null;
const animationCallBack = (currentTime) => {
if (null === startTime) {
startTime = currentTime;
}
const timeElapsed = currentTime - startTime;
const runPosition = getAnimateWithEasePosition(timeElapsed, startPosition, targetPosition, duration);
window.scrollTo(0, runPosition);
if (timeElapsed < duration) {
window.requestAnimationFrame(animationCallBack);
}
};
window.requestAnimationFrame(animationCallBack);
};
/**
* Animation With Ease Position.
*
* @param {number} timeElapsed Time elapsed.
* @param {number} startPosition Start position.
* @param {number} targetPosition Target position.
* @param {number} duration Duration in milliseconds.
*
* @return {number} Position.
*/
const getAnimateWithEasePosition = (timeElapsed, startPosition, targetPosition, duration) => {
timeElapsed /= duration / 2;
if (1 > timeElapsed) {
return ((targetPosition / 2) * timeElapsed * timeElapsed) + startPosition;
}
timeElapsed--;
return -((targetPosition / 2) * ((timeElapsed * (timeElapsed - 2)) - 1)) + startPosition;
};
utils\pagination.js
/**
* Create Pagination Links Array.
*
* Example: [1, "...", 521, 522, 523, 524, 525, "...", 529]
*
* @param {int} currentPage Current page no.
* @param {int} totalPages Count of total no of pages.
*
* @return {Array} Array containing the indexes to be looped through to create pagination
*/
export const createPaginationLinks = ( currentPage, totalPages ) => {
const paginationArray = [];
let countOfDotItems = 0;
// If there is only one page, return an empty array.
if ( ! totalPages && 1 >= totalPages ) {
return paginationArray;
}
/**
* Push the two index items before the current page.
*/
if ( 0 < currentPage - 2 ) {
paginationArray.push( currentPage - 2 );
}
if ( 0 < currentPage - 1 ) {
paginationArray.push( currentPage - 1 );
}
// Push the current page index item.
paginationArray.push( currentPage );
/**
* Push the two index items after the current page.
*/
if ( totalPages >= currentPage + 1 ) {
paginationArray.push( currentPage + 1 );
}
if ( totalPages >= currentPage + 2 ) {
paginationArray.push( currentPage + 2 );
}
/**
* Push the '...' at the beginning of the array
* only if the difference of between the 1st and 2nd index item is greater than 1.
*/
if ( 1 < paginationArray[ 0 ] - 1 ) {
paginationArray.unshift( '...' );
countOfDotItems += 1;
}
/**
* Push the '...' at the end of the array.
* only if the difference of between the last and 2nd last item is greater than 2.
* We remove the count of dot items from the array to get the actual indexes, while checking the condition.
*/
if ( 2 < totalPages - paginationArray[ paginationArray.length - ( 2 - countOfDotItems ) ] ) {
paginationArray.push( '...' );
}
// Push first index item in the array if it does not already exists.
if ( -1 === paginationArray.indexOf( 1 ) ) {
paginationArray.unshift( 1 );
}
// Push last index item in the array if it does not already exists.
if ( -1 === paginationArray.indexOf( totalPages ) ) {
paginationArray.push( totalPages );
}
return paginationArray;
};
utils\slug.js
import { isEmpty } from 'lodash';
export const FALLBACK = 'blocking';
/**
* Check if it's a custom page uri.
*
*
* @param uri
* @return {boolean}
*/
export const isCustomPageUri = ( uri ) => {
const pagesToExclude = [
'/',
'/blog/',
'/checkout/',
'/cart/',
'/thank-you/',
];
return pagesToExclude.includes( uri );
};
/**
* Handle Redirect Data.
*
* @param defaultProps
* @param data
* @param field
* @return {{notFound: boolean}|*|{redirect: {destination: string, statusCode: number}}}
*/
export const handleRedirectsAndReturnData = ( defaultProps, data, field = '' ) => {
// If no data is available then redirect to 503.
if ( isEmpty( data ) ) {
return {
redirect: {
destination: '/503',
statusCode: 301,
},
};
}
// If data for a given field is not available, redirect to 404.
if ( field && isEmpty( data?.[ field ] ) ) {
return {
// returns the default 404 page with a status code of 404
notFound: true,
};
}
return defaultProps;
};
validator\is-empty.js
/**
* Returns true if the value is undefined/null/empty object/empty string.
*
* @param value
* @return {boolean}
*/
const isEmpty = (value) =>
value === undefined || value === null || (typeof value === 'object' && Object.keys(value).length === 0) || (typeof value === 'string' && value.trim().length === 0);
export default isEmpty;
validator\comments.js
import validator from 'validator';
import isEmpty from './is-empty';
const validateAndSanitizeCommentsForm = (data) => {
let errors = {};
let sanitizedData = {};
/**
* Set the name value equal to an empty string if user has not entered the name, otherwise the Validator.isEmpty() wont work down below.
* Note that the isEmpty() here is our custom function defined in is-empty.js and
* Validator.isEmpty() down below comes from validator library.
* Similarly, we do it for the rest of the fields
*/
data.comment = (!isEmpty(data.comment)) ? data.comment : '';
data.author = (!isEmpty(data.author)) ? data.author : '';
data.email = (!isEmpty(data.email)) ? data.email : '';
data.url = (!isEmpty(data.url)) ? data.url : '';
/**
* Checks for error if required is true
* and adds Error and Sanitized data to the errors and sanitizedData object
*
* @param {String} fieldName Field name e.g. First name, last name
* @param {String} errorContent Error Content to be used in showing error e.g. First Name, Last Name
* @param {Integer} min Minimum characters required
* @param {Integer} max Maximum characters required
* @param {String} type Type e.g. email, phone etc.
* @param {boolean} required Required if required is passed as false, it will not validate error and just do sanitization.
*/
const addErrorAndSanitizedData = (fieldName, errorContent, min, max, type = '', required) => {
/**
* Please note that this isEmpty() belongs to validator and not our custom function defined above.
*
* Check for error and if there is no error then sanitize data.
*/
if (!validator.isLength(data[fieldName], { min, max }) && required) {
errors[fieldName] = `${errorContent} must be ${min} to ${max} characters`;
}
if ('email' === type && !validator.isEmail(data[fieldName])) {
errors[fieldName] = `${errorContent} is not valid`;
}
if ('url' === type && !validator.isURL(data[fieldName]) && required) {
errors[fieldName] = `${errorContent} is not valid`;
}
if (required && validator.isEmpty(data[fieldName])) {
errors[fieldName] = `${errorContent} is required`;
}
// If no errors
if (!errors[fieldName]) {
sanitizedData[fieldName] = validator.trim(data[fieldName]);
sanitizedData[fieldName] = ('email' === type) ? validator.normalizeEmail(sanitizedData[fieldName]) : sanitizedData[fieldName];
sanitizedData[fieldName] = validator.escape(sanitizedData[fieldName]);
}
};
addErrorAndSanitizedData('comment', 'Comment', 12, 100, 'string', true);
addErrorAndSanitizedData('author', 'Author Name', 0, 35, 'string', true);
addErrorAndSanitizedData('email', 'Email', 11, 254, 'email', true);
addErrorAndSanitizedData('url', 'Site URL', 2, 55, 'url', false);
return {
sanitizedData,
errors,
isValid: isEmpty(errors)
}
};
export default validateAndSanitizeCommentsForm;
validator\checkout.js
import validator from 'validator';
import isEmpty from './is-empty';
const validateAndSanitizeCheckoutForm = ( data, hasStates = true ) => {
let errors = {};
let sanitizedData = {};
/**
* Set the firstName value equal to an empty string if user has not entered the firstName, otherwise the Validator.isEmpty() wont work down below.
* Note that the isEmpty() here is our custom function defined in is-empty.js and
* Validator.isEmpty() down below comes from validator library.
* Similarly we do it for for the rest of the fields
*/
data.firstName = ( ! isEmpty( data.firstName ) ) ? data.firstName : '';
data.lastName = ( ! isEmpty( data.lastName ) ) ? data.lastName : '';
data.company = ( ! isEmpty( data.company ) ) ? data.company : '';
data.country = ( ! isEmpty( data.country ) ) ? data.country : '';
data.address1 = ( ! isEmpty( data.address1 ) ) ? data.address1 : '';
data.address2 = ( ! isEmpty( data.address2 ) ) ? data.address2 : '';
data.city = ( ! isEmpty( data.city ) ) ? data.city : '';
data.state = ( ! isEmpty( data.state ) ) ? data.state : '';
data.postcode = ( ! isEmpty( data.postcode ) ) ? data.postcode : '';
data.phone = ( ! isEmpty( data.phone ) ) ? data.phone : '';
data.email = ( ! isEmpty( data.email ) ) ? data.email : '';
data.createAccount = ( ! isEmpty( data.createAccount ) ) ? data.createAccount : '';
data.orderNotes = ( ! isEmpty( data.orderNotes ) ) ? data.orderNotes : '';
// data.paymentMethod = ( ! isEmpty( data.paymentMethod ) ) ? data.paymentMethod : '';
/**
* Checks for error if required is true
* and adds Error and Sanitized data to the errors and sanitizedData object
*
* @param {String} fieldName Field name e.g. First name, last name
* @param {String} errorContent Error Content to be used in showing error e.g. First Name, Last Name
* @param {Integer} min Minimum characters required
* @param {Integer} max Maximum characters required
* @param {String} type Type e.g. email, phone etc.
* @param {boolean} required Required if required is passed as false, it will not validate error and just do sanitization.
*/
const addErrorAndSanitizedData = ( fieldName, errorContent, min, max, type = '', required ) => {
/**
* Please note that this isEmpty() belongs to validator and not our custom function defined above.
*
* Check for error and if there is no error then sanitize data.
*/
if ( ! validator.isLength( data[ fieldName ], { min, max } ) ){
errors[ fieldName ] = `${errorContent} must be ${min} to ${max} characters`;
}
if ( 'email' === type && ! validator.isEmail( data[ fieldName ] ) ){
errors[ fieldName ] = `${errorContent} is not valid`;
}
if ( 'phone' === type && ! validator.isMobilePhone( data[ fieldName ] ) ) {
errors[ fieldName ] = `${errorContent} is not valid`;
}
if ( required && validator.isEmpty( data[ fieldName ] ) ) {
errors[ fieldName ] = `${errorContent} is required`;
}
// If no errors
if ( ! errors[ fieldName ] ) {
sanitizedData[ fieldName ] = validator.trim( data[ fieldName ] );
sanitizedData[ fieldName ] = ( 'email' === type ) ? validator.normalizeEmail( sanitizedData[ fieldName ] ) : sanitizedData[ fieldName ];
sanitizedData[ fieldName ] = validator.escape( sanitizedData[ fieldName ] );
}
};
addErrorAndSanitizedData( 'firstName', 'First name', 2, 35, 'string', true );
addErrorAndSanitizedData( 'lastName', 'Last name', 2, 35, 'string', true );
addErrorAndSanitizedData( 'company', 'Company Name', 0, 35, 'string', false );
addErrorAndSanitizedData( 'country', 'Country name', 2, 55, 'string', true );
addErrorAndSanitizedData( 'address1', 'Street address line 1', 12, 100,'string',true );
addErrorAndSanitizedData( 'address2', '', 0, 254, 'string', false );
addErrorAndSanitizedData( 'city', 'City field', 3, 25, 'string', true );
addErrorAndSanitizedData( 'state', 'State/County', 0, 254, 'string', hasStates );
addErrorAndSanitizedData( 'postcode', 'Post code', 2, 10, 'postcode', true );
addErrorAndSanitizedData( 'phone', 'Phone number', 10, 15, 'phone', true );
addErrorAndSanitizedData( 'email', 'Email', 11, 254, 'email', true );
// The data.createAccount is a boolean value.
sanitizedData.createAccount = data.createAccount;
addErrorAndSanitizedData( 'orderNotes', '', 0, 254, 'string', false );
// @TODO Payment mode error to be handled later.
// addErrorAndSanitizedData( 'paymentMethod', 'Payment mode field', 2, 50, 'string', false );
return {
sanitizedData,
errors,
isValid: isEmpty( errors )
}
};
export default validateAndSanitizeCheckoutForm;
utils\constants\images.js
export const DEFAULT_IMG_URL = 'https://placeholder.pics/svg/380x380';
utils\constants\endpoints.js
export const HEADER_FOOTER_ENDPOINT = `${process.env.NEXT_PUBLIC_WORDPRESS_URL}/wp-json/rae/v1/header-footer?header_location_id=hcms-menu-header&footer_location_id=hcms-menu-footer`;
export const GET_POSTS_ENDPOINT = `${process.env.NEXT_PUBLIC_WORDPRESS_URL }/wp-json/rae/v1/posts`;
export const GET_POST_ENDPOINT = `${process.env.NEXT_PUBLIC_WORDPRESS_URL }/wp-json/wp/v2/posts`;
export const GET_PAGES_ENDPOINT = `${process.env.NEXT_PUBLIC_WORDPRESS_URL }/wp-json/wp/v2/pages`;
export const COMMENTS_ENDPOINT = `${process.env.NEXT_PUBLIC_WORDPRESS_URL }/wp-json/wp/v2/comments`;
components\loading.js
const Loading = ({ text }) => {
return (
<button disabled type="button"
className="p-4 text-sm text-blue-800 rounded-lg bg-blue-50 dark:bg-gray-800 dark:text-blue-400 w-full text-left">
<svg
aria-hidden="true"
role="status"
className="inline w-4 h-4 mr-3 text-gray-200 animate-spin dark:text-gray-600"
viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="#1C64F2"
/>
</svg>
{text ? text : 'Loading...'}
</button>
);
};
export default Loading;
components\posts\index.js
/**
* External Dependencies.
*/
import PropTypes from 'prop-types';
import { isEmpty, isArray } from 'lodash';
/**
* Internal Dependency.
*/
import Post from './post';
const Posts = ( { posts } ) => {
if ( isEmpty( posts ) && ! isArray( posts ) ) {
return null;
}
return (
<div className="flex flex-wrap -mb-4">
{
posts?.map( ( post, index ) => {
return (
<div
key={ `${ post?.id ?? '' }-${ index }` ?? '' }
className="w-full px-2 mb-4 md:w-1/2 lg:w-1/3"
>
<Post post={ post }/>
</div>
);
} )
}
</div>
);
};
Posts.propTypes = {
posts: PropTypes.array,
};
Posts.defaultProps = {
posts: [],
};
export default Posts;
components\posts\post.js
/**
* External Dependencies.
*/
import Link from 'next/link';
/**
* Internal Dependencies.
*/
import Image from '../image';
import { sanitize } from '@/utils/miscellaneous';
import PostMeta from '../post-meta';
/**
* Post Component.
*
* @param {Object} post Post.
*/
const Post = ({ post }) => {
return (
<div className="mb-8">
<Link href={`/blog/${post?.slug}/`}>
<figure className="mb-4 overflow-hidden">
<Image
sourceUrl={post?.attachment_image?.img_src?.[0] ?? '/12.jpg'}
title={post?.title ?? ''}
width="400"
height="225"
containerClassNames="w-96 sm:-w-600px md:w-400px h-56 sm:h-338px md:h-225px"
/>
</figure>
</Link>
<PostMeta date={post?.date ?? ''} authorName={post?.meta?.author_name ?? ''} />
<Link href={`/blog/${post?.slug}/`}>
<h2 className="mb-3 text-lg font-bold uppercase text-brand-gun-powder hover:text-blue-500" dangerouslySetInnerHTML={{ __html: sanitize(post?.title ?? '') }} />
</Link>
<div dangerouslySetInnerHTML={{ __html: sanitize(post?.excerpt ?? '') }} />
</div>
);
};
export default Post;
components\post-meta\index.js
const PostMeta = ({ date, authorName }) => {
return (
<div className="font-bold mb-2">
<time className="text-brand-wild-blue" dateTime={ date || '' }>{ date || '' }</time>
<span className="ml-2"><span className="italic mr-2">by</span>{ authorName || '' }</span>
</div>
);
}
export default PostMeta;
components\pagination\index.js
import Link from 'next/link';
import PropTypes from 'prop-types';
import { useRouter } from 'next/router';
import { createPaginationLinks } from '@/utils/pagination';
import cx from 'classnames';
import Previous from './previous';
import Next from './next';
const Pagination = ({ pagesCount, postName }) => {
if (!pagesCount || !postName) {
return null;
}
const router = useRouter();
const currentPageNo = parseInt(router?.query?.pageNo ?? 1) || 1;
const paginationLinks = createPaginationLinks(currentPageNo, pagesCount);
return (
<div className="flex justify-center my-8">
<Previous currentPageNo={currentPageNo} postName={postName} />
{paginationLinks.map((pageNo, index) => {
const paginationLink = `/${postName}/page/${pageNo}/`;
return (
'number' === typeof pageNo ? (
<Link key={`id-${index}`} href={paginationLink} className={cx('border border-gray-300 px-3 py-2 transition duration-500 ease-in-out hover:bg-gray-500 hover:text-white', {
'is-active bg-gray-500 text-white': pageNo === currentPageNo,
})}>
{pageNo}
</Link>
) : (
// If its "..."
<span key={`id-${index}`} className="px-3 py-2">{pageNo}</span>
)
);
})}
<Next currentPageNo={currentPageNo} pagesCount={pagesCount} postName={postName} />
</div>
);
};
Pagination.propTypes = {
pagesCount: PropTypes.number,
postName: PropTypes.string,
};
Pagination.defaultProps = {
pagesCount: 0,
postName: 'blog',
};
export default Pagination;
components\pagination\next.js
import { isEmpty } from 'lodash';
import Link from 'next/link';
const Next = ( { currentPageNo, pagesCount, postName } ) => {
if ( ! currentPageNo || ! pagesCount || isEmpty( postName ) ) {
return null;
}
// If you are on the last page, don't show next link.
if ( pagesCount < currentPageNo + 1 ) {
return null;
}
const paginationLink = `/${ postName }/page/${ currentPageNo + 1 }/`;
return (
<Link href={ paginationLink } className="border border-gray-300 px-3 py-2 ml-4 transition duration-500 ease-in-out hover:bg-gray-500 hover:text-white">
Next
</Link>
);
};
export default Next;
components\pagination\previous.js
import { isEmpty } from 'lodash';
import Link from 'next/link';
const Previous = ( { currentPageNo, postName } ) => {
if ( ! currentPageNo || isEmpty( postName ) ) {
return null;
}
// If you are on the first page, don't show previous link.
if ( 0 === currentPageNo - 1 ) {
return null;
}
const paginationLink = `/${ postName }/page/${ currentPageNo - 1 }/`;
return (
<Link href={ paginationLink } className="border border-gray-300 px-3 py-2 mr-4 transition duration-500 ease-in-out hover:bg-gray-500 hover:text-white">
Previous
</Link>
);
};
export default Previous;
components\image\index.js
import Img from 'next/image';
import PropTypes from 'prop-types';
import cx from 'classnames';
import { DEFAULT_IMG_URL } from './../../utils/constants/images';
/**
* Image Component.
* We don't need to add srcSet, as Next js will generate that.
* @see https://nextjs.org/docs/api-reference/next/image#other-props
* @see https://nextjs.org/docs/basic-features/image-optimization#device-sizes
*
* @param {Object} props Component props.
*
* @return {jsx}
*/
const Image = ( props ) => {
const {altText, title, width, height, sourceUrl, className, layout, objectFit, containerClassNames, showDefault, ...rest} = props;
if ( ! sourceUrl && ! showDefault ) {
return null;
}
/**
* If we use layout = fill then, width and height of the image cannot be used.
* and the image fills on the entire width and the height of its parent container.
* That's we need to wrap our image in a container and give it a height and width.
* Notice that in this case, the given height and width is being used for container and not img.
*/
if ( 'fill' === layout ) {
const attributes = {
alt: altText || title,
src: sourceUrl || ( showDefault ? DEFAULT_IMG_URL : '' ),
className: cx( 'object-cover', className ),
...rest
};
return (
<div className={cx( 'relative', containerClassNames ) }>
<Img {...attributes}/>
</div>
);
} else {
const attributes = {
alt: altText || title,
src: sourceUrl || ( showDefault ? DEFAULT_IMG_URL : '' ),
width: width || 'auto',
height: height || 'auto',
className,
...rest
};
return <Img {...attributes} />;
}
};
Image.propTypes = {
altText: PropTypes.string,
title: PropTypes.string,
sourceUrl: PropTypes.string,
layout: PropTypes.string,
showDefault: PropTypes.bool,
containerClassName: PropTypes.string,
className: PropTypes.string
};
Image.defaultProps = {
altText: '',
title: '',
sourceUrl: '',
showDefault: true,
containerClassNames: '',
className: 'product__image',
};
export default Image;
components\form-elements\checkbox.js
import PropTypes from 'prop-types';
import Error from './error';
import Abbr from './abbr';
const Checkbox = ( { handleOnChange, checked, name, errors, required, label, placeholder, containerClassNames, inputValue } ) => {
return (
<div className={ containerClassNames }>
<label className="leading-7 text-md text-gray-700 flex items-center cursor-pointer" htmlFor={ name }>
<input
type="checkbox"
onChange={ handleOnChange }
placeholder={ placeholder }
checked={ checked }
name={ name }
id={ name }
value={ checked }
/>
<span className="ml-2">{ label || '' }</span>
<Abbr required={ required }/>
</label>
<Error errors={ errors } fieldName={ name }/>
</div>
);
};
Checkbox.propTypes = {
handleOnChange: PropTypes.func,
checked: PropTypes.bool,
name: PropTypes.string,
type: PropTypes.string,
errors: PropTypes.object,
label: PropTypes.string,
placeholder: PropTypes.string,
containerClassNames: PropTypes.string,
};
Checkbox.defaultProps = {
handleOnChange: () => null,
checked: false,
name: '',
label: '',
placeholder: '',
errors: {},
containerClassNames: '',
};
export default Checkbox;
components\form-elements\error.js
const Error = ( { errors, fieldName } ) => {
return(
errors && ( errors.hasOwnProperty( fieldName ) ) ? (
<div className="invalid-feedback d-block text-red-500">{ errors[fieldName] }</div>
) : ''
)
};
export default Error;
components\form-elements\input.js
import Error from './error';
import PropTypes from 'prop-types';
import Abbr from './abbr';
const Input = ({ handleOnChange, inputValue, name, type, label, errors, placeholder, required, containerClassNames, inputId, }) => {
return (
<div className={containerClassNames}>
<label className="leading-7 text-sm text-gray-700" htmlFor={inputId}>
{label || ''}
<Abbr required={required} />
</label>
<input
onChange={handleOnChange}
value={inputValue || ''}
placeholder={placeholder || ''}
type={type || 'text'}
name={name || ''}
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 w-full block p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
id={inputId || name}
/>
<Error errors={errors} fieldName={name} />
</div>
);
};
Input.propTypes = {
handleOnChange: PropTypes.func,
inputValue: PropTypes.string,
name: PropTypes.string,
type: PropTypes.string,
label: PropTypes.string,
placeholder: PropTypes.string,
errors: PropTypes.object,
required: PropTypes.bool,
containerClassNames: PropTypes.string,
};
Input.defaultProps = {
handleOnChange: () => null,
inputValue: '',
name: '',
type: 'text',
label: '',
placeholder: '',
errors: {},
required: false,
containerClassNames: '',
};
export default Input;
components\form-elements\text-area.js
import Error from './error';
import PropTypes from 'prop-types';
import Abbr from './abbr';
const TextArea = ({ handleOnChange, textAreaValue, name, label, errors, placeholder, required, containerClassNames, textAreaId, cols, rows, maxLength }) => {
return (
<div className={containerClassNames}>
<label className="leading-7 text-sm text-gray-700" htmlFor={textAreaId}>
{label || ''}
<Abbr required={required} />
</label>
<textarea
onChange={handleOnChange}
value={textAreaValue || ''}
placeholder={placeholder || ''}
cols={cols || 45}
rows={rows || 5}
maxLength={maxLength || 65525}
name={name || ''}
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
id={textAreaId || name}
/>
<Error errors={errors} fieldName={name} />
</div>
);
};
TextArea.propTypes = {
handleOnChange: PropTypes.func,
textAreaValue: PropTypes.string,
name: PropTypes.string,
label: PropTypes.string,
placeholder: PropTypes.string,
errors: PropTypes.object,
required: PropTypes.bool,
containerClassNames: PropTypes.string,
};
TextArea.defaultProps = {
handleOnChange: () => null,
textAreaValue: '',
name: '',
label: '',
placeholder: '',
errors: {},
required: false,
containerClassNames: '',
};
export default TextArea;
components\comments\index.js
import { isArray, isEmpty } from 'lodash';
import Comment from './comment';
import CommentForm from './comment-form';
import { useRef, useState } from 'react';
import { smoothScroll } from '@/utils/miscellaneous';
const Comments = ({ comments, postId }) => {
if (isEmpty(comments) || !isArray(comments)) {
return null;
}
/**
* Initialize.
*/
const commentFormEl = useRef(null);
const [replyCommentID, setReplyCommentID] = useState(0);
/**
* Handle Reply Button Click.
*
* @param {Event} event Event.
* @param {number} commentId Comment Id.
*/
const handleReplyButtonClick = (event, commentId) => {
setReplyCommentID(commentId);
smoothScroll(commentFormEl.current, 20);
}
return (
<div className="mt-20">
<h2>{comments.length} Comments</h2>
{
comments.map((comment, index) => {
return (
<div
key={`${comment?.id ?? ''}-${index}` ?? ''}
className="comment"
>
<Comment comment={comment} handleReplyButtonClick={handleReplyButtonClick} />
</div>
);
})
}
<div ref={commentFormEl}>
<CommentForm postId={postId} replyCommentID={replyCommentID} />
</div>
</div>
)
}
export default Comments;
components\comments\comment.js
import { isEmpty } from 'lodash';
import Image from '../image';
import { sanitize, getFormattedDate } from '@/utils/miscellaneous';
const Comment = ({ comment, handleReplyButtonClick }) => {
if (isEmpty(comment)) {
return null;
}
return (
<article className="p-6 mb-6 text-base bg-white border-t border-gray-200 dark:border-gray-700 dark:bg-gray-900">
<footer className="flex justify-between items-center mb-4">
<div className="flex items-center">
<div className="inline-flex items-center mr-3 text-sm text-gray-900 dark:text-white">
<Image
sourceUrl={comment?.author_avatar_urls?.['48'] ?? ''}
title={comment?.author_name ?? ''}
width="24"
height="24"
containerClassNames="mr-2 w-9 h-9"
style={{ borderRadius: '50%', overflow: 'hidden' }}
/>
{comment?.author_name ?? ''}
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
<time dateTime={comment?.date ?? ''} title="March 12th, 2022">{getFormattedDate(comment?.date ?? '')}</time>
</div>
</div>
</footer>
<div
className="text-gray-500 dark:text-gray-400"
dangerouslySetInnerHTML={{ __html: sanitize(comment?.content?.rendered ?? '') }}
/>
<div className="flex items-center mt-4 space-x-4">
<button
type="button"
className="flex items-center text-sm text-gray-500 hover:underline dark:text-gray-400"
onClick={(event) => handleReplyButtonClick(event, comment.id)}
>
<svg aria-hidden="true" className="mr-1 w-4 h-4" fill="none" stroke="currentColor"
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z">
</path>
</svg>
Reply
</button>
</div>
</article>
)
}
export default Comment;
components\comments\comment-form.js
/**
* External Dependencies.
*/
import { useEffect, useState } from 'react';
import cx from 'classnames';
/**
* Internal Dependencies.
*/
import validateAndSanitizeCommentsForm from '@/validator/comments';
import TextArea from '../form-elements/text-area';
import Input from '../form-elements/input';
import { postComment } from '@/utils/blog';
import { sanitize } from '@/utils/miscellaneous';
import Loading from '../loading';
const CommentForm = ({ postId, replyCommentID }) => {
/**
* Initialize Input State.
*
* @type {{date: Date, postId: number, wp_comment_cookies_consent: boolean}}
*/
const initialInputState = {
postId: postId || 0,
date: new Date(),
parent: replyCommentID || 0,
}
const [input, setInput] = useState(initialInputState);
const [commentPostSuccess, setCommentPostSuccess] = useState(false);
const [commentPostError, setCommentPostError] = useState('');
const [clearFormValues, setClearFormValues] = useState(false);
const [loading, setLoading] = useState(false);
const submitBtnClasses = cx(
'text-white hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-16px uppercase w-full sm:w-auto px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800',
{
'cursor-pointer bg-blue-700': !loading,
'bg-blue-400 dark:bg-blue-500 cursor-not-allowed': loading,
},
);
/**
* When the reply Comment id gets updated
* then update the input.
*/
useEffect(() => {
setInput({
...input,
parent: replyCommentID || 0,
});
}, [replyCommentID]);
/**
* If 'clearFormValues' becomes true,
* reset the input value to initialInputState
*/
useEffect(() => {
if (clearFormValues) {
setInput(initialInputState);
}
}, [clearFormValues]);
/**
* If 'commentPostSuccess' is set to true, set to false after
* few seconds so the message disappears.
*/
useEffect(() => {
if (commentPostSuccess) {
const intervalId = setTimeout(() => {
setCommentPostSuccess(false)
}, 10000)
// Unsubscribe from the interval.
return () => {
clearInterval(intervalId);
};
}
}, [commentPostSuccess])
/**
* Handle form submit.
*
* @param {Event} event Event.
*
* @return {null}
*/
const handleFormSubmit = (event) => {
event.preventDefault();
const commentFormValidationResult = validateAndSanitizeCommentsForm(input);
setInput({
...input,
errors: commentFormValidationResult.errors,
});
// If there are any errors, return.
if (!commentFormValidationResult.isValid) {
return null;
}
// Set loading to true.
setLoading(true);
// Make a POST request to post comment.
const response = postComment(377, input);
/**
* The postComment() returns a promise,
* When the promise gets resolved, i.e. request is complete,
* then handle the success or error messages.
*/
response.then((res) => {
setLoading(false);
if (res.success) {
setCommentPostSuccess(true);
setClearFormValues(true);
} else {
setCommentPostError(res.error ?? 'Something went wrong. Please try again');
}
})
}
/*
* Handle onchange input.
*
* @param {Object} event Event Object.
*
* @return {void}
*/
const handleOnChange = (event) => {
// Reset the comment post success and error messages, first.
if (commentPostSuccess) {
setCommentPostSuccess(false);
}
if (commentPostError) {
setCommentPostError('');
}
const { target } = event || {};
const newState = { ...input, [target.name]: target.value };
setInput(newState);
};
return (
<form action="/" noValidate onSubmit={handleFormSubmit} id="comment-form">
<h2>Leave a comment</h2>
<p className="comment-notes">
<span id="email-notes">Your email address will not be published.</span>
<span className="required-field-message">Required fields are marked <span className="required">*</span></span>
</p>
<TextArea
id="comment"
containerClassNames="comment-form-comment mb-2"
name="comment"
label="Comment"
cols="45"
rows="5"
required
textAreaValue={input?.comment ?? ''}
handleOnChange={handleOnChange}
errors={input?.errors ?? {}}
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-2">
<Input
name="author"
inputValue={input?.author}
required
handleOnChange={handleOnChange}
label="Name"
errors={input?.errors ?? {}}
containerClassNames="comment-form-author"
/>
<Input
name="email"
inputValue={input?.email}
required
handleOnChange={handleOnChange}
label="Email"
errors={input?.errors ?? {}}
containerClassNames="comment-form-email mb-2"
/>
</div>
<Input
name="url"
inputValue={input?.url ?? ''}
handleOnChange={handleOnChange}
label="Website"
errors={input?.errors ?? {}}
containerClassNames="comment-form-url mb-2"
/>
<div className="form-submit py-4">
<input
name="submit"
type="submit"
id="submit"
className={submitBtnClasses}
value="Post Comment"
disabled={loading}
/>
</div>
{
commentPostSuccess && !loading ?
(
<div
className="p-4 mb-4 text-sm text-green-800 rounded-lg bg-green-50 dark:bg-gray-800 dark:text-green-400"
role="alert">
<span className="font-medium">Success!</span> Your comment has been submitted for approval.
It will be posted after admin's approval.
</div>
) : null
}
{
commentPostError && !loading ?
(
<div
className="p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400"
role="alert">
<span className="font-medium">Error! </span>
<div className="inline-block" dangerouslySetInnerHTML={{ __html: sanitize(commentPostError) }} />
</div>
) : null
}
{
loading ? <Loading text="Processing..." /> : null
}
</form>
)
}
export default CommentForm;
pages\[...slug].js
import { isArray, isEmpty } from 'lodash';
import { useRouter } from 'next/router';
import Layout from '@/components/Layout';
import { FALLBACK, handleRedirectsAndReturnData, isCustomPageUri } from '@/utils/slug';
import { getFormattedDate, getPathNameFromUrl, sanitize } from '@/utils/miscellaneous';
import { getPage, getPages} from '@/utils/blog';
import axios from 'axios';
import { HEADER_FOOTER_ENDPOINT } from '@/utils/constants/endpoints';
import Image from '@/components/image';
import PostMeta from '@/components/post-meta';
const Page = ({ headerFooter, pageData }) => {
const router = useRouter();
// If the page is not yet generated, this will be displayed
// initially until getStaticProps() finishes running
if (router.isFallback) {
return <div>Loading...</div>;
}
return (
<Layout headerFooter={headerFooter || {}} seo={pageData?.yoast_head_json ?? {}}>
<div className="w-4/5 m-auto mb-8">
<figure className="mb-4 overflow-hidden">
<Image
sourceUrl={pageData?._embedded['wp:featuredmedia']?.[0]?.source_url ?? '/12.jpg'}
title={pageData?.title?.rendered ?? ''}
width="600"
height="400"
classNames="w-full h-600px"
/>
</figure>
<PostMeta date={getFormattedDate(pageData?.date ?? '')} authorName={pageData?._embedded?.author?.[0]?.name ?? ''} />
<h1 dangerouslySetInnerHTML={{ __html: sanitize(pageData?.title?.rendered ?? '') }} />
<div dangerouslySetInnerHTML={{ __html: sanitize(pageData?.content?.rendered ?? '') }} />
</div>
</Layout>
);
};
export default Page;
export async function getStaticProps({ params }) {
const { data: headerFooterData } = await axios.get(HEADER_FOOTER_ENDPOINT);
const pageData = await getPage(params?.slug.pop() ?? '');
const defaultProps = {
props: {
headerFooter: headerFooterData?.data ?? {},
pageData: pageData?.[0] ?? {}
},
/**
* Revalidate means that if a new request comes to server, then every 1 sec it will check
* if the data is changed, if it is changed then it will update the
* static file inside .next folder with the new data, so that any 'SUBSEQUENT' requests should have updated data.
*/
revalidate: 1,
};
return handleRedirectsAndReturnData(defaultProps, pageData);
}
/**
* Since the page name uses catch-all routes,
* for example [...slug],
* that's why params would contain slug which is an array.
* For example, If we need to have dynamic route '/foo/bar'
* Then we would add paths: [ params: { slug: ['foo', 'bar'] } } ]
* Here slug will be an array is ['foo', 'bar'], then Next.js will statically generate the page at /foo/bar
*
* At build time next js will make an api call get the data and
* generate a page bar.js inside .next/foo directory, so when the page is served on browser
* data is already present, unlike getInitialProps which gets the page at build time but makes an api
* call after page is served on the browser.
*
* @see https://nextjs.org/docs/basic-features/data-fetching#the-paths-key-required
*
* @returns {Promise<{paths: [], fallback: boolean}>}
*/
export async function getStaticPaths() {
const pagesData = await getPages();
const pathsData = [];
isArray(pagesData) && pagesData.map(page => {
/**
* Extract pathname from url.
* e.g.
* getPathNameFromUrl( 'https://example.com/hello/hi/' ) will return '/hello/hi'
* getPathNameFromUrl( 'https://example.com' ) will return '/'
* @type {String}
*/
const pathName = getPathNameFromUrl(page?.link ?? '');
// Build paths data.
if (!isEmpty(pathName) && !isCustomPageUri(pathName)) {
const slugs = pathName?.split('/').filter(pageSlug => pageSlug);
pathsData.push({ params: { slug: slugs } });
}
});
return {
paths: pathsData,
fallback: FALLBACK,
};
}
http://localhost:3000/category/bird
http://localhost:3000/category/uncategorized
pages\category\[slug].js
/**
* External Dependencies.
*/
import { isEmpty } from 'lodash';
import { useRouter } from 'next/router';
import axios from 'axios';
/**
* Internal Dependencies.
*/
import Layout from '@/components/Layout';
import { handleRedirectsAndReturnData, FALLBACK } from '@/utils/slug';
import { HEADER_FOOTER_ENDPOINT } from '@/utils/constants/endpoints';
import { getSlugCategory, getPostsCategory } from '@/utils/category';
import Posts from '@/components/posts';
import { getPosts } from '@/utils/blog';
const Category = ({ headerFooter, postsData }) => {
const router = useRouter();
/**
* If the page is not yet generated, this will be displayed
* initially until getStaticProps() finishes running
*/
if (router.isFallback) {
return <div>Loading...</div>;
}
return (
<Layout headerFooter={headerFooter || {}}>
<h1>Category</h1>
<Posts posts={ postsData ?? [] } />
</Layout>
);
};
export default Category;
export async function getStaticProps({ params }) {
const {slug} = params;
const { data: headerFooterData } = await axios.get(HEADER_FOOTER_ENDPOINT);
const { category: id } = await getSlugCategory(slug);
const postsData = await getPostsCategory(id);
const defaultProps = {
props: {
headerFooter: headerFooterData?.data ?? {},
postsData: postsData.data || [],
},
/**
* Revalidate means that if a new request comes to server, then every 1 sec it will check
* if the data is changed, if it is changed then it will update the
* static file inside .next folder with the new data, so that any 'SUBSEQUENT' requests should have updated data.
*/
revalidate: 1,
};
return handleRedirectsAndReturnData(defaultProps, postsData);
}
export async function getStaticPaths() {
const pathsCategories = [];
const testData = [
{
slug: "uncategorized"
}
];
testData.map(post => {
pathsCategories.push({ params: { slug: post?.slug } });
});
return {
paths: pathsCategories,
fallback: FALLBACK,
};
}
components\Layout.js
import Head from 'next/head';
import Header from "./Header";
import Footer from "./Footer";
import { AppProvider } from './context';
const Layout = (props) => {
return (
<AppProvider>
<div>
<Head>
<title>Woocommerce React Theme</title>
</Head>
<Header />
{props.children}
<Footer />
</div>
</AppProvider>
);
};
export default Layout;
pages\index.js
import Layout from "@/components/Layout";
import fetch from 'isomorphic-unfetch';
import Product from "@/components/Product";
import clientConfig from "@/client-config";
import { useEffect, useState } from "react";
export default function Home(props) {
const [listproducts, setProduct] = useState([]);
useEffect(() => {
fetch(`${clientConfig.siteUrl}/api/getproducts`)
.then((r) => r.json())
.then((data) => {
setProduct(data);
});
},[]);
return (
<Layout>
<div className="container m-auto grid grid-cols-4 gap-2 mt-6">
{listproducts.length ? (listproducts.map(product => <Product key={product.id} product={product} />)) : ''}
</div>
</Layout>
);
}
components\Product.js
import AddToCart from "./cart/add-to-cart";
const Product = ( props ) => {
const { product } = props;
return (
<div className="shadow border text-center">
<h3 className="card-header">{product.name}</h3>
<img src={product.images[0]?.src ?? '/23.jpg'} className="h-[465px] w-full object-cover" alt="Product image"/>
<div className="card-body p-1">
<h6 className="card-subtitle mb-3 text-black">Price{ product.price }</h6>
</div>
<AddToCart product={product} />
</div>
);
}
export default Product;
components\cart\add-to-cart.js
import { useContext, useState } from 'react';
import Link from 'next/link';
import { isEmpty } from 'lodash';
import { addToCart } from '../../utils/cart';
import { AppContext } from '../context';
import cx from 'classnames';
const AddToCart = ({ product }) => {
const [cart, setCart] = useContext(AppContext);
const [isAddedToCart, setIsAddedToCart] = useState(false);
const [loading, setLoading] = useState(false);
const addToCartBtnClasses = cx(
'duration-500 text-gray-800 font-semibold py-2 px-4 border border-gray-400 rounded shadow',
{
'bg-white hover:bg-gray-100': !loading,
'bg-gray-200': loading,
},
);
if (isEmpty(product)) {
return null;
}
return (
<>
<button
className={addToCartBtnClasses}
onClick={() => addToCart(product?.id ?? 0, 1, setCart, setIsAddedToCart, setLoading)}
disabled={loading}
>
{loading ? 'Adding...' : 'Add to cart'}
</button>
{isAddedToCart && !loading ? (
<Link href="/cart" className="bg-white hover:bg-gray-100 text-gray-800 font-semibold ml-4 py-11px px-4 border border-gray-400 rounded shadow">
View cart
</Link>
) : null}
</>
);
};
export default AddToCart;
utils\cart\index.js
import { getSession, storeSession } from './session';
import { getApiCartConfig } from './api';
import axios from 'axios';
import { CART_ENDPOINT } from '../constants/endpoints';
import { isEmpty, isArray } from 'lodash';
/**
* Add To Cart Request Handler.
*
* @param {int} productId Product Id.
* @param {int} qty Product Quantity.
* @param {Function} setCart Sets The New Cart Value
* @param {Function} setIsAddedToCart Sets A Boolean Value If Product Is Added To Cart.
* @param {Function} setLoading Sets A Boolean Value For Loading State.
*/
export const addToCart = (productId, qty = 1, setCart, setIsAddedToCart, setLoading) => {
const storedSession = getSession();
const addOrViewCartConfig = getApiCartConfig();
setLoading(true);
axios.post(CART_ENDPOINT, { product_id: productId, quantity: qty, }, addOrViewCartConfig)
.then((res) => {
if (isEmpty(storedSession)) {
storeSession(res?.headers?.['x-wc-session']);
}
setIsAddedToCart(true);
setLoading(false);
viewCart(setCart);
})
.catch(err => {
console.log('err', err);
});
};
/**
* View Cart Request Handler
*
* @param {Function} setCart Set Cart Function.
* @param {Function} setProcessing Set Processing Function.
*/
export const viewCart = (setCart, setProcessing = () => { }) => {
const addOrViewCartConfig = getApiCartConfig();
axios.get(CART_ENDPOINT, addOrViewCartConfig)
.then((res) => {
const formattedCartData = getFormattedCartData(res?.data ?? [])
setCart(formattedCartData);
setProcessing(false);
})
.catch(err => {
console.log('err', err);
setProcessing(false);
});
};
/**
* Update Cart Request Handler
*/
export const updateCart = (cartKey, qty = 1, setCart, setUpdatingProduct) => {
const addOrViewCartConfig = getApiCartConfig();
setUpdatingProduct(true);
axios.put(`${CART_ENDPOINT}${cartKey}`, {
quantity: qty,
}, addOrViewCartConfig)
.then((res) => {
viewCart(setCart, setUpdatingProduct);
})
.catch(err => {
console.log('err', err);
setUpdatingProduct(false);
});
};
/**
* Delete a cart item Request handler.
*
* Deletes all products in the cart of a
* specific product id ( by its cart key )
* In a cart session, each product maintains
* its data( qty etc ) with a specific cart key
*
* @param {String} cartKey Cart Key.
* @param {Function} setCart SetCart Function.
* @param {Function} setRemovingProduct Set Removing Product Function.
*/
export const deleteCartItem = (cartKey, setCart, setRemovingProduct) => {
const addOrViewCartConfig = getApiCartConfig();
setRemovingProduct(true);
axios.delete(`${CART_ENDPOINT}${cartKey}`, addOrViewCartConfig)
.then((res) => {
viewCart(setCart, setRemovingProduct);
})
.catch(err => {
console.log('err', err);
setRemovingProduct(false);
});
};
/**
* Clear Cart Request Handler
*
* @param {Function} setCart Set Cart
* @param {Function} setClearCartProcessing Set Clear Cart Processing.
*/
export const clearCart = async (setCart, setClearCartProcessing) => {
setClearCartProcessing(true);
const addOrViewCartConfig = getApiCartConfig();
try {
const response = await axios.delete(CART_ENDPOINT, addOrViewCartConfig);
viewCart(setCart, setClearCartProcessing);
} catch (err) {
console.log('err', err);
setClearCartProcessing(false);
}
};
/**
* Get Formatted Cart Data.
*
* @param cartData
* @return {null|{cartTotal: {totalQty: number, totalPrice: number}, cartItems: ({length}|*|*[])}}
*/
const getFormattedCartData = (cartData) => {
if (!cartData.length) {
return null;
}
const cartTotal = calculateCartQtyAndPrice(cartData || []);
return {
cartItems: cartData || [],
...cartTotal,
};
};
/**
* Calculate Cart Qty And Price.
*
* @param cartItems
* @return {{totalQty: number, totalPrice: number}}
*/
const calculateCartQtyAndPrice = (cartItems) => {
const qtyAndPrice = {
totalQty: 0,
totalPrice: 0,
}
if (!isArray(cartItems) || !cartItems?.length) {
return qtyAndPrice;
}
cartItems.forEach((item, index) => {
qtyAndPrice.totalQty += item?.quantity ?? 0;
qtyAndPrice.totalPrice += item?.line_total ?? 0;
})
return qtyAndPrice;
}
utils\cart\api.js
import { getSession } from './session';
import { isEmpty } from 'lodash';
export const getApiCartConfig = () => {
const config = {
headers: {
'X-Headless-CMS': true,
},
}
const storedSession = getSession();
if ( !isEmpty( storedSession ) ) {
config.headers['x-wc-session'] = storedSession;
}
return config;
}
utils\cart\session.js
import { isEmpty } from 'lodash';
export const storeSession = ( session ) => {
if ( isEmpty( session ) ) {
return null;
}
localStorage.setItem( 'x-wc-session', session );
}
export const getSession = () => {
return localStorage.getItem( 'x-wc-session' );
}
components\cart\cart-items-container.js
import React, { useContext, useState } from 'react';
import { AppContext } from '../context';
import CartItem from './cart-item';
import Link from 'next/link';
import { clearCart } from '../../utils/cart';
const CartItemsContainer = () => {
const [cart, setCart] = useContext(AppContext);
const { cartItems, totalPrice, totalQty } = cart || {};
const [isClearCartProcessing, setClearCartProcessing] = useState(false);
// Clear the entire cart.
const handleClearCart = async (event) => {
event.stopPropagation();
if (isClearCartProcessing) {
return;
}
await clearCart(setCart, setClearCartProcessing);
};
return (
<div className="content-wrap-cart">
{cart ? (
<div className="woo-next-cart-table-row grid lg:grid-cols-3 gap-4">
{/*Cart Items*/}
<div className="woo-next-cart-table lg:col-span-2 mb-md-0 mb-5">
{cartItems?.length &&
cartItems.map((item) => (
<CartItem
key={item.product_id}
item={item}
products={cartItems}
setCart={setCart}
/>
))}
</div>
{/*Cart Total*/}
<div className="woo-next-cart-total-container lg:col-span-1 p-5 pt-0">
<h2>Cart Total</h2>
<div className="grid grid-cols-3 bg-gray-100 mb-4">
<p className="col-span-2 p-2 mb-0">Total({totalQty})</p>
<p className="col-span-1 p-2 mb-0">{cartItems?.[0]?.currency ?? ''}{totalPrice}</p>
</div>
<div className="flex justify-between">
{/*Clear entire cart*/}
<div className="clear-cart">
<button
className="text-gray-900 bg-white border border-gray-300 hover:bg-gray-100 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center mr-2 mb-2 dark:bg-gray-600 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-700 dark:focus:ring-gray-800"
onClick={(event) => handleClearCart(event)}
disabled={isClearCartProcessing}
>
<span className="woo-next-cart">{!isClearCartProcessing ? "Clear Cart" : "Clearing..."}</span>
</button>
</div>
{/*Checkout*/}
<Link href="/checkout">
<button className="border bg-brand-orange hover:bg-brand-royal-blue focus:ring-4 font-medium rounded-lg text-sm px-5 py-2.5 text-center mr-2">
<span className="woo-next-cart-checkout-txt">
Proceed to Checkout
</span>
<i className="fas fa-long-arrow-alt-right" />
</button>
</Link>
</div>
</div>
</div>
) : (
<div className="mt-14">
<h2>No items in the cart</h2>
<Link href="/">
<button className="text-white duration-500 bg-brand-orange hover:bg-brand-royal-blue font-medium rounded-lg text-sm px-5 py-2.5 text-center mr-2 mb-2 dark:focus:ring-yellow-900">
<span className="woo-next-cart-checkout-txt">
Add New Products
</span>
<i className="fas fa-long-arrow-alt-right" />
</button>
</Link>
</div>
)}
</div>
);
};
export default CartItemsContainer;
components\cart\cart-item.js
import React, { useEffect, useState, useRef } from 'react';
import {isEmpty} from "lodash";
import Image from '../image';
import { deleteCartItem, updateCart } from '../../utils/cart';
const CartItem = ( {
item,
products,
setCart
} ) => {
const [productCount, setProductCount] = useState( item.quantity );
const [updatingProduct, setUpdatingProduct] = useState( false );
const [removingProduct, setRemovingProduct] = useState( false );
const productImg = item?.data?.images?.[0] ?? '';
/**
* Do not allow state update on an unmounted component.
*
* isMounted is used so that we can set it's value to false
* when the component is unmounted.
* This is done so that setState ( e.g setRemovingProduct ) in asynchronous calls
* such as axios.post, do not get executed when component leaves the DOM
* due to product/item deletion.
* If we do not do this as unsubscription, we will get
* "React memory leak warning- Can't perform a React state update on an unmounted component"
*
* @see https://dev.to/jexperton/how-to-fix-the-react-memory-leak-warning-d4i
* @type {React.MutableRefObject<boolean>}
*/
const isMounted = useRef( false );
useEffect( () => {
isMounted.current = true
// When component is unmounted, set isMounted.current to false.
return () => {
isMounted.current = false
}
}, [] )
/*
* Handle remove product click.
*
* @param {Object} event event
* @param {Integer} Product Id.
*
* @return {void}
*/
const handleRemoveProductClick = ( event, cartKey ) => {
event.stopPropagation();
// If the component is unmounted, or still previous item update request is in process, then return.
if ( !isMounted || updatingProduct ) {
return;
}
deleteCartItem( cartKey, setCart, setRemovingProduct );
};
/*
* When user changes the qty from product input update the cart in localStorage
* Also update the cart in global context
*
* @param {Object} event event
*
* @return {void}
*/
const handleQtyChange = ( event, cartKey, type ) => {
if ( typeof window !== 'undefined' ) {
event.stopPropagation();
let newQty;
// If the previous cart request is still updatingProduct or removingProduct, then return.
if ( updatingProduct || removingProduct || ( 'decrement' === type && 1 === productCount ) ) {
return;
}
if ( !isEmpty( type ) ) {
newQty = 'increment' === type ? productCount + 1 : productCount - 1;
} else {
// If the user tries to delete the count of product, set that to 1 by default ( This will not allow him to reduce it less than zero )
newQty = ( event.target.value ) ? parseInt( event.target.value ) : 1;
}
// Set the new qty in state.
setProductCount( newQty );
if ( products.length ) {
updateCart(item?.key, newQty, setCart, setUpdatingProduct);
}
}
};
return (
<div className="cart-item-wrap grid grid-cols-3 gap-6 mb-5 border border-brand-bright-grey p-5">
<div className="col-span-1 cart-left-col">
<figure >
<Image
width="300"
height="300"
altText={productImg?.alt ?? 'altText'}
sourceUrl={! isEmpty( productImg?.src ) ? productImg?.src : ''} // use normal <img> attributes as props
/>
</figure>
</div>
<div className="col-span-2 cart-right-col">
<div className="flex justify-between flex-col h-full">
<div className="cart-product-title-wrap relative">
<h3 className="cart-product-title text-brand-orange">{ item?.data?.name }</h3>
{item?.data?.description ? <p>{item?.data?.description}</p> : ''}
<button className="cart-remove-item absolute right-0 top-0 px-4 py-2 flex items-center text-22px leading-22px bg-transparent border border-brand-bright-grey" onClick={ ( event ) => handleRemoveProductClick( event, item?.key ) }>×</button>
</div>
<footer className="cart-product-footer flex justify-between p-4 border-t border-brand-bright-grey">
<div className="">
<span className="cart-total-price">{item?.currency}{item?.line_subtotal}</span>
</div>
{ updatingProduct ? <img className="woo-next-cart-item-spinner" width="24" src="/cart-spinner.gif" alt="spinner"/> : null }
{/*Qty*/}
<div style={{ display: 'flex', alignItems: 'center' }}>
<button className="decrement-btn text-24px" onClick={( event ) => handleQtyChange( event, item?.cartKey, 'decrement' )} >-</button>
<input
type="number"
min="1"
style={{ textAlign: 'center', width: '50px', paddingRight: '0' }}
data-cart-key={ item?.data?.cartKey }
className={ `woo-next-cart-qty-input ml-3 ${ updatingProduct ? 'disabled' : '' } ` }
value={ productCount }
onChange={ ( event ) => handleQtyChange( event, item?.cartKey, '' ) }
/>
<button className="increment-btn text-20px" onClick={( event ) => handleQtyChange( event, item?.cartKey, 'increment' )}>+</button>
</div>
</footer>
</div>
</div>
</div>
)
};
export default CartItem;
pages\checkout.js
import Layout from '@/components/Layout';
import {HEADER_FOOTER_ENDPOINT,WOOCOMMERCE_COUNTRIES_ENDPOINT} from '../utils/constants/endpoints';
import axios from 'axios';
import CheckoutForm from '@/components/checkout/checkout-form';
export default function Checkout({ headerFooter, countries }) {
return (
<Layout headerFooter={headerFooter || {}}>
<h1>Checkout</h1>
<CheckoutForm countriesData={countries}/>
</Layout>
);
}
export async function getStaticProps() {
const { data: headerFooterData } = await axios.get( HEADER_FOOTER_ENDPOINT );
const { data: countries } = await axios.get( WOOCOMMERCE_COUNTRIES_ENDPOINT );
return {
props: {
headerFooter: headerFooterData?.data ?? {},
countries: countries || {}
},
/**
* Revalidate means that if a new request comes to server, then every 1 sec it will check
* if the data is changed, if it is changed then it will update the
* static file inside .next folder with the new data, so that any 'SUBSEQUENT' requests should have updated data.
*/
revalidate: 1,
};
}
components\checkout\your-order.js
import { Fragment } from 'react';
import CheckoutCartItem from "./checkout-cart-item";
const YourOrder = ({ cart }) => {
return (
<Fragment>
{cart ? (
<Fragment>
{/*Product Listing*/}
<table className="checkout-cart table table-hover w-full mb-10">
<thead>
<tr className="woo-next-cart-head-container text-left">
<th className="woo-next-cart-heading-el" scope="col" />
<th className="woo-next-cart-heading-el" scope="col">Product</th>
<th className="woo-next-cart-heading-el" scope="col">Total</th>
</tr>
</thead>
<tbody>
{cart?.cartItems?.length && (
cart.cartItems.map((item, index) => (
<CheckoutCartItem key={item?.productId ?? index} item={item} />
))
)}
{/*Total*/}
<tr className="bg-gray-200">
<td className="" />
<td className="woo-next-checkout-total font-normal text-xl">Total</td>
<td className="woo-next-checkout-total font-bold text-xl">{cart?.cartItems?.[0]?.currency ?? ''}{cart?.totalPrice ?? ''}</td>
</tr>
</tbody>
</table>
</Fragment>
) : ''}
</Fragment>
)
};
export default YourOrder;
components\checkout\user-address.js
import PropTypes from 'prop-types';
import CountrySelection from "./country-selection";
import StateSelection from "./states-selection";
import InputField from "./form-elements/input-field";
const Address = ({ input, countries, states, handleOnChange, isFetchingStates, isShipping }) => {
const { errors } = input || {};
return (
<>
<div className="flex flex-wrap overflow-hidden sm:-mx-3">
<InputField
name="firstName"
inputValue={input?.firstName}
required
handleOnChange={handleOnChange}
label="First name"
errors={errors}
isShipping={isShipping}
containerClassNames="w-full overflow-hidden sm:my-2 sm:px-2 md:w-1/2"
/>
<InputField
name="lastName"
inputValue={input?.lastName}
required
handleOnChange={handleOnChange}
label="Last name"
errors={errors}
isShipping={isShipping}
containerClassNames="w-full overflow-hidden sm:my-2 sm:px-2 md:w-1/2"
/>
</div>
<InputField
name="company"
inputValue={input?.company}
handleOnChange={handleOnChange}
label="Company Name (Optional)"
errors={errors}
isShipping={isShipping}
containerClassNames="mb-4"
/>
{/* Country Selection*/}
<CountrySelection
input={input}
handleOnChange={handleOnChange}
countries={countries}
isShipping={isShipping}
/>
<InputField
name="address1"
inputValue={input?.address1}
required
handleOnChange={handleOnChange}
label="Street address"
placeholder="House number and street name"
errors={errors}
isShipping={isShipping}
containerClassNames="mb-4"
/>
<InputField
name="address2"
inputValue={input?.address2}
handleOnChange={handleOnChange}
label="Street address line two"
placeholder="Apartment floor unit building floor etc(optional)"
errors={errors}
isShipping={isShipping}
containerClassNames="mb-4"
/>
<InputField
name="city"
required
inputValue={input?.city}
handleOnChange={handleOnChange}
label="Town/City"
errors={errors}
isShipping={isShipping}
containerClassNames="mb-4"
/>
{/* State */}
<StateSelection
input={input}
handleOnChange={handleOnChange}
states={states}
isShipping={isShipping}
isFetchingStates={isFetchingStates}
/>
<div className="flex flex-wrap overflow-hidden sm:-mx-3">
<InputField
name="postcode"
inputValue={input?.postcode}
required
handleOnChange={handleOnChange}
label="Post code"
errors={errors}
isShipping={isShipping}
containerClassNames="w-full overflow-hidden sm:my-2 sm:px-2 md:w-1/2"
/>
<InputField
name="phone"
inputValue={input?.phone}
required
handleOnChange={handleOnChange}
label="Phone"
errors={errors}
isShipping={isShipping}
containerClassNames="w-full overflow-hidden sm:my-2 sm:px-2 md:w-1/2"
/>
</div>
<InputField
name="email"
type="email"
inputValue={input?.email}
required
handleOnChange={handleOnChange}
label="Email"
errors={errors}
isShipping={isShipping}
containerClassNames="mb-4"
/>
{/* @TODO Create an Account */}
{/*<div className="form-check">*/}
{/* <label className="leading-7 text-sm text-gray-600" className="form-check-label">*/}
{/* <input onChange={ handleOnChange } className="form-check-input" name="createAccount" type="checkbox"/>*/}
{/* Create an account?*/}
{/* </label>*/}
{/*</div>*/}
{/*<h2 className="mt-4 mb-4">Additional Information</h2>*/}
{/* @TODO Order Notes */}
{/*<div className="form-group mb-3">*/}
{/* <label className="leading-7 text-sm text-gray-600" htmlFor="order-notes">Order Notes</label>*/}
{/* <textarea onChange={ handleOnChange } defaultValue={ input.orderNotes } name="orderNotes" className="form-control woo-next-checkout-textarea" id="order-notes" rows="4"/>*/}
{/* <Error errors={ input.errors } fieldName={ 'orderNotes' }/>*/}
{/*</div>*/}
</>
);
};
Address.propTypes = {
input: PropTypes.object,
countries: PropTypes.array,
handleOnChange: PropTypes.func,
isFetchingStates: PropTypes.bool,
isShipping: PropTypes.bool
}
Address.defaultProps = {
input: {},
countries: [],
handleOnChange: () => null,
isFetchingStates: false,
isShipping: false
}
export default Address;
components\checkout\states-selection.js
import PropTypes from 'prop-types';
import { memo } from 'react';
import cx from 'classnames';
import Abbr from "./form-elements/abbr";
import Error from './error';
const StateSelection = ({ handleOnChange, input, states, isFetchingStates, isShipping }) => {
const { state, errors } = input || {};
const inputId = `state-${isShipping ? 'shipping' : 'billing'}`;
if (isFetchingStates) {
// Show loading component.
return (
<div className="mb-3">
<label className="leading-7 text-sm text-gray-700">
State/County
<Abbr required />
</label>
<div className="relative w-full border-none">
<select
disabled
value=""
name="state"
className="opacity-50 bg-gray-100 bg-opacity-50 border border-gray-500 text-gray-500 appearance-none inline-block py-3 pl-3 pr-8 rounded leading-tight w-full"
>
<option value="">Loading...</option>
</select>
</div>
</div>
)
}
if (!states.length) {
return null;
}
return (
<div className="mb-3">
<label className="leading-7 text-sm text-gray-600" htmlFor={inputId}>
State/County
<Abbr required />
</label>
<div className="relative w-full border-none">
<select
disabled={isFetchingStates}
onChange={handleOnChange}
value={state}
name="state"
className={cx(
'bg-gray-100 bg-opacity-50 border border-gray-400 text-gray-500 appearance-none inline-block py-3 pl-3 pr-8 rounded leading-tight w-full',
{ 'opacity-50': isFetchingStates }
)}
id={inputId}
>
<option value="">Select a state...</option>
{states.map((state, index) => (
<option key={state?.stateCode ?? index} value={state?.stateName ?? ''}>
{state?.stateName}
</option>
))}
</select>
</div>
<Error errors={errors} fieldName={'state'} />
</div>
)
}
StateSelection.propTypes = {
handleOnChange: PropTypes.func,
input: PropTypes.object,
states: PropTypes.array,
isFetchingStates: PropTypes.bool,
isShipping: PropTypes.bool
}
StateSelection.defaultProps = {
handleOnChange: () => null,
input: {},
states: [],
isFetchingStates: false,
isShipping: true
}
export default memo(StateSelection);
components\checkout\payment-modes.js
import Error from "./error";
const PaymentModes = ( { input, handleOnChange } ) => {
const { errors, paymentMethod } = input || {}
return (
<div className="mt-3">
<Error errors={ errors } fieldName={ 'paymentMethod' }/>
{/*Direct bank transfers*/}
<div className="form-check woo-next-payment-input-container mt-2">
<label className="form-check-label">
<input onChange={ handleOnChange } value="bacs" className="form-check-input mr-3" name="paymentMethod" type="radio" checked={'bacs' === paymentMethod}/>
<span className="woo-next-payment-content">Direct Bank Transfer</span>
</label>
</div>
{/*Pay with Paypal*/}
<div className="form-check woo-next-payment-input-container mt-2">
<label className="form-check-label">
<input onChange={ handleOnChange } value="paypal" className="form-check-input mr-3" name="paymentMethod" type="radio" checked={'paypal' === paymentMethod}/>
<span className="woo-next-payment-content">Pay with Paypal</span>
</label>
</div>
{/*Check Payments*/}
<div className="form-check woo-next-payment-input-container mt-2">
<label className="form-check-label">
<input onChange={ handleOnChange } value="cheque" className="form-check-input mr-3" name="paymentMethod" type="radio" checked={'cheque' === paymentMethod}/>
<span className="woo-next-payment-content">Check Payments</span>
</label>
</div>
{/*Pay with Stripe*/}
<div className="form-check woo-next-payment-input-container mt-2">
<label className="form-check-label">
<input onChange={ handleOnChange } value="cod" className="form-check-input mr-3" name="paymentMethod" type="radio" checked={'cod' === paymentMethod}/>
<span className="woo-next-payment-content">Cash on Delivery</span>
</label>
</div>
<div className="form-check woo-next-payment-input-container mt-2">
<label className="form-check-label">
<input onChange={ handleOnChange } value="jccpaymentgatewayredirect" className="form-check-input mr-3" name="paymentMethod" type="radio" checked={'jccpaymentgatewayredirect' === paymentMethod}/>
<span className="woo-next-payment-content">JCC</span>
</label>
</div>
<div className="form-check woo-next-payment-input-container mt-2">
<label className="form-check-label">
<input onChange={ handleOnChange } value="ccavenue" className="form-check-input mr-3" name="paymentMethod" type="radio" checked={'ccavenue' === paymentMethod}/>
<span className="woo-next-payment-content">CC Avenue</span>
</label>
</div>
<div className="form-check woo-next-payment-input-container mt-2">
<label className="form-check-label">
<input onChange={ handleOnChange } value="stripe" className="form-check-input mr-3" name="paymentMethod" type="radio" checked={'stripe' === paymentMethod}/>
<span className="woo-next-payment-content">Stripe</span>
</label>
</div>
{/* Payment Instructions*/}
<div className="woo-next-checkout-payment-instructions mt-2">
Please send a check to Store Name, Store Street, Store Town, Store State / County, Store Postcode.
</div>
</div>
);
};
export default PaymentModes;
components\checkout\error.js
const Error = ( { errors, fieldName } ) => {
return(
errors && ( errors.hasOwnProperty( fieldName ) ) ? (
<div className="invalid-feedback d-block text-red-500">{ errors[fieldName] }</div>
) : ''
)
};
export default Error;
components\checkout\country-selection.js
import Error from './error';
import { isEmpty, map } from "lodash";
import Abbr from "./form-elements/abbr";
import ArrowDown from "../icons/ArrowDown";
const CountrySelection = ({ input, handleOnChange, countries, isShipping }) => {
const { country, errors } = input || {};
const inputId = `country-${isShipping ? 'shipping' : 'billing'}`;
return (
<div className="mb-3">
<label className="leading-7 text-sm text-gray-700" htmlFor={inputId}>
Country
<Abbr required />
</label>
<div className="relative w-full border-none">
<select
onChange={handleOnChange}
value={country}
name="country"
className="bg-gray-100 bg-opacity-50 border border-gray-500 text-gray-500 appearance-none inline-block py-3 pl-3 pr-8 rounded leading-tight w-full"
id={inputId}
>
<option value="">Select a country...</option>
{!isEmpty(countries) &&
map(countries, (country) => (
<option key={country?.countryCode} data-countrycode={country?.countryCode}
value={country?.countryCode}>
{country?.countryName}
</option>
))}
</select>
<span className="absolute right-0 mr-1 text-gray-500" style={{ top: '25%' }}>
<ArrowDown width={24} height={24} className="fill-current" />
</span>
</div>
<Error errors={errors} fieldName={'country'} />
</div>
);
}
export default CountrySelection;
components\checkout\checkout-form.js
import { useState, useContext } from 'react';
import cx from 'classnames';
import YourOrder from './your-order';
import PaymentModes from './payment-modes';
import validateAndSanitizeCheckoutForm from '../../validator/checkout';
import Address from './user-address';
import { AppContext } from '../context';
import CheckboxField from './form-elements/checkbox-field';
import {
handleBillingDifferentThanShipping,
handleCreateAccount, handleOtherPaymentMethodCheckout, handleStripeCheckout,
setStatesForCountry,
} from '../../utils/checkout';
// Use this for testing purposes, so you dont have to fill the checkout form over an over again.
// const defaultCustomerInfo = {
// firstName: 'Imran',
// lastName: 'Sayed',
// address1: '123 Abc farm',
// address2: 'Hill Road',
// city: 'Mumbai',
// country: 'IN',
// state: 'Maharastra',
// postcode: '221029',
// email: 'codeytek.academy@gmail.com',
// phone: '9883778278',
// company: 'The Company',
// errors: null,
// };
const defaultCustomerInfo = {
firstName: '',
lastName: '',
address1: '',
address2: '',
city: '',
country: '',
state: '',
postcode: '',
email: '',
phone: '',
company: '',
errors: null
}
const CheckoutForm = ({ countriesData }) => {
const { billingCountries, shippingCountries } = countriesData || {};
const initialState = {
billing: {
...defaultCustomerInfo,
},
shipping: {
...defaultCustomerInfo,
},
createAccount: false,
orderNotes: '',
billingDifferentThanShipping: false,
paymentMethod: 'cod',
};
const [cart, setCart] = useContext(AppContext);
const [input, setInput] = useState(initialState);
const [requestError, setRequestError] = useState(null);
const [theShippingStates, setTheShippingStates] = useState([]);
const [isFetchingShippingStates, setIsFetchingShippingStates] = useState(false);
const [theBillingStates, setTheBillingStates] = useState([]);
const [isFetchingBillingStates, setIsFetchingBillingStates] = useState(false);
const [isOrderProcessing, setIsOrderProcessing] = useState(false);
const [createdOrderData, setCreatedOrderData] = useState({});
/**
* Handle form submit.
*
* @param {Object} event Event Object.
*
* @return Null.
*/
const handleFormSubmit = async (event) => {
event.preventDefault();
/**
* Validate Billing and Shipping Details
*
* Note:
* 1. If billing is different than shipping address, only then validate billing.
* 2. We are passing theBillingStates?.length and theShippingStates?.length, so that
* the respective states should only be mandatory, if a country has states.
*/
const billingValidationResult = input?.billingDifferentThanShipping ? validateAndSanitizeCheckoutForm(input?.billing, theBillingStates?.length) : {
errors: null,
isValid: true,
};
const shippingValidationResult = validateAndSanitizeCheckoutForm(input?.shipping, theShippingStates?.length);
setInput({
...input,
billing: { ...input.billing, errors: billingValidationResult.errors },
shipping: { ...input.shipping, errors: shippingValidationResult.errors },
});
// If there are any errors, return.
if (!shippingValidationResult.isValid || !billingValidationResult.isValid) {
return null;
}
// For stripe payment mode, handle the strip payment and thank you.
if ('stripe' === input.paymentMethod) {
const createdOrderData = await handleStripeCheckout(input, cart?.cartItems, setRequestError, setCart, setIsOrderProcessing, setCreatedOrderData);
return null;
}
// For Any other payment mode, create the order and redirect the user to payment url.
const createdOrderData = await handleOtherPaymentMethodCheckout(input, cart?.cartItems, setRequestError, setCart, setIsOrderProcessing, setCreatedOrderData);
if (createdOrderData.paymentUrl) {
window.location.href = createdOrderData.paymentUrl;
}
setRequestError(null);
};
/*
* Handle onchange input.
*
* @param {Object} event Event Object.
* @param {bool} isShipping If this is false it means it is billing.
* @param {bool} isBillingOrShipping If this is false means its standard input and not billing or shipping.
*
* @return {void}
*/
const handleOnChange = async (event, isShipping = false, isBillingOrShipping = false) => {
const { target } = event || {};
if ('createAccount' === target.name) {
handleCreateAccount(input, setInput, target);
} else if ('billingDifferentThanShipping' === target.name) {
handleBillingDifferentThanShipping(input, setInput, target);
} else if (isBillingOrShipping) {
if (isShipping) {
await handleShippingChange(target);
} else {
await handleBillingChange(target);
}
} else {
const newState = { ...input, [target.name]: target.value };
setInput(newState);
}
};
const handleShippingChange = async (target) => {
const newState = { ...input, shipping: { ...input?.shipping, [target.name]: target.value } };
setInput(newState);
await setStatesForCountry(target, setTheShippingStates, setIsFetchingShippingStates);
};
const handleBillingChange = async (target) => {
const newState = { ...input, billing: { ...input?.billing, [target.name]: target.value } };
setInput(newState);
await setStatesForCountry(target, setTheBillingStates, setIsFetchingBillingStates);
};
return (
<>
{cart ? (
<form onSubmit={handleFormSubmit} className="woo-next-checkout-form">
<div className="grid grid-cols-1 md:grid-cols-2 gap-20">
<div>
{/*Shipping Details*/}
<div className="billing-details">
<h2 className="text-xl font-medium mb-4">Shipping Details</h2>
<Address
states={theShippingStates}
countries={shippingCountries}
input={input?.shipping}
handleOnChange={(event) => handleOnChange(event, true, true)}
isFetchingStates={isFetchingShippingStates}
isShipping
isBillingOrShipping
/>
</div>
<div>
<CheckboxField
name="billingDifferentThanShipping"
type="checkbox"
checked={input?.billingDifferentThanShipping}
handleOnChange={handleOnChange}
label="Billing different than shipping"
containerClassNames="mb-4 pt-4"
/>
</div>
{/*Billing Details*/}
{input?.billingDifferentThanShipping ? (
<div className="billing-details">
<h2 className="text-xl font-medium mb-4">Billing Details</h2>
<Address
states={theBillingStates}
countries={billingCountries.length ? billingCountries : shippingCountries}
input={input?.billing}
handleOnChange={(event) => handleOnChange(event, false, true)}
isFetchingStates={isFetchingBillingStates}
isShipping={false}
isBillingOrShipping
/>
</div>
) : null}
</div>
{/* Order & Payments*/}
<div className="your-orders">
{/* Order*/}
<h2 className="text-xl font-medium mb-4">Your Order</h2>
<YourOrder cart={cart} />
{/*Payment*/}
<PaymentModes input={input} handleOnChange={handleOnChange} />
<div className="woo-next-place-order-btn-wrap mt-5">
<button
disabled={isOrderProcessing}
className={cx(
'bg-purple-600 text-white px-5 py-3 rounded-sm w-auto xl:w-full',
{ 'opacity-50': isOrderProcessing },
)}
type="submit"
>
Place Order
</button>
</div>
{/* Checkout Loading*/}
{isOrderProcessing && <p>Processing Order...</p>}
{requestError && <p>Error : {requestError} :( Please try again</p>}
</div>
</div>
</form>
) : null}
</>
);
};
export default CheckoutForm;
components\checkout\checkout-cart-item.js
import Image from '../image';
import { isEmpty } from 'lodash';
const CheckoutCartItem = ( { item } ) => {
const productImg = item?.data?.images?.[0] ?? '';
return (
<tr className="woo-next-cart-item" key={ item?.productId ?? '' }>
<td className="woo-next-cart-element">
<figure >
<Image
width="50"
height="50"
altText={productImg?.alt ?? ''}
sourceUrl={! isEmpty( productImg?.src ) ? productImg?.src : ''} // use normal <img> attributes as props
/>
</figure>
</td>
<td className="woo-next-cart-element">{ item?.data?.name ?? '' }</td>
<td className="woo-next-cart-element">{item?.currency ?? ''}{item?.line_subtotal ?? ''}</td>
</tr>
)
};
export default CheckoutCartItem;
components\checkout\form-elements\abbr.js
import PropTypes from 'prop-types';
const Abbr = ({required}) => {
if ( !required ) {
return null;
}
return <abbr className="text-red-500" style={{textDecoration: 'none'}} title="required">*</abbr>
}
Abbr.propTypes = {
required: PropTypes.bool
}
Abbr.defaultProps = {
required: false
}
export default Abbr
components\checkout\form-elements\checkbox-field.js
import PropTypes from 'prop-types';
const CheckboxField = ({ handleOnChange, checked, name, label, placeholder, containerClassNames }) => {
return (
<div className={containerClassNames}>
<label className="leading-7 text-md text-gray-700 flex items-center cursor-pointer" htmlFor={name}>
<input
onChange={ handleOnChange }
placeholder={placeholder}
type="checkbox"
checked={checked}
name={name}
id={name}
/>
<span className="ml-2">{ label || '' }</span>
</label>
</div>
)
}
CheckboxField.propTypes = {
handleOnChange: PropTypes.func,
checked: PropTypes.bool,
name: PropTypes.string,
type: PropTypes.string,
label: PropTypes.string,
placeholder: PropTypes.string,
containerClassNames: PropTypes.string
}
CheckboxField.defaultProps = {
handleOnChange: () => null,
checked: false,
name: '',
label: '',
placeholder: '',
errors: {},
containerClassNames: ''
}
export default CheckboxField;
components\checkout\form-elements\input-field.js
import Error from "../error";
import PropTypes from 'prop-types';
import Abbr from "./abbr";
const InputField = ({ handleOnChange, inputValue, name, type, label, errors, placeholder, required, containerClassNames, isShipping }) => {
const inputId = `${name}-${isShipping ? 'shipping' : ''}`;
return (
<div className={containerClassNames}>
<label className="leading-7 text-sm text-gray-700" htmlFor={inputId}>
{ label || '' }
<Abbr required={required}/>
</label>
<input
onChange={ handleOnChange }
value={ inputValue }
placeholder={placeholder}
type={type}
name={name}
className="w-full bg-gray-100 bg-opacity-50 rounded border border-gray-500 focus:border-indigo-500 focus:bg-transparent focus:ring-2 focus:ring-indigo-200 text-base outline-none text-gray-800 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
id={inputId}
/>
<Error errors={ errors } fieldName={ name }/>
</div>
)
}
InputField.propTypes = {
handleOnChange: PropTypes.func,
inputValue: PropTypes.string,
name: PropTypes.string,
type: PropTypes.string,
label: PropTypes.string,
placeholder: PropTypes.string,
errors: PropTypes.object,
required: PropTypes.bool,
containerClassNames: PropTypes.string
}
InputField.defaultProps = {
handleOnChange: () => null,
inputValue: '',
name: '',
type: 'text',
label: '',
placeholder: '',
errors: {},
required: false,
containerClassNames: ''
}
export default InputField;
components\context\index.js
(Có chỉnh lại một điều kiện để khi chuyển sang trang khác nó không bị xóa mất cart)
import React, { useState, useEffect } from 'react';
export const AppContext = React.createContext([{}, () => { }]);
export const AppProvider = (props) => {
const [cart, setCart] = useState(null);
/**
* This will be called once on initial load ( component mount ).
*
* Sets the cart data from localStorage to `cart` in the context.
*/
useEffect(() => {
if (typeof window !== 'undefined') {
let cartData = localStorage.getItem('next-cart');
cartData = null !== cartData ? JSON.parse(cartData) : '';
setCart(cartData);
}
}, []);
/**
* 1.When setCart() is called that changes the value of 'cart',
* this will set the new data in the localStorage.
*
* 2.The 'cart' will anyways have the new data, as setCart()
* would have set that.
*/
useEffect(() => {
if (typeof window !== 'undefined' && cart) {
localStorage.setItem('next-cart', JSON.stringify(cart));
}
}, [cart]);
return (
<AppContext.Provider value={[cart, setCart]}>
{props.children}
</AppContext.Provider>
);
};
pages\api\stripe-web-hook.js
import { buffer } from "micro";
const Stripe = require('stripe');
const WooCommerceRestApi = require("@woocommerce/woocommerce-rest-api").default;
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2020-08-27'
});
const webhookSecret = process.env.STRIPE_WEBHOOK_ENDPOINT_SECRET;
export const config = {
api: {
bodyParser: false,
},
};
const api = new WooCommerceRestApi({
url: process.env.NEXT_PUBLIC_WORDPRESS_URL,
consumerKey: process.env.WC_CONSUMER_KEY,
consumerSecret: process.env.WC_CONSUMER_SECRET,
version: "wc/v3"
});
/**
* Update Order.
*
* Once payment is successful or failed,
* Update Order Status to 'Processing' or 'Failed' and set the transaction id.
*
* @param {String} newStatus Order Status to be updated.
* @param {String} orderId Order id
* @param {String} transactionId Transaction id.
*
* @returns {Promise<void>}
*/
const updateOrder = async (newStatus, orderId, transactionId = '') => {
let newOrderData = {
status: newStatus
}
if (transactionId) {
newOrderData.transaction_id = transactionId
}
try {
const { data } = await api.put(`orders/${orderId}`, newOrderData);
console.log('✅ Order updated data', data);
} catch (ex) {
console.error('Order creation error', ex);
throw ex;
}
}
const handler = async (req, res) => {
if (req.method === "POST") {
const buf = await buffer(req);
const sig = req.headers["stripe-signature"];
let stripeEvent;
try {
stripeEvent = stripe.webhooks.constructEvent(buf, sig, webhookSecret);
console.log('stripeEvent', stripeEvent);
} catch (err) {
res.status(400).send(`Webhook Error: ${err.message}`);
return;
}
if ('checkout.session.completed' === stripeEvent.type) {
const session = stripeEvent.data.object;
console.log('sessionsession', session);
console.log('✅ session.metadata.orderId', session.metadata.orderId, session.id);
// Payment Success.
try {
await updateOrder('processing', session.metadata.orderId, session.id);
} catch (error) {
await updateOrder('failed', session.metadata.orderId);
console.error('Update order error', error);
}
}
res.json({ received: true });
} else {
res.setHeader("Allow", "POST");
res.status(405).end("Method Not Allowed");
}
};
export default handler;
pages\api\get-stripe-session.js
const stripe = require( 'stripe' )(process.env.STRIPE_SECRET_KEY);
module.exports = async ( req, res ) => {
const { session_id } = req.query;
const session = await stripe.checkout.sessions.retrieve( session_id );
res.status( 200 ).json( session );
};
pages\api\get-products.js
const WooCommerceRestApi = require("@woocommerce/woocommerce-rest-api").default;
const api = new WooCommerceRestApi({
url: process.env.NEXT_PUBLIC_WORDPRESS_URL,
consumerKey: process.env.WC_CONSUMER_KEY,
consumerSecret: process.env.WC_CONSUMER_SECRET,
version: "wc/v3"
});
/**
* Get Products.
*
* Endpoint /api/get-products or '/api/get-products?perPage=2'
*
* @param req
* @param res
* @return {Promise<void>}
*/
export default async function handler(req, res) {
const responseData = {
success: false,
products: []
}
const { perPage } = req?.query ?? {};
try {
const { data } = await api.get(
'products',
{
per_page: perPage || 50
}
);
responseData.success = true;
responseData.products = data;
res.json(responseData);
} catch (error) {
responseData.error = error.message;
res.status(500).json(responseData);
}
}
pages\api\create-order.js
const WooCommerceRestApi = require( '@woocommerce/woocommerce-rest-api' ).default;
import { isEmpty } from 'lodash';
const api = new WooCommerceRestApi( {
url: process.env.NEXT_PUBLIC_WORDPRESS_URL,
consumerKey: process.env.WC_CONSUMER_KEY,
consumerSecret: process.env.WC_CONSUMER_SECRET,
version: "wc/v3"
} );
/**
* Create order endpoint.
*
* @see http://woocommerce.github.io/woocommerce-rest-api-docs/?javascript#create-an-order
*
* @param {Object} req Request.
* @param {Object} res Response.
*
* @return {Promise<{orderId: string, success: boolean, error: string}>}
*/
export default async function handler( req, res ) {
const responseData = {
success: false,
orderId: '',
total: '',
currency: '',
error: '',
};
if ( isEmpty( req.body ) ) {
responseData.error = 'Required data not sent';
return responseData;
}
const data = req.body;
data.status = 'pending';
data.set_paid = false;
try {
const { data } = await api.post(
'orders',
req.body,
);
responseData.success = true;
responseData.orderId = data.number;
responseData.total = data.total;
responseData.currency = data.currency;
responseData.paymentUrl = data.payment_url;
res.json( responseData );
} catch ( error ) {
console.log( 'error', error );
/**
* Request usually fails if the data in req.body is not sent in the format required.
*
* @see Data shape expected: https://stackoverflow.com/questions/49349396/create-an-order-with-coupon-lines-in-woocomerce-rest-api
*/
responseData.error = error.message;
res.status( 500 ).json( responseData );
}
}
pages\api\stripe\[...nextstripe].js
import NextStripe from 'next-stripe'
export default NextStripe({
stripe_key: process.env.STRIPE_SECRET_KEY
})