├── .babelrc
├── .gitignore
├── README.md
├── doc
└── recording.gif
├── next-env.d.ts
├── next.config.js
├── package.json
├── public
├── favicon.ico
├── images
│ ├── background-3045402_1280.png
│ ├── carousel-demo-images
│ │ ├── bazaar-1853361_1920.jpg
│ │ ├── fashion-1031469_1920.jpg
│ │ └── vinyl-records-945396_1920.jpg
│ ├── guitar-756326_1280.jpg
│ └── person-692159_1280.jpg
└── zeit.svg
├── src
├── actions
│ ├── cart
│ │ ├── cart.ts
│ │ └── cart_interfaces.ts
│ ├── category
│ │ ├── category.ts
│ │ └── category_interfaces.ts
│ ├── index.ts
│ ├── product
│ │ ├── product.ts
│ │ └── product_interfaces.ts
│ └── types.ts
├── assets
│ └── css
│ │ └── antd-custom.less
├── components
│ ├── Cart
│ │ ├── CartItem.tsx
│ │ ├── CartList.tsx
│ │ ├── CartListRenderer.tsx
│ │ ├── Checkout
│ │ │ ├── CheckoutItem.tsx
│ │ │ ├── CheckoutList.less
│ │ │ ├── CheckoutList.tsx
│ │ │ └── CheckoutModal.tsx
│ │ ├── OrderSummary.tsx
│ │ └── ProductInfo.tsx
│ ├── CategoryList
│ │ ├── CategoryItem.tsx
│ │ ├── CategoryList.less
│ │ ├── CategoryList.tsx
│ │ └── CategoryListRenderer.tsx
│ ├── MainCarousel
│ │ ├── MainCarousel.less
│ │ └── MainCarousel.tsx
│ ├── MainLayout
│ │ ├── HeaderMeta.tsx
│ │ ├── MainFooter.tsx
│ │ ├── MainLayout.tsx
│ │ └── MainNav
│ │ │ ├── MainNav.less
│ │ │ └── MainNav.tsx
│ ├── MainPageHeader
│ │ ├── MainPageHeader.less
│ │ └── MainPageHeader.tsx
│ ├── MainRowLayout
│ │ └── MainRowLayout.tsx
│ ├── ProductList
│ │ ├── ProductItem.tsx
│ │ ├── ProductList.less
│ │ ├── ProductList.tsx
│ │ └── ProductListRenderer.tsx
│ ├── ProgressLine.tsx
│ ├── SimpleHeading.tsx
│ ├── SingleProduct
│ │ ├── CartNotification.tsx
│ │ ├── SingleProduct.less
│ │ ├── SingleProduct.tsx
│ │ ├── SingleProductRenderer.tsx
│ │ └── SingleProductSkeleton
│ │ │ ├── SingleProductSkeleton.less
│ │ │ └── SingleProductSkeleton.tsx
│ ├── SkeletonList
│ │ ├── SkeletonItem.tsx
│ │ ├── SkeletonList.less
│ │ └── SkeletonList.tsx
│ └── Spinner
│ │ ├── Spinner.less
│ │ └── Spinner.tsx
├── config
│ └── index.ts
├── contexts
│ └── index.ts
├── helpers
│ ├── cart_helper.ts
│ └── index.ts
├── pages
│ ├── _app.tsx
│ ├── app.less
│ ├── cart.less
│ ├── cart.tsx
│ ├── category
│ │ └── [...category].tsx
│ ├── index.tsx
│ └── product
│ │ └── [...product].tsx
├── reducers
│ ├── cart_reducer.ts
│ ├── category_reducer.ts
│ ├── index.ts
│ └── product_reducer.ts
└── selectors
│ └── index.ts
├── tsconfig.json
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["next/babel"],
3 | "plugins": [
4 | [
5 | "import",
6 | {
7 | "libraryName": "antd",
8 | "style": true
9 | }
10 | ]
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | .env*
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | .now
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | :sparkles::sparkles::sparkles: **I am currently updating this project to use WP GraphQL & NextJS's SSG (Static Site Generation)!** :tada::tada:
2 |
3 | ## :moneybag: React eCommerce Application
4 |
5 |
6 |
7 |
8 |
9 | ## Basic Overview
10 |
11 | This simple eCommerce application shows how to integrate WooCommerce REST API into a NextJS framework. It is also using Typescript, Redux & React hooks making this application fun to use. Also with Ant Design as the design system, this project can be a very good starting point for your next shopping cart website.
12 |
13 | ## Setup
14 |
15 | ```bash
16 | yarn install
17 | yarn dev
18 | ```
19 |
20 | Then open http://localhost:3000/
21 |
22 | ## Copyright and license
23 |
24 | MIT
25 |
--------------------------------------------------------------------------------
/doc/recording.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/loq24/react-ecommerce/e32c2979da2df275fa0da17a718a40920e909dc2/doc/recording.gif
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | const withLess = require('@zeit/next-less');
3 | const lessToJS = require('less-vars-to-js');
4 | const fs = require('fs');
5 | const path = require('path');
6 | const FilterWarningsPlugin = require('webpack-filter-warnings-plugin');
7 |
8 | // Where your antd-custom.less file lives
9 | const themeVariables = lessToJS(
10 | fs.readFileSync(
11 | path.resolve(__dirname, './src/assets/css/antd-custom.less'),
12 | 'utf8'
13 | )
14 | );
15 |
16 | module.exports = withLess({
17 | lessLoaderOptions: {
18 | javascriptEnabled: true,
19 | modifyVars: themeVariables // make your antd custom effective
20 | },
21 | webpack: (config, { isServer }) => {
22 | config.plugins.push(
23 | new FilterWarningsPlugin({
24 | // ignore ANTD chunk styles [mini-css-extract-plugin] warning
25 | exclude: /Conflicting order/
26 | })
27 | );
28 |
29 | if (isServer) {
30 | const antStyles = /antd\/.*?\/style.*?/;
31 | const origExternals = [...config.externals];
32 | config.externals = [
33 | (context, request, callback) => {
34 | if (request.match(antStyles)) return callback();
35 | if (typeof origExternals[0] === 'function') {
36 | origExternals[0](context, request, callback);
37 | } else {
38 | callback();
39 | }
40 | },
41 | ...(typeof origExternals[0] === 'function' ? [] : origExternals)
42 | ];
43 |
44 | config.module.rules.unshift({
45 | test: antStyles,
46 | use: 'null-loader'
47 | });
48 | }
49 | return config;
50 | }
51 | });
52 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-ecommerce",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start"
9 | },
10 | "dependencies": {
11 | "@ant-design/icons": "^4.0.0",
12 | "@woocommerce/woocommerce-rest-api": "^1.0.1",
13 | "antd": "^4.0.2",
14 | "js-cookie": "^2.2.1",
15 | "less": "^3.11.1",
16 | "less-vars-to-js": "^1.3.0",
17 | "next": "9.3.2",
18 | "react": "16.12.0",
19 | "react-dom": "16.12.0",
20 | "react-progress": "^0.0.12",
21 | "react-redux": "^7.2.0",
22 | "redux": "^4.0.5",
23 | "redux-thunk": "^2.3.0"
24 | },
25 | "devDependencies": {
26 | "@testing-library/react": "^10.0.2",
27 | "@types/js-cookie": "^2.2.5",
28 | "@types/node": "^13.7.1",
29 | "@types/react": "^16.9.20",
30 | "@types/react-redux": "^7.1.7",
31 | "@zeit/next-less": "^1.0.1",
32 | "babel-plugin-import": "^1.13.0",
33 | "jest": "^25.2.6",
34 | "null-loader": "^3.0.0",
35 | "typescript": "^3.7.5",
36 | "webpack-filter-warnings-plugin": "^1.2.1"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/loq24/react-ecommerce/e32c2979da2df275fa0da17a718a40920e909dc2/public/favicon.ico
--------------------------------------------------------------------------------
/public/images/background-3045402_1280.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/loq24/react-ecommerce/e32c2979da2df275fa0da17a718a40920e909dc2/public/images/background-3045402_1280.png
--------------------------------------------------------------------------------
/public/images/carousel-demo-images/bazaar-1853361_1920.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/loq24/react-ecommerce/e32c2979da2df275fa0da17a718a40920e909dc2/public/images/carousel-demo-images/bazaar-1853361_1920.jpg
--------------------------------------------------------------------------------
/public/images/carousel-demo-images/fashion-1031469_1920.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/loq24/react-ecommerce/e32c2979da2df275fa0da17a718a40920e909dc2/public/images/carousel-demo-images/fashion-1031469_1920.jpg
--------------------------------------------------------------------------------
/public/images/carousel-demo-images/vinyl-records-945396_1920.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/loq24/react-ecommerce/e32c2979da2df275fa0da17a718a40920e909dc2/public/images/carousel-demo-images/vinyl-records-945396_1920.jpg
--------------------------------------------------------------------------------
/public/images/guitar-756326_1280.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/loq24/react-ecommerce/e32c2979da2df275fa0da17a718a40920e909dc2/public/images/guitar-756326_1280.jpg
--------------------------------------------------------------------------------
/public/images/person-692159_1280.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/loq24/react-ecommerce/e32c2979da2df275fa0da17a718a40920e909dc2/public/images/person-692159_1280.jpg
--------------------------------------------------------------------------------
/public/zeit.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/actions/cart/cart.ts:
--------------------------------------------------------------------------------
1 | import Cookies from 'js-cookie';
2 | import { Dispatch } from 'redux';
3 | import { CartTypes } from '../types';
4 | import {
5 | AddToCart,
6 | RemoveFromCart,
7 | UpdateCartItemCount
8 | } from './cart_interfaces';
9 |
10 | export const addToCart = (id: string, price: string, count = 1) => {
11 | return (dispatch: Dispatch) => {
12 | dispatch({
13 | type: CartTypes.addToCart,
14 | payload: { id, price, count }
15 | });
16 | };
17 | };
18 |
19 | export const removeFromCart = (id: string) => {
20 | return (dispatch: Dispatch) => {
21 | dispatch({
22 | type: CartTypes.removeFromCart,
23 | payload: id
24 | });
25 | };
26 | };
27 |
28 | export const updateCartItemCount = (
29 | id: string,
30 | price: string,
31 | count: number
32 | ) => {
33 | return (dispatch: Dispatch) => {
34 | dispatch({
35 | type: CartTypes.updateCartItemCount,
36 | payload: { id, price, count }
37 | });
38 | };
39 | };
40 |
--------------------------------------------------------------------------------
/src/actions/cart/cart_interfaces.ts:
--------------------------------------------------------------------------------
1 | import { CartTypes } from '../types';
2 |
3 | export interface Cart {
4 | id: string;
5 | price: string;
6 | count: number;
7 | }
8 |
9 | export interface AddToCart {
10 | type: CartTypes.addToCart;
11 | payload: Cart;
12 | }
13 |
14 | export interface RemoveFromCart {
15 | type: CartTypes.removeFromCart;
16 | payload: string;
17 | }
18 |
19 | export interface UpdateCartItemCount {
20 | type: CartTypes.updateCartItemCount;
21 | payload: Cart;
22 | }
23 |
--------------------------------------------------------------------------------
/src/actions/category/category.ts:
--------------------------------------------------------------------------------
1 | import { Dispatch } from 'redux';
2 | import { wooApi } from '../../config';
3 | import {
4 | FetchMainProductCategories,
5 | FetchCategory
6 | } from './category_interfaces';
7 | import { CategoryTypes } from '../types';
8 |
9 | export const fetchMainProductCategories = () => {
10 | return async (dispatch: Dispatch) => {
11 | try {
12 | const response = await wooApi.get(
13 | `products/categories?hide_empty=true&parent=0`
14 | );
15 | dispatch({
16 | type: CategoryTypes.fetchMainProductCategories,
17 | payload: response.data
18 | });
19 | } catch (error) {
20 | console.log(error);
21 | }
22 | };
23 | };
24 |
25 | export const fetchCategory = (id: string) => {
26 | return async (dispatch: Dispatch) => {
27 | try {
28 | const response = await wooApi.get(`products/categories/${id}`);
29 | dispatch({
30 | type: CategoryTypes.fetchCategory,
31 | payload: response.data
32 | });
33 | } catch (error) {
34 | console.log(error);
35 | }
36 | };
37 | };
38 |
--------------------------------------------------------------------------------
/src/actions/category/category_interfaces.ts:
--------------------------------------------------------------------------------
1 | import { CategoryTypes } from '../types';
2 |
3 | export interface ProductCategory {
4 | id: number;
5 | name: string;
6 | slug: string;
7 | description: string;
8 | parent: number;
9 | count: number;
10 | image: {
11 | id: number;
12 | src: string;
13 | };
14 | }
15 |
16 | export interface FetchMainProductCategories {
17 | type: CategoryTypes.fetchMainProductCategories;
18 | payload: ProductCategory[];
19 | }
20 |
21 | export interface FetchCategory {
22 | type: CategoryTypes.fetchCategory;
23 | payload: ProductCategory;
24 | }
25 |
--------------------------------------------------------------------------------
/src/actions/index.ts:
--------------------------------------------------------------------------------
1 | export * from './types';
2 | export * from './product/product';
3 | export * from './product/product_interfaces';
4 | export * from './category/category';
5 | export * from './category/category_interfaces';
6 | export * from './cart/cart';
7 | export * from './cart/cart_interfaces';
8 |
--------------------------------------------------------------------------------
/src/actions/product/product.ts:
--------------------------------------------------------------------------------
1 | //@ts-nocheck
2 | import { wooApi } from '../../config';
3 | import { Dispatch } from 'redux';
4 | import { ProductTypes } from '../types';
5 | import {
6 | FetchSaleProducts,
7 | FetchCategoryProducts,
8 | FetchProductById,
9 | FetchProductsByIds
10 | } from './product_interfaces';
11 |
12 | export const fetchSaleProducts = (itemCount = 4) => {
13 | return async (dispatch: Dispatch) => {
14 | try {
15 | const response = await wooApi.get(
16 | `products?per_page=${itemCount}&purchasable=true&on_sale=true`
17 | );
18 |
19 | dispatch({
20 | type: ProductTypes.fetchSaleProduts,
21 | payload: response.data
22 | });
23 | } catch (error) {
24 | console.log(error);
25 | }
26 | };
27 | };
28 |
29 | export const fetchCategoryProducts = (
30 | categoryId: string,
31 | callback?: () => void
32 | ) => {
33 | return async (dispatch: Dispatch) => {
34 | try {
35 | const response = await wooApi.get(`products?category=${categoryId}`);
36 |
37 | dispatch({
38 | type: ProductTypes.fetchCategoryProducts,
39 | payload: response.data
40 | });
41 | callback();
42 | } catch (error) {
43 | console.log(error);
44 | }
45 | };
46 | };
47 |
48 | export const fetchProductById = (id: string, callback?: () => void) => {
49 | return async (dispatch: Dispatch) => {
50 | try {
51 | const response = await wooApi.get(`products/${id}`);
52 |
53 | dispatch({
54 | type: ProductTypes.fetchProductById,
55 | payload: response.data
56 | });
57 | callback();
58 | } catch (error) {
59 | console.log(error);
60 | }
61 | };
62 | };
63 |
64 | export const fetchProductsByIds = (ids: string) => {
65 | return async (dispatch: Dispatch) => {
66 | try {
67 | const response = await wooApi.get(`products?include=${ids}`);
68 |
69 | dispatch({
70 | type: ProductTypes.fetchProductsByIds,
71 | payload: response.data
72 | });
73 | } catch (error) {
74 | console.log(error);
75 | }
76 | };
77 | };
78 |
--------------------------------------------------------------------------------
/src/actions/product/product_interfaces.ts:
--------------------------------------------------------------------------------
1 | import { ProductTypes } from '../types';
2 |
3 | export interface ProductImage {
4 | id: number;
5 | src: string;
6 | alt: string;
7 | }
8 |
9 | export interface Product {
10 | id: number;
11 | name: string;
12 | slug: string;
13 | date_created: string;
14 | description: string;
15 | price: string;
16 | regular_price: string;
17 | sale_price: string;
18 | on_sale: boolean;
19 | related_ids: number[];
20 | images: ProductImage[];
21 | }
22 |
23 | export interface FetchSaleProducts {
24 | type: ProductTypes.fetchSaleProduts;
25 | payload: Product[];
26 | }
27 |
28 | export interface FetchCategoryProducts {
29 | type: ProductTypes.fetchCategoryProducts;
30 | payload: Product[];
31 | }
32 |
33 | export interface FetchProductById {
34 | type: ProductTypes.fetchProductById;
35 | payload: Product;
36 | }
37 |
38 | export interface FetchProductsByIds {
39 | type: ProductTypes.fetchProductsByIds;
40 | payload: Product[];
41 | }
42 |
--------------------------------------------------------------------------------
/src/actions/types.ts:
--------------------------------------------------------------------------------
1 | export enum ProductTypes {
2 | fetchSaleProduts = 'FETCH_SALE_PRODUCTS',
3 | fetchCategoryProducts = 'FETCH_CATEGORY_PRODUCTS',
4 | fetchProductById = 'FETCH_PRODUCT_BY_ID',
5 | fetchProductsByIds = 'FETCH_PRODUCTS_BY_IDS'
6 | }
7 |
8 | export enum CategoryTypes {
9 | fetchMainProductCategories = 'FETCH_MAIN_PRODUCT_CATEGORIES',
10 | fetchCategory = 'FETCH_CATEGORY'
11 | }
12 |
13 | export enum CartTypes {
14 | addToCart = 'ADD_TO_CART',
15 | removeFromCart = 'REMOVE_FROM_CART',
16 | updateCartItemCount = 'UPDATE_CART_ITEM_COUNT'
17 | }
18 |
--------------------------------------------------------------------------------
/src/assets/css/antd-custom.less:
--------------------------------------------------------------------------------
1 | @primary-color: #1890ff; // primary color for all components
2 | @link-color: #1890ff; // link color
3 | @success-color: #52c41a; // success state color
4 | @warning-color: #faad14; // warning state color
5 | @error-color: #f5222d; // error state color
6 | @font-size-base: 14px; // major text font size
7 | @heading-color: rgba(0, 0, 0, 0.85); // heading text color
8 | @text-color: rgba(0, 0, 0, 0.65); // major text color
9 | @text-color-secondary: rgba(0, 0, 0, 0.45); // secondary text color
10 | @disabled-color: rgba(0, 0, 0, 0.25); // disable state color
11 | @border-radius-base: 4px; // major border radius
12 | @border-color-base: #d9d9d9; // major border color
13 | @box-shadow-base: 0 2px 8px rgba(0, 0, 0, 0.15); // major shadow for layers
14 |
15 | @primary-color: #ff4c3b;
16 | @menu-item-color: #777;
17 | @layout-header-padding: 0px 0px;
18 | @layout-header-background: #ffffff;
19 | @layout-body-background: #ffffff;
20 | @layout-footer-padding: 24px 50px;
21 | @layout-footer-background: @layout-header-background;
22 | @border-radius-base: 2px;
23 | @collapse-content-bg: #ffffff;
24 | @full-box-width: 1200px;
25 |
--------------------------------------------------------------------------------
/src/components/Cart/CartItem.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { useDispatch } from 'react-redux';
3 | import { Product, removeFromCart, updateCartItemCount } from '../../actions';
4 | import { InputNumber } from 'antd';
5 | import { DeleteOutlined } from '@ant-design/icons';
6 | import { useCartSelector } from '../../selectors';
7 | import { getCartItemCount } from '../../helpers';
8 | import ProductInfo from './ProductInfo';
9 |
10 | interface CartItemProps {
11 | product: Product;
12 | }
13 |
14 | const CartItem: React.FC = ({ product }) => {
15 | const [isDeleting, setDeleting] = useState(false);
16 | const [itemCount, setItemCount] = useState(0);
17 |
18 | const { id, price } = product;
19 | const product_id = `${id}`;
20 |
21 | const { items } = useCartSelector();
22 | const dispatch = useDispatch();
23 |
24 | const removeThisItem = () => {
25 | setDeleting(true);
26 | dispatch(removeFromCart(product_id));
27 | };
28 |
29 | const handleUpdateCartItem = (count = 1) => {
30 | if (itemCount > count) {
31 | dispatch(updateCartItemCount(product_id, price, -1));
32 | } else {
33 | dispatch(updateCartItemCount(product_id, price, 1));
34 | }
35 | };
36 |
37 | useEffect(() => {
38 | const totalItemCount = getCartItemCount(items, product_id);
39 | setItemCount(totalItemCount);
40 | });
41 |
42 | return (
43 |
44 |
45 |
46 | handleUpdateCartItem(count)}
51 | />
52 |
53 |
54 |
55 |
56 |
57 | );
58 | };
59 |
60 | export default CartItem;
61 |
--------------------------------------------------------------------------------
/src/components/Cart/CartList.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import CartItem from './CartItem';
3 | import { Product } from '../../actions';
4 |
5 | interface CartListProps {
6 | products: Product[];
7 | }
8 |
9 | const CartList: React.FC = ({ products }) => {
10 | return (
11 |
12 | {products.map((product) => {
13 | return ;
14 | })}
15 |
16 | );
17 | };
18 |
19 | export default CartList;
20 |
--------------------------------------------------------------------------------
/src/components/Cart/CartListRenderer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Product } from '../../actions';
3 | import CartList from './CartList';
4 | import SkeletonList from '../SkeletonList/SkeletonList';
5 |
6 | interface CartListRendererProps {
7 | cartProducts: Product[];
8 | totalItems: number;
9 | }
10 |
11 | const CartListRenderer: React.FC = ({
12 | cartProducts,
13 | totalItems
14 | }) => {
15 | return (
16 | <>
17 | {cartProducts.length > 0 && totalItems > 0 ? (
18 |
19 | ) : (
20 |
21 | )}
22 | >
23 | );
24 | };
25 |
26 | export default CartListRenderer;
27 |
--------------------------------------------------------------------------------
/src/components/Cart/Checkout/CheckoutItem.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Link from 'next/link';
3 | import { Product } from '../../../actions';
4 | import { useCartSelector } from '../../../selectors';
5 | import { getCartItemCount } from '../../../helpers';
6 | import ProductInfo from '../ProductInfo';
7 |
8 | interface CheckoutItemProps {
9 | product: Product;
10 | }
11 |
12 | const CheckoutItem: React.FC = ({ product }) => {
13 | const { items } = useCartSelector();
14 | const { id, price } = product;
15 |
16 | const product_id = `${id}`;
17 | const totalItemCount = getCartItemCount(items, product_id);
18 | const subtotal = parseFloat(price) * totalItemCount;
19 |
20 | return (
21 |
22 |
23 |
{totalItemCount}
24 |
${subtotal}
25 |
26 | );
27 | };
28 |
29 | export default CheckoutItem;
30 |
--------------------------------------------------------------------------------
/src/components/Cart/Checkout/CheckoutList.less:
--------------------------------------------------------------------------------
1 | .checkout-list {
2 | .table-heading {
3 | display: flex;
4 | flex-flow: row wrap;
5 | font-size: 16px;
6 | margin-bottom: 20px;
7 | @media (max-width: 767px) {
8 | display: none;
9 | }
10 |
11 | & > div:not(:first-child) {
12 | width: 20%;
13 | color: #bbb;
14 | }
15 | & > div:first-child {
16 | width: 385px;
17 | padding-left: 10px;
18 | }
19 | & > div:last-child {
20 | color: #bbb;
21 | margin-left: auto;
22 | text-align: center;
23 | }
24 | }
25 | .overall-total-price {
26 | width: 100%;
27 | display: flex;
28 | text-align: center;
29 | padding: 25px;
30 | background: #f7f7f7;
31 | justify-content: flex-end;
32 | div {
33 | width: 25%;
34 | font-size: 15px;
35 | @media (max-width: 767px) {
36 | width: 35%;
37 | }
38 | span {
39 | font-size: 30px;
40 | margin-left: 10px;
41 | color: @primary-color;
42 | }
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/Cart/Checkout/CheckoutList.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import CheckoutItem from './CheckoutItem';
3 | import { Product } from '../../../actions';
4 | import { CartContext } from '../../../contexts';
5 | import './CheckoutList.less';
6 |
7 | interface CheckoutListProps {
8 | products: Product[];
9 | }
10 |
11 | const CheckoutList: React.FC = ({ products }) => {
12 | const { totalPrice } = React.useContext(CartContext);
13 |
14 | return (
15 |
16 |
17 |
Products Ordered
18 |
Amount
19 |
Item Subtotal
20 |
21 | {products.map((product) => {
22 | return
;
23 | })}
24 |
25 |
26 | TOTAL: ${totalPrice}
27 |
28 |
29 |
30 | );
31 | };
32 |
33 | export default CheckoutList;
34 |
--------------------------------------------------------------------------------
/src/components/Cart/Checkout/CheckoutModal.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Modal } from 'antd';
3 | import CheckoutList from './CheckoutList';
4 | import { useProductSelector } from '../../../selectors';
5 |
6 | interface CheckoutSummaryProps {
7 | visible: boolean;
8 | hideModal: () => void;
9 | }
10 |
11 | const CheckoutSummary: React.FC = ({
12 | visible,
13 | hideModal,
14 | }) => {
15 | const { cartProducts } = useProductSelector();
16 |
17 | return (
18 |
26 |
27 |
28 | );
29 | };
30 |
31 | export default CheckoutSummary;
32 |
--------------------------------------------------------------------------------
/src/components/Cart/OrderSummary.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Typography, Button } from 'antd';
3 | import { Product } from '../../actions';
4 | import { CartContext } from '../../contexts';
5 | import CheckoutModal from './Checkout/CheckoutModal';
6 |
7 | const { Title, Text } = Typography;
8 |
9 | interface OrderSummaryProps {
10 | cartProducts: Product[];
11 | totalItems: number;
12 | }
13 |
14 | const OrderSummary: React.FC = ({
15 | cartProducts,
16 | totalItems,
17 | }) => {
18 | const { totalPrice } = React.useContext(CartContext);
19 | const [modalVisibility, setModalVisibility] = useState(false);
20 |
21 | return (
22 |
23 |
Order Summary
24 |
25 | Total
26 | ₱{Number(totalPrice)}
27 |
28 |
36 |
setModalVisibility(false)}
39 | />
40 |
41 | );
42 | };
43 |
44 | export default OrderSummary;
45 |
--------------------------------------------------------------------------------
/src/components/Cart/ProductInfo.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Link from 'next/link';
3 | import { Product } from '../../actions';
4 | import { Typography } from 'antd';
5 |
6 | const { Title, Text } = Typography;
7 |
8 | interface ProductInfoProps {
9 | product: Product;
10 | }
11 |
12 | const ProductInfo: React.FC = ({ product }) => {
13 | const {
14 | id,
15 | name,
16 | images,
17 | regular_price,
18 | sale_price,
19 | on_sale,
20 | slug,
21 | } = product;
22 | const featured_image = images.length > 0 ? images[0].src : '';
23 | const product_id = `${id}`;
24 | return (
25 | <>
26 |
34 |
54 | >
55 | );
56 | };
57 |
58 | export default ProductInfo;
59 |
--------------------------------------------------------------------------------
/src/components/CategoryList/CategoryItem.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Link from 'next/link';
3 | import { ProductCategory } from '../../actions';
4 | import { Col, Card } from 'antd';
5 | import { SkeletonListContext } from '../../contexts';
6 | const { Meta } = Card;
7 |
8 | interface CategoryItemProps {
9 | category: ProductCategory;
10 | }
11 |
12 | const CategoryItem: React.FC = ({ category }) => {
13 | const { xl, md, sm, lg, xs } = React.useContext(SkeletonListContext);
14 | const { name, description, image } = category;
15 | const featured_image = image?.src ?? '';
16 |
17 | return (
18 |
19 |
24 | : null
29 | }
30 | >
31 |
32 |
33 |
34 |
35 | );
36 | };
37 |
38 | export default CategoryItem;
39 |
--------------------------------------------------------------------------------
/src/components/CategoryList/CategoryList.less:
--------------------------------------------------------------------------------
1 | .category-item-card {
2 | .ant-card-cover {
3 | min-height: 224px;
4 | background-color: #ededed;
5 | @media (max-width: 1200px) {
6 | min-height: auto;
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/CategoryList/CategoryList.tsx:
--------------------------------------------------------------------------------
1 | import { ProductCategory } from '../../actions';
2 | import CategoryItem from './CategoryItem';
3 | import MainRowLayout from '../MainRowLayout/MainRowLayout';
4 | import './CategoryList.less';
5 |
6 | interface CategoryListProps {
7 | categories: ProductCategory[];
8 | }
9 |
10 | const CategoryList: React.FC = ({ categories }) => {
11 | return (
12 |
13 | {categories.map(category => {
14 | return ;
15 | })}
16 |
17 | );
18 | };
19 |
20 | export default CategoryList;
21 |
--------------------------------------------------------------------------------
/src/components/CategoryList/CategoryListRenderer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import CategoryList from './CategoryList';
3 | import SkeletonList from '../SkeletonList/SkeletonList';
4 | import { ProductCategory } from '../../actions';
5 | import { SkeletonListContext, Breakpoints } from '../../contexts';
6 |
7 | interface CategoryListRendererProps {
8 | categories: ProductCategory[];
9 | breakpoints: Breakpoints;
10 | }
11 |
12 | const CategoryListRenderer: React.FC = ({
13 | categories,
14 | breakpoints
15 | }) => {
16 | return (
17 |
18 | {categories.length === 0 ? (
19 |
20 | ) : (
21 |
22 | )}
23 |
24 | );
25 | };
26 |
27 | export default CategoryListRenderer;
28 |
--------------------------------------------------------------------------------
/src/components/MainCarousel/MainCarousel.less:
--------------------------------------------------------------------------------
1 | .ant-carousel {
2 | .slick-slide {
3 | text-align: center;
4 | height: 500px;
5 | line-height: 160px;
6 | background: #364d79;
7 | overflow: hidden;
8 | }
9 | }
10 |
11 | .ant-carousel {
12 | .slick-slide {
13 | img {
14 | object-fit: cover;
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/MainCarousel/MainCarousel.tsx:
--------------------------------------------------------------------------------
1 | import { Carousel } from 'antd';
2 | import './MainCarousel.less';
3 |
4 | const MainCarousel = () => {
5 | return (
6 |
7 |
8 |

9 |
10 |
11 |

12 |
13 |
14 |

15 |
16 |
17 | );
18 | };
19 |
20 | export default MainCarousel;
21 |
--------------------------------------------------------------------------------
/src/components/MainLayout/HeaderMeta.tsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head';
2 |
3 | interface HeaderMetaProps {
4 | title: string;
5 | }
6 |
7 | const HeaderMeta: React.FC = ({ title }) => {
8 | return (
9 |
10 | {title}
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | export default HeaderMeta;
18 |
--------------------------------------------------------------------------------
/src/components/MainLayout/MainFooter.tsx:
--------------------------------------------------------------------------------
1 | import { Layout } from 'antd';
2 | const { Footer } = Layout;
3 |
4 | const MainFooter = () => {
5 | return (
6 |
17 | );
18 | };
19 |
20 | export default MainFooter;
21 |
--------------------------------------------------------------------------------
/src/components/MainLayout/MainLayout.tsx:
--------------------------------------------------------------------------------
1 | import HeaderMeta from './HeaderMeta';
2 | import MainNav from './MainNav/MainNav';
3 | import MainFooter from './MainFooter';
4 | import { Layout } from 'antd';
5 |
6 | const { Content } = Layout;
7 |
8 | interface LayoutProps {
9 | children: React.ReactNode;
10 | title: string;
11 | }
12 |
13 | const MainLayout: React.FC = ({ children, title }) => {
14 | return (
15 | <>
16 |
17 |
18 |
19 | {children}
20 |
21 |
22 | >
23 | );
24 | };
25 |
26 | export default MainLayout;
27 |
--------------------------------------------------------------------------------
/src/components/MainLayout/MainNav/MainNav.less:
--------------------------------------------------------------------------------
1 | .main-nav {
2 | max-width: @full-box-width;
3 | width: 100%;
4 | margin: 0 auto;
5 | padding: 0 16px;
6 | .left-nav-items {
7 | display: flex;
8 | flex-flow: row wrap;
9 | justify-content: space-between;
10 | width: 80px;
11 | a {
12 | color: @menu-item-color;
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/MainLayout/MainNav/MainNav.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Link from 'next/link';
3 | import { Layout, Row, Col, Badge } from 'antd';
4 | import { ShoppingCartOutlined, GithubOutlined } from '@ant-design/icons';
5 | import { useCartSelector } from '../../../selectors';
6 | import './MainNav.less';
7 |
8 | const { Header } = Layout;
9 |
10 | const MainNav = () => {
11 | const { totalItems } = useCartSelector();
12 |
13 | return (
14 |
15 |
16 |
17 |
25 |
26 |
27 |
28 |
29 |
37 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | );
47 | };
48 |
49 | export default MainNav;
50 |
--------------------------------------------------------------------------------
/src/components/MainPageHeader/MainPageHeader.less:
--------------------------------------------------------------------------------
1 | .site-page-header {
2 | margin: 30px auto 20px;
3 |
4 | .ant-page-header-heading-title {
5 | text-transform: capitalize;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/MainPageHeader/MainPageHeader.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useRouter } from 'next/router';
3 | import { PageHeader } from 'antd';
4 | import './MainPageHeader.less';
5 |
6 | interface MainPageHeaderProps {
7 | title: string;
8 | subTitle?: string;
9 | }
10 |
11 | const MainPageHeader: React.FC = ({
12 | title,
13 | subTitle = ''
14 | }) => {
15 | const router = useRouter();
16 | const goBack = React.useCallback(() => {
17 | router.back();
18 | }, []);
19 |
20 | return (
21 |
27 | );
28 | };
29 |
30 | export default MainPageHeader;
31 |
--------------------------------------------------------------------------------
/src/components/MainRowLayout/MainRowLayout.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Layout, Row } from 'antd';
3 |
4 | interface MainRowLayoutProps {
5 | children: React.ReactNode;
6 | rowClassName?: string;
7 | }
8 |
9 | const MainRowLayout: React.FC = ({
10 | children,
11 | rowClassName
12 | }) => {
13 | return (
14 |
15 |
16 | {children}
17 |
18 |
19 | );
20 | };
21 |
22 | export default MainRowLayout;
23 |
--------------------------------------------------------------------------------
/src/components/ProductList/ProductItem.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Link from 'next/link';
3 | import { Product } from '../../actions';
4 | import { Row, Col, Card, Typography, Button } from 'antd';
5 | import { SkeletonListContext } from '../../contexts';
6 | const { Text } = Typography;
7 |
8 | interface SaleProductItemProps {
9 | product: Product;
10 | }
11 |
12 | const SaleProductItem: React.FC = ({ product }) => {
13 | const { xl, md, sm, lg, xs } = React.useContext(SkeletonListContext);
14 | const {
15 | id,
16 | slug,
17 | name,
18 | regular_price,
19 | sale_price,
20 | on_sale,
21 | images
22 | } = product;
23 | const featured_image = images.length > 0 ? images[0].src : '';
24 | return (
25 |
26 |
27 | : null
31 | }
32 | >
33 |
34 |
35 | {name}
36 |
37 | {on_sale && }
38 |
39 |
40 |
41 | {`$${regular_price}`}
42 |
43 | {on_sale && (
44 | {`$${sale_price}`}
45 | )}
46 |
47 |
48 |
49 |
50 | );
51 | };
52 |
53 | export default SaleProductItem;
54 |
--------------------------------------------------------------------------------
/src/components/ProductList/ProductList.less:
--------------------------------------------------------------------------------
1 | .product-list {
2 | .ant-card-cover {
3 | min-height: 282px;
4 | background-color: #ededed;
5 | @media (max-width: 1200px) {
6 | min-height: auto;
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/ProductList/ProductList.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Product } from '../../actions';
3 | import ProductItem from './ProductItem';
4 | import MainRowLayout from '../MainRowLayout/MainRowLayout';
5 | import './ProductList.less';
6 |
7 | interface SaleProductListProps {
8 | products: Product[];
9 | }
10 |
11 | const ProductList: React.FC = ({ products }) => {
12 | return (
13 |
14 | {products.map(product => {
15 | return ;
16 | })}
17 |
18 | );
19 | };
20 |
21 | export default ProductList;
22 |
--------------------------------------------------------------------------------
/src/components/ProductList/ProductListRenderer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ProductList from './ProductList';
3 | import Spinner from '../Spinner/Spinner';
4 | import { Product } from '../../actions';
5 | import SkeletonList from '../SkeletonList/SkeletonList';
6 | import { SkeletonListContext, Breakpoints } from '../../contexts';
7 |
8 | interface ProductListRendererProps {
9 | products: Product[];
10 | skeletonCount?: number;
11 | skeleton?: boolean;
12 | spin?: boolean;
13 | breakpoints: Breakpoints;
14 | }
15 |
16 | const ProductListRenderer: React.FC = ({
17 | products,
18 | skeleton,
19 | skeletonCount = 0,
20 | spin,
21 | breakpoints
22 | }) => {
23 | return (
24 |
25 | {skeleton && products.length === 0 && (
26 |
27 | )}
28 | {products.length > 0 && !spin && }
29 | {spin && }
30 |
31 | );
32 | };
33 |
34 | export default ProductListRenderer;
35 |
--------------------------------------------------------------------------------
/src/components/ProgressLine.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | // @ts-ignore
3 | import Progress from 'react-progress';
4 | import Router from 'next/router';
5 |
6 | const ProgressLine = () => {
7 | const [percent, setPercent] = useState(0);
8 |
9 | useEffect(() => {
10 | Router.events.on('routeChangeStart', () => {
11 | setPercent(95);
12 | });
13 | Router.events.on('routeChangeComplete', () => {
14 | setPercent(100);
15 | });
16 | });
17 |
18 | return (
19 |
22 | );
23 | };
24 |
25 | export default ProgressLine;
26 |
--------------------------------------------------------------------------------
/src/components/SimpleHeading.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Typography, Row, Col } from 'antd';
3 | const { Title } = Typography;
4 |
5 | interface SimpleHeadingProps extends React.CSSProperties {
6 | title: string;
7 | level?: 1 | 2 | 3 | 4;
8 | }
9 |
10 | const SimpleHeading: React.FC = ({
11 | marginTop = 50,
12 | marginBottom = 30,
13 | textTransform = 'none',
14 | level,
15 | title
16 | }) => {
17 | return (
18 |
19 |
20 |
21 | {title}
22 |
23 |
24 |
25 | );
26 | };
27 |
28 | export default SimpleHeading;
29 |
--------------------------------------------------------------------------------
/src/components/SingleProduct/CartNotification.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { notification, Button } from 'antd';
3 | import { SmileOutlined } from '@ant-design/icons';
4 | import Link from 'next/link';
5 |
6 | const cartNotification = () => {
7 | const key = `open${Date.now()}`;
8 | const btn = (
9 |
10 |
17 |
18 | );
19 |
20 | notification.open({
21 | message: 'Item added to cart',
22 | description:
23 | 'You have successfully added an item to your cart. To check out, click the button below.',
24 | icon: ,
25 | top: 50,
26 | duration: 2,
27 | btn,
28 | key
29 | });
30 | };
31 |
32 | export default cartNotification;
33 |
--------------------------------------------------------------------------------
/src/components/SingleProduct/SingleProduct.less:
--------------------------------------------------------------------------------
1 | .product-wrapper {
2 | max-width: 700px;
3 | margin: 50px auto;
4 | @media (max-width: 767px) {
5 | margin: 0 auto;
6 | }
7 | .product-image {
8 | background-color: #ededed;
9 | img {
10 | width: 100%;
11 | object-fit: cover;
12 | }
13 | }
14 | .product-description {
15 | padding-left: 25px;
16 | @media (max-width: 700px) {
17 | padding-right: 25px;
18 | }
19 | .price-description {
20 | span.ant-typography {
21 | font-size: 30px;
22 | &:not(.on_sale) {
23 | color: @primary-color;
24 | }
25 | }
26 | }
27 | .ant-descriptions-title {
28 | font-size: 35px;
29 | }
30 | .ant-descriptions-item-label {
31 | font-size: 18px;
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/SingleProduct/SingleProduct.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Row, Col, Typography, Descriptions, Button } from 'antd';
3 | import cartNotification from './CartNotification';
4 | import { Product, addToCart } from '../../actions';
5 | import { useDispatch } from 'react-redux';
6 | import { SingleProductContext } from '../../contexts';
7 | import './SingleProduct.less';
8 |
9 | const { Text } = Typography;
10 | const { Item } = Descriptions;
11 |
12 | interface SingleProductProps {
13 | product: Product;
14 | }
15 |
16 | const SingleProduct: React.FC = ({ product }) => {
17 | const breakpoints = React.useContext(SingleProductContext);
18 | const dispatch = useDispatch();
19 |
20 | const {
21 | id,
22 | name,
23 | description,
24 | images,
25 | regular_price,
26 | sale_price,
27 | on_sale,
28 | price
29 | } = product;
30 | const productId = `${id}`;
31 | const featured_image = images.length > 0 ? images[0].src : '';
32 |
33 | const addItemToCart = () => {
34 | dispatch(addToCart(productId, price));
35 | cartNotification();
36 | };
37 |
38 | return (
39 | <>
40 |
41 |
48 | {featured_image &&
}
49 |
50 |
57 |
58 | -
59 |
64 | ${regular_price}
65 |
66 | {on_sale && ${sale_price}}
67 |
68 | -
69 |
70 |
71 | -
72 |
75 |
76 |
77 |
78 |
79 | >
80 | );
81 | };
82 |
83 | export default SingleProduct;
84 |
--------------------------------------------------------------------------------
/src/components/SingleProduct/SingleProductRenderer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Product } from '../../actions';
3 | import SingleProduct from './SingleProduct';
4 | import SingleProductSkeleton from './SingleProductSkeleton/SingleProductSkeleton';
5 | import { SingleProductContext, Breakpoints } from '../../contexts';
6 |
7 | interface SingleProductRendererProps {
8 | product?: Product;
9 | loading: boolean;
10 | breakpoints: Breakpoints[];
11 | }
12 |
13 | const SingleProductRenderer: React.FC = ({
14 | product,
15 | loading,
16 | breakpoints
17 | }) => {
18 | return (
19 |
20 | {!product || loading ? (
21 |
22 | ) : (
23 |
24 | )}
25 |
26 | );
27 | };
28 |
29 | export default SingleProductRenderer;
30 |
--------------------------------------------------------------------------------
/src/components/SingleProduct/SingleProductSkeleton/SingleProductSkeleton.less:
--------------------------------------------------------------------------------
1 | .product-skeleton-wrapper {
2 | max-width: 700px;
3 | margin: 50px auto;
4 |
5 | .product-skeleton-image {
6 | @media (max-width: 700px) {
7 | width: 100%;
8 | }
9 | .ant-skeleton-header {
10 | padding-right: 0;
11 | .ant-skeleton-avatar {
12 | width: 100%;
13 | min-height: 350px;
14 | }
15 | }
16 | }
17 | .product-skeleton-description {
18 | padding-left: 25px;
19 | @media (max-width: 700px) {
20 | width: 100%;
21 | padding-right: 25px;
22 | }
23 | .ant-skeleton-title {
24 | height: 40px;
25 | margin-bottom: 35px;
26 | }
27 | .ant-skeleton-paragraph {
28 | & > li:nth-child(1) {
29 | height: 30px;
30 | margin-bottom: 30px;
31 | }
32 | & > li:nth-child(2) {
33 | height: 20px;
34 | }
35 | }
36 | .ant-skeleton-button {
37 | width: 100px;
38 | margin-top: 20px;
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/SingleProduct/SingleProductSkeleton/SingleProductSkeleton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Row, Col, Skeleton } from 'antd';
3 | import { SingleProductContext } from '../../../contexts';
4 | import './SingleProductSkeleton.less';
5 |
6 | const { Button } = Skeleton;
7 |
8 | const SingleProductSkeleton: React.FC = () => {
9 | const breakpoints = React.useContext(SingleProductContext);
10 |
11 | return (
12 |
13 |
20 |
27 |
28 |
35 |
45 |
46 |
47 |
48 | );
49 | };
50 |
51 | export default SingleProductSkeleton;
52 |
--------------------------------------------------------------------------------
/src/components/SkeletonList/SkeletonItem.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Skeleton, Col } from 'antd';
3 | import { SkeletonListContext } from '../../contexts';
4 |
5 | interface SkeletonItemProps {
6 | itemCount: number;
7 | }
8 |
9 | const SkeletonItem: React.FC = ({ itemCount }) => {
10 | const { xl, md, sm, lg, xs } = React.useContext(SkeletonListContext);
11 |
12 | return (
13 |
21 |
28 |
29 | );
30 | };
31 |
32 | export default SkeletonItem;
33 |
--------------------------------------------------------------------------------
/src/components/SkeletonList/SkeletonList.less:
--------------------------------------------------------------------------------
1 | .product-categories-skeleton {
2 | .ant-skeleton-header {
3 | padding-right: 0;
4 | .ant-skeleton-avatar-lg {
5 | height: 315px;
6 | width: 100%;
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/SkeletonList/SkeletonList.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import SkeletonItem from './SkeletonItem';
3 | import MainRowLayout from '../MainRowLayout/MainRowLayout';
4 | import './SkeletonList.less';
5 |
6 | interface SkeletonListProps {
7 | itemCount: number;
8 | }
9 |
10 | const SkeletonList: React.FC = ({ itemCount }) => {
11 | return (
12 |
13 | {Array.from(Array(itemCount)).map((_, i) => (
14 |
15 | ))}
16 |
17 | );
18 | };
19 |
20 | export default SkeletonList;
21 |
--------------------------------------------------------------------------------
/src/components/Spinner/Spinner.less:
--------------------------------------------------------------------------------
1 | .spinner {
2 | display: flex;
3 | flex-flow: wrap row;
4 | justify-content: center;
5 | align-items: center;
6 | }
7 |
--------------------------------------------------------------------------------
/src/components/Spinner/Spinner.tsx:
--------------------------------------------------------------------------------
1 | import { Spin } from 'antd';
2 | import { LoadingOutlined } from '@ant-design/icons';
3 | import './Spinner.less';
4 |
5 | const Spinner = () => {
6 | return (
7 |
8 | } />
9 |
10 | );
11 | };
12 |
13 | export default Spinner;
14 |
--------------------------------------------------------------------------------
/src/config/index.ts:
--------------------------------------------------------------------------------
1 | //@ts-nocheck
2 | import WooCommerceRestApi from '@woocommerce/woocommerce-rest-api';
3 |
4 | export const wooApi = new WooCommerceRestApi({
5 | url: 'https://react-ecommerce.lougiequisel.com',
6 | consumerKey: 'ck_64aa28b5e566c317b5a9a809fa6f07b19e646abc',
7 | consumerSecret: 'cs_a56e63dc12ab253643dc05a7c6b934024aee55cf',
8 | version: 'wc/v3',
9 | queryStringAuth: true
10 | });
11 |
--------------------------------------------------------------------------------
/src/contexts/index.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const CartContext = React.createContext({
4 | totalPrice: 0
5 | });
6 |
7 | export interface Breakpoints {
8 | xl: number;
9 | sm: number;
10 | md: number;
11 | lg: number;
12 | xs: number;
13 | }
14 |
15 | const breakpoints: Breakpoints = {
16 | xl: 0,
17 | sm: 0,
18 | md: 0,
19 | lg: 0,
20 | xs: 0
21 | };
22 |
23 | export const SkeletonListContext = React.createContext(breakpoints);
24 |
25 | const multipleBreakpoints: Breakpoints[] = [
26 | {
27 | xl: 0,
28 | sm: 0,
29 | md: 0,
30 | lg: 0,
31 | xs: 0
32 | },
33 | {
34 | xl: 0,
35 | sm: 0,
36 | md: 0,
37 | lg: 0,
38 | xs: 0
39 | }
40 | ];
41 | export const SingleProductContext = React.createContext(multipleBreakpoints);
42 |
--------------------------------------------------------------------------------
/src/helpers/cart_helper.ts:
--------------------------------------------------------------------------------
1 | import Cookies from 'js-cookie';
2 | import { Product, Cart } from '../actions';
3 |
4 | //Cookies.remove(CART_ITEMS);
5 |
6 | export const CART_ITEMS = 'CART_ITEMS';
7 | export const CART_ITEMS_DELIMETER = ',';
8 |
9 | export const calculateTotalPrice = (cartItems: Cart[]): number => {
10 | let totalPrice = 0;
11 | cartItems.map(item => {
12 | totalPrice += Number(item.price) * item.count;
13 | });
14 | return totalPrice;
15 | };
16 |
17 | export const getCartIds = (cartItems: Cart[]): string => {
18 | let cartItemIds = cartItems.map(item => item.id);
19 | return cartItemIds.join(CART_ITEMS_DELIMETER);
20 | };
21 |
22 | export const getCartItemCount = (
23 | cartItems: Cart[],
24 | currentProductId: string
25 | ): number => {
26 | return cartItems.find(item => item.id === currentProductId)?.count || 0;
27 | };
28 |
29 | export const stringifyArr = (cartItems: Cart[]): string => {
30 | return cartItems
31 | .map((item: Cart) => {
32 | return `${item.id}-${item.price}-${item.count}`;
33 | })
34 | .join(CART_ITEMS_DELIMETER);
35 | };
36 |
37 | export const saveToCartCookie = (cartItems: Cart[]) => {
38 | Cookies.set(CART_ITEMS, stringifyArr(cartItems), { expires: 7 });
39 | };
40 |
41 | export const getCartCookie = (): string => {
42 | return Cookies.get(CART_ITEMS) || '';
43 | };
44 |
45 | export const getCartCookieArr = (): Cart[] => {
46 | const cartCookieStr = getCartCookie();
47 | const cartItems = cartCookieStr
48 | ? cartCookieStr.split(CART_ITEMS_DELIMETER)
49 | : [];
50 | let items: Cart[] = [];
51 | if (cartItems.length > 0) {
52 | cartItems.forEach((cartItem, i) => {
53 | const cartItemPieces = cartItem.split('-');
54 | items = [
55 | ...items,
56 | {
57 | id: cartItemPieces[0],
58 | price: cartItemPieces[1],
59 | count: Number(cartItemPieces[2])
60 | }
61 | ];
62 | });
63 | }
64 |
65 | return items;
66 | };
67 |
68 | export const getTotalItemsCookie = (): number => {
69 | const items = getCartCookie();
70 | return items ? items.split(CART_ITEMS_DELIMETER).length : 0;
71 | };
72 |
73 | export const getItemIndex = (cartItems: Cart[], id: string) => {
74 | return cartItems.map(item => item.id).indexOf(id);
75 | };
76 |
77 | export const removeItem = (cartItems: Cart[], id: string) => {
78 | const index = getItemIndex(cartItems, id);
79 | if (index > -1) {
80 | cartItems.splice(index, 1);
81 | }
82 | return cartItems;
83 | };
84 |
85 | export const updateCartItemCount = (
86 | cartItems: Cart[],
87 | index: number,
88 | count: number
89 | ) => {
90 | return cartItems.map((item, i) => {
91 | return i === index ? { ...item, count: item.count + count } : item;
92 | });
93 | };
94 |
--------------------------------------------------------------------------------
/src/helpers/index.ts:
--------------------------------------------------------------------------------
1 | export * from './cart_helper';
2 |
--------------------------------------------------------------------------------
/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { AppProps } from 'next/app';
3 | import { Provider } from 'react-redux';
4 | import { createStore, applyMiddleware } from 'redux';
5 | import thunk from 'redux-thunk';
6 | import { rootReducer } from '../reducers';
7 | import ProgressLine from '../components/ProgressLine';
8 | import './app.less';
9 |
10 | const store = createStore(rootReducer, applyMiddleware(thunk));
11 |
12 | const App = ({ Component, pageProps }: AppProps) => {
13 | return (
14 |
15 |
16 |
17 |
18 | );
19 | };
20 |
21 | export default App;
22 |
--------------------------------------------------------------------------------
/src/pages/app.less:
--------------------------------------------------------------------------------
1 | .boxed-width {
2 | max-width: @full-box-width;
3 | margin: 0 auto;
4 | @media (max-width: 1200px) {
5 | margin: 0 25px;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/pages/cart.less:
--------------------------------------------------------------------------------
1 | .cart-item {
2 | display: flex;
3 | flex-flow: row wrap;
4 | margin-bottom: 25px;
5 | padding: 10px;
6 |
7 | &.deleting {
8 | opacity: 0.5;
9 | }
10 |
11 | .featured-pp {
12 | width: 80px;
13 | height: 80px;
14 | @media (max-width: 767px) {
15 | margin-bottom: 20px;
16 | }
17 | @media (max-width: 520px) {
18 | width: 30%;
19 | }
20 | img {
21 | width: 100%;
22 | height: 100%;
23 | object-fit: cover;
24 | }
25 | }
26 | .description {
27 | width: 315px;
28 | @media (max-width: 767px) {
29 | width: 80%;
30 | }
31 | @media (max-width: 520px) {
32 | width: 70%;
33 | }
34 | padding-left: 15px;
35 |
36 | div {
37 | span:last-child {
38 | color: @primary-color;
39 | }
40 | }
41 | }
42 | .quantity-control {
43 | display: flex;
44 | flex-flow: column wrap;
45 | justify-content: center;
46 | input {
47 | text-align: center;
48 | }
49 | @media (max-width: 320px) {
50 | width: 50%;
51 | }
52 | }
53 | .quantity {
54 | .quantity-control();
55 | width: 20%;
56 | }
57 | .delete {
58 | .quantity-control();
59 | width: 150px;
60 | span {
61 | font-size: 20px;
62 | opacity: 0.6;
63 | cursor: pointer;
64 | &:hover {
65 | opacity: 1;
66 | }
67 | }
68 | @media (max-width: 320px) {
69 | width: 50%;
70 | }
71 | }
72 | .subtotal {
73 | .delete();
74 | width: 20%;
75 | font-size: 20px;
76 | text-align: center;
77 | margin-left: auto;
78 | }
79 | }
80 |
81 | .cart-wrapper {
82 | .ant-col {
83 | padding: 20px;
84 | .product-categories-skeleton {
85 | flex-flow: column wrap;
86 | .ant-col {
87 | width: 100%;
88 | max-width: 100%;
89 | span.ant-skeleton-avatar-square {
90 | height: 102px;
91 | }
92 | }
93 | }
94 | .cart-list {
95 | .cart-item {
96 | border: 1px solid #f0f0f0;
97 | }
98 | }
99 |
100 | .order-summary {
101 | border: 1px solid #f0f0f0;
102 | padding: 10px;
103 |
104 | div {
105 | display: flex;
106 | flex-flow: row wrap;
107 | justify-content: space-between;
108 | span {
109 | font-size: 18px;
110 | &:last-child {
111 | color: @primary-color;
112 | font-size: 20px;
113 | }
114 | }
115 | }
116 | button {
117 | width: 100%;
118 | margin-top: 25px;
119 | }
120 | }
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/pages/cart.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { useDispatch } from 'react-redux';
3 | import { Row, Col } from 'antd';
4 | import MainLayout from '../components/MainLayout/MainLayout';
5 | import CartListRenderer from '../components/Cart/CartListRenderer';
6 | import { useCartSelector, useProductSelector } from '../selectors';
7 | import { fetchProductsByIds } from '../actions';
8 | import OrderSummary from '../components/Cart/OrderSummary';
9 | import { calculateTotalPrice, getCartIds } from '../helpers';
10 | import { CartContext, SkeletonListContext, Breakpoints } from '../contexts';
11 | import './cart.less';
12 |
13 | const Cart = () => {
14 | const { items, totalItems } = useCartSelector();
15 | const { cartProducts } = useProductSelector();
16 | const itemsLength = items.length;
17 |
18 | const [totalPrice, setTotalPrice] = useState(0);
19 |
20 | const dispatch = useDispatch();
21 |
22 | useEffect(() => {
23 | if (itemsLength > 0) {
24 | const cartItemIds = getCartIds(items);
25 | dispatch(fetchProductsByIds(cartItemIds));
26 | }
27 | }, [itemsLength]);
28 |
29 | useEffect(() => {
30 | setTotalPrice(calculateTotalPrice(items));
31 | });
32 |
33 | return (
34 |
39 |
42 |
43 |
44 |
45 |
49 |
50 |
51 |
55 |
56 |
57 |
58 |
59 |
60 | );
61 | };
62 |
63 | export default Cart;
64 |
--------------------------------------------------------------------------------
/src/pages/category/[...category].tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { useRouter } from 'next/router';
3 | import { useDispatch } from 'react-redux';
4 | import MainLayout from '../../components/MainLayout/MainLayout';
5 | import { fetchCategoryProducts, fetchCategory } from '../../actions';
6 | import { useProductSelector, useCategorySelector } from '../../selectors';
7 | import ProductListRenderer from '../../components/ProductList/ProductListRenderer';
8 | import MainPageHeader from '../../components/MainPageHeader/MainPageHeader';
9 |
10 | const Category = () => {
11 | const [isLoading, setLoading] = useState(false);
12 |
13 | const router = useRouter();
14 | const { category: categoryParam } = router.query;
15 | const category_id = categoryParam ? categoryParam[0] : null;
16 | const currentCategoryName = categoryParam ? categoryParam[1] : '...';
17 |
18 | const dispatch = useDispatch();
19 | const { categoryProducts } = useProductSelector();
20 | const { category } = useCategorySelector();
21 | const currentCategoryId = `${category?.id ?? ''}`;
22 | const curretCategoryDesc = category?.description ?? '...';
23 |
24 | useEffect(() => {
25 | if (category_id && category_id !== currentCategoryId) {
26 | setLoading(true);
27 | dispatch(
28 | fetchCategoryProducts(category_id, () => {
29 | setLoading(false);
30 | })
31 | );
32 | dispatch(fetchCategory(category_id));
33 | }
34 | }, [category_id]);
35 |
36 | return (
37 |
38 |
42 |
47 |
48 | );
49 | };
50 |
51 | export default Category;
52 |
--------------------------------------------------------------------------------
/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { useDispatch } from 'react-redux';
3 | import MainLayout from '../components/MainLayout/MainLayout';
4 | import MainCarousel from '../components/MainCarousel/MainCarousel';
5 | import CategoryListRenderer from '../components/CategoryList/CategoryListRenderer';
6 | import ProductListRenderer from '../components/ProductList/ProductListRenderer';
7 | import SimpleHeading from '../components/SimpleHeading';
8 | import { fetchMainProductCategories, fetchSaleProducts } from '../actions';
9 | import { useCategorySelector, useProductSelector } from '../selectors';
10 |
11 | const Home = () => {
12 | const dispatch = useDispatch();
13 | const { saleProducts } = useProductSelector();
14 | const { mainCategories } = useCategorySelector();
15 |
16 | useEffect(() => {
17 | dispatch(fetchMainProductCategories());
18 | dispatch(fetchSaleProducts());
19 | }, []);
20 |
21 | return (
22 |
23 |
24 |
25 |
26 |
36 |
37 |
38 |
44 |
45 | );
46 | };
47 |
48 | export default Home;
49 |
--------------------------------------------------------------------------------
/src/pages/product/[...product].tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { useRouter } from 'next/router';
3 | import { useDispatch } from 'react-redux';
4 | import MainLayout from '../../components/MainLayout/MainLayout';
5 | import SingleProductRenderer from '../../components/SingleProduct/SingleProductRenderer';
6 | import { useProductSelector } from '../../selectors';
7 | import { fetchProductById } from '../../actions';
8 |
9 | const Product = () => {
10 | const [isLoading, setLoading] = useState(false);
11 |
12 | const router = useRouter();
13 | const { product: productParam } = router.query;
14 | const productId = productParam ? productParam[0] : null;
15 |
16 | const { currentProduct } = useProductSelector();
17 | const currentProductId = `${currentProduct?.id ?? ''}`;
18 | const currentProductName = currentProduct?.name ?? '...';
19 |
20 | const dispatch = useDispatch();
21 |
22 | useEffect(() => {
23 | if (productId && productId !== currentProductId) {
24 | setLoading(true);
25 | dispatch(
26 | fetchProductById(productId, () => {
27 | setLoading(false);
28 | })
29 | );
30 | }
31 | }, [productId]);
32 |
33 | return (
34 |
35 |
43 |
44 | );
45 | };
46 |
47 | export default Product;
48 |
--------------------------------------------------------------------------------
/src/reducers/cart_reducer.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Cart,
3 | RemoveFromCart,
4 | AddToCart,
5 | CartTypes,
6 | UpdateCartItemCount
7 | } from '../actions';
8 | import {
9 | getTotalItemsCookie,
10 | getCartCookieArr,
11 | getItemIndex,
12 | updateCartItemCount,
13 | saveToCartCookie,
14 | removeItem
15 | } from '../helpers';
16 |
17 | type Actions = AddToCart | RemoveFromCart | UpdateCartItemCount;
18 |
19 | export interface CartState {
20 | totalItems: number;
21 | items: Cart[];
22 | }
23 |
24 | export const initialState: CartState = {
25 | totalItems: getTotalItemsCookie(),
26 | items: getCartCookieArr()
27 | };
28 |
29 | export default function(state = initialState, action: Actions) {
30 | switch (action.type) {
31 | case CartTypes.addToCart:
32 | case CartTypes.updateCartItemCount:
33 | const cartItem = action.payload;
34 | const index = getItemIndex(state.items, cartItem.id);
35 | const currentItems = state.items;
36 |
37 | let cartItems: Cart[] = [];
38 |
39 | // if item already exists increase count
40 | if (index > -1) {
41 | cartItems = updateCartItemCount(
42 | [...currentItems],
43 | index,
44 | cartItem.count
45 | );
46 | } else {
47 | cartItems = [...currentItems, action.payload];
48 | }
49 |
50 | saveToCartCookie(cartItems);
51 |
52 | return { ...state, items: cartItems, totalItems: cartItems.length };
53 |
54 | case CartTypes.removeFromCart:
55 | const item = action.payload;
56 | const items = [...state.items];
57 | const filteredItems = removeItem(items, item);
58 | saveToCartCookie(filteredItems);
59 |
60 | return {
61 | ...state,
62 | items: filteredItems,
63 | totalItems: filteredItems.length
64 | };
65 |
66 | default:
67 | return state;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/reducers/category_reducer.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CategoryTypes,
3 | ProductCategory,
4 | FetchMainProductCategories,
5 | FetchCategory
6 | } from '../actions';
7 |
8 | type Actions = FetchMainProductCategories | FetchCategory;
9 |
10 | export interface CategoryState {
11 | mainCategories: ProductCategory[];
12 | category?: ProductCategory;
13 | }
14 |
15 | export const initialState: CategoryState = {
16 | mainCategories: [],
17 | category: undefined
18 | };
19 |
20 | export default function(state = initialState, action: Actions) {
21 | switch (action.type) {
22 | case CategoryTypes.fetchMainProductCategories:
23 | return { ...state, mainCategories: action.payload };
24 | case CategoryTypes.fetchCategory:
25 | return { ...state, category: action.payload };
26 | default:
27 | return state;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/reducers/index.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import ProductReducer from './product_reducer';
3 | import CategoryReducer from './category_reducer';
4 | import CartReducer from './cart_reducer';
5 |
6 | export const rootReducer = combineReducers({
7 | product: ProductReducer,
8 | category: CategoryReducer,
9 | cart: CartReducer
10 | });
11 |
12 | export type AppState = ReturnType;
13 |
--------------------------------------------------------------------------------
/src/reducers/product_reducer.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ProductTypes,
3 | Product,
4 | FetchSaleProducts,
5 | FetchCategoryProducts,
6 | FetchProductById,
7 | FetchProductsByIds
8 | } from '../actions';
9 |
10 | type Actions =
11 | | FetchSaleProducts
12 | | FetchCategoryProducts
13 | | FetchProductById
14 | | FetchProductsByIds;
15 |
16 | export interface ProductState {
17 | saleProducts: Product[];
18 | categoryProducts: Product[];
19 | currentProduct?: Product;
20 | cartProducts: Product[];
21 | }
22 |
23 | export const initialState: ProductState = {
24 | saleProducts: [],
25 | categoryProducts: [],
26 | cartProducts: [],
27 | currentProduct: undefined
28 | };
29 |
30 | export default function(state = initialState, action: Actions) {
31 | switch (action.type) {
32 | case ProductTypes.fetchSaleProduts:
33 | return {
34 | ...state,
35 | saleProducts: action.payload
36 | };
37 | case ProductTypes.fetchCategoryProducts:
38 | return { ...state, categoryProducts: action.payload };
39 | case ProductTypes.fetchProductById:
40 | return { ...state, currentProduct: action.payload };
41 | case ProductTypes.fetchProductsByIds:
42 | return { ...state, cartProducts: action.payload };
43 | default:
44 | return state;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/selectors/index.ts:
--------------------------------------------------------------------------------
1 | import { useSelector } from 'react-redux';
2 | import { AppState } from '../reducers';
3 |
4 | export const useProductSelector = () =>
5 | useSelector((state: AppState) => state.product);
6 | export const useCategorySelector = () =>
7 | useSelector((state: AppState) => state.category);
8 | export const useCartSelector = () =>
9 | useSelector((state: AppState) => state.cart);
10 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve"
16 | },
17 | "exclude": ["node_modules"],
18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "next.config.js"]
19 | }
20 |
--------------------------------------------------------------------------------