Skip to content

karluiz/training-ii-node-react

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 

Repository files navigation

Second Activity: Create Your Shopping Cart with NextJS

  • React with NextJS

Objectives

  • Create NextJS project using GitHub Copilot from a Scratch.
  • Create a Product List Page and Shopping Cart Page.

Shopping Cart

Backend Development

Requirements

  • VS Code version
  • Docker for Desktop
  • Node Installed (nvm optional)
  • GitHub CLI + GitHub Copilot Extension Enabled
  • Insomnia or Postman or any Rest Client Installed.
  • GitHub CLI

Step 1: Create a NextJS Project

@workspace /new create a nextjs 14 and react 18 application with page router with tailwind useHookform and rsuite autprefixer

  • Select this folder as your workspace to create the project.
  • Install yarn if you haven't installed it yet npm install -g yarn
  • Install deps using yarn yarn install
  • Check if the project is running yarn dev

Troubleshooting

  • The package.json could be different because we are working with gen-ai, if you have problems use this.
{
  "name": "my-nextjs-app",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  },
  "dependencies": {
    "next": "^14.0.0",
    "react": "^18.0.0",
    "react-dom": "^18.0.0",
    "rsuite": "^4.9.0",
    "tailwindcss": "^3.0.0"
  },
  "devDependencies": {
    "@types/node": "22.0.1",
    "@types/react": "^17.0.0",
    "@types/react-dom": "^17.0.0",
    "autoprefixer": "^10.4.0",
    "eslint": "^8.5.0",
    "eslint-config-next": "^12.0.0",
    "postcss": "^8.4.5",
    "postcss-preset-env": "^7.3.1",
    "typescript": "^4.5.4"
  },
  "eslintConfig": {
    "extends": [
      "next",
      "next/core-web-vitals"
    ]
  },
  "browserslist": [
    "defaults"
  ]
}

Step 2: Create a Layout Component

@workspace create a layout with navbar and footer using rsuite

  • Install rsuite yarn add rsuite
  • Create a layout component in the components folder
// src/components/Layout.tsx
import React from 'react';
import { Container, Header, Content, Footer, Navbar, Nav } from 'rsuite';

const Layout: React.FC = ({ children }) => {
  return (
    <Container>
      <Header>
        <Navbar>
          <Navbar.Brand href="#">Brand</Navbar.Brand>
          <Nav>
            <Nav.Item href="/">Home</Nav.Item>
            <Nav.Item href="/about">About</Nav.Item>
            <Nav.Item href="/contact">Contact</Nav.Item>
          </Nav>
        </Navbar>
      </Header>
      <Content>
        {children}
      </Content>
      <Footer>
        <p>© 2023 Your Company</p>
      </Footer>
    </Container>
  );
};

export default Layout;
  • Change the _app.tsx file to use the layout component
// src/pages/_app.tsx
import React from 'react';
import { AppProps } from 'next/app';
import Layout from '../components/Layout';
import 'rsuite/dist/styles/rsuite-default.css';
import '../styles/globals.css';

const MyApp = ({ Component, pageProps }: AppProps) => {
  return (
    <Layout>
      <Component {...pageProps} />
    </Layout>
  );
};

export default MyApp;

Troubleshooting

Tailwind problems

Check if Tailwind is working properly by adding a Tailwind class to the layout component.

@workspace why tailwind classes inst working

  • Suggest install tailwindcss using yarn add tailwindcss
  • Check tailwind config tailwind.config.js
// tailwind.config.js
module.exports = {
  content: [
    './src/**/*.{js,ts,jsx,tsx}', // Adjust the paths according to your project structure
  ],
  theme: {
    extend: {},
  },
  plugins: [],
};
  • Import Tailwind CSS in your global styles:
/* src/styles/globals.css */
@import './tailwind.css';
  • Check also _app.tsx if the global styles are imported properly.
// src/pages/_app.tsx
import { AppProps } from "next/app";
import "rsuite/dist/styles/rsuite-default.css";
import "../styles/globals.css"; // This should import Tailwind CSS
import Layout from "../components/Layout";

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <Layout>
      <Component {...pageProps} />
    </Layout>
  );
}

export default MyApp;
  • Check if application is running yarn dev

React Problems

  • Check if React is installed properly by checking the package.json file.

Use explain in GH Copilot to see how to solve problems...

{
  "dependencies": {
    "react": "^18.0.0",
    "react-dom": "^18.0.0",
    "next": "^14.0.0",
    "rsuite": "^5.0.0"
  },
  "devDependencies": {
    "@types/react": "^18.0.0",
    "@types/react-dom": "^18.0.0",
    "typescript": "^4.0.0"
  }
}
  • Is going to suggest remove the node_modules folder and install the dependencies again.
rm -rf node_modules yarn.lock package-lock.json
yarn install
  • Check if the application is running yarn dev

Step 3: Add i18n Support

@workspace i want to add support i18n using next-translate to my project

  • Install next-translate yarn add next-translate
  • Update next.config.js: Configure next-translate in your next.config.js file.
const nextTranslate = require('next-translate');

module.exports = nextTranslate({
  // Any other Next.js configuration options here
});
  • Create i18n configuration file: Create a new file called i18n.js in the root of your project.
// i18n.js
module.exports = {
  locales: ['en', 'es'], // Add your supported languages here
  defaultLocale: 'en',
  pages: {
    '*': ['common'], // Specify namespaces for each page
  },
};
  • Create locales at root level: Create a folder called locales at the root of your project.

  • Create translation files: Inside the locales folder, create a folder for each supported language and add translation files.

// locales/en/common.json
{
  "hello": "Hello, World!",
}
// locales/es/common.json
{
  "hello": "¡Hola, Mundo!",
}
  • Update your index page: Update your index page to use the useTranslation hook from next-translate.
// src/pages/index.tsx
import useTranslation from 'next-translate/useTranslation';

const IndexPage: React.FC = () => {
  const { t } = useTranslation('common');
  return (
    <div>
      <span className="text-xl">Welcome</span>
      <p>{t('welcome')}</p>
    </div>
  );
};

export default IndexPage;

Trobleshooting

  • If i18n is not working properly, downgrade page to 1.6.0

Step 4: Adding .env Support

@workspace i want to add support for .env files in my project

  • Install dotenv yarn add dotenv
  • Create a .env file in the root of your project
# .env
API_MESSAGE="Hello, API from .env!"
  • Load environment variables in next.config.js: Update your next.config.js to load the environment variables using dotenv.
// next.config.js
require('dotenv').config();

module.exports = {

};
  • Test the environment variables: Create a new page to test the environment variables.
// src/pages/hello.ts

import { NextApiRequest, NextApiResponse } from 'next';

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  res.status(200).json({ message: process.env.API_MESSAGE });
}
  • Check if the application is running yarn dev

Step 5: Create mocks with Copilot

create a mock function that return and array with products, every product has and id, name, description, image and price.

type Product = {
  id: number;
  name: string;
  description: string;
  image: string;
  price: number;
};

function getMockProducts(): Product[] {
  return [
    {
      id: 1,
      name: "Product 1",
      description: "Description for product 1",
      image: "https://via.placeholder.com/150",
      price: 19.99,
    },
    {
      id: 2,
      name: "Product 2",
      description: "Description for product 2",
      image: "https://via.placeholder.com/150",
      price: 29.99,
    },
    {
      id: 3,
      name: "Product 3",
      description: "Description for product 3",
      image: "https://via.placeholder.com/150",
      price: 39.99,
    },
    {
      id: 4,
      name: "Product 4",
      description: "Description for product 4",
      image: "https://via.placeholder.com/150",
      price: 49.99,
    },
    {
      id: 5,
      name: "Product 5",
      description: "Description for product 5",
      image: "https://via.placeholder.com/150",
      price: 59.99,
    },
  ];
}

Step 6: Create a Product Card Component

create a productcard component based on #file:index.tsx

import React from 'react';
import { Product } from './api/hello';

type ProductCardProps = {
  product: Product;
};

const ProductCard: React.FC<ProductCardProps> = ({ product }) => {
  return (
    <div className="flex items-center space-x-4 border p-4 rounded-xl">
      <img
        src={product.image}
        alt={product.name}
        className="w-24 h-24"
      />
      <div>
        <h2>{product.name}</h2>
        <p>{product.description}</p>
        <p>{product.price}</p>
      </div>
    </div>
  );
};

export default ProductCard;

Step 7: Add Icons Support

@workspace i want to add support for icons in my project using react-icons

  • Install react-icons yarn add react-icons
  • Use the icons in your components
import React from 'react';
import { Product } from './api/hello';
import { FaShoppingCart } from 'react-icons/fa'; // Import an icon from react-icons

type ProductCardProps = {
  product: Product;
};

const ProductCard: React.FC<ProductCardProps> = ({ product }) => {
  return (
    <div className="flex items-center space-x-4 border p-4 rounded-xl">
      <img
        src={product.image}
        alt={product.name}
        className="w-24 h-24"
      />
      <div>
        <h2>{product.name}</h2>
        <p>{product.description}</p>
        <p>{product.price}</p>
        <button className="flex items-center space-x-2 mt-2">
          <FaShoppingCart /> {/* Use the imported icon */}
          <span>Add to Cart</span>
        </button>
      </div>
    </div>
  );
};

export default ProductCard;

Troubleshooting

how to manage broken images with nextjs

  • Check if the image path is correct
  • Use the next/image component to handle broken images
import React, { useState } from "react";
import { FaShoppingCart } from "react-icons/fa";
import { Button } from "rsuite";
import { Product } from "../pages/api/hello";
import Image from "next/image";

type ProductCardProps = {
  product: Product;
};

const ProductCard: React.FC<ProductCardProps> = ({ product }) => {
  const [imgSrc, setImgSrc] = useState(product.image);

  const handleError = () => {
    setImgSrc("/fallback-image.png"); // Path to your fallback image
  };

  return (
    <div className="flex items-center space-x-4 border p-4 rounded-xl">
      <Image
        src={imgSrc}
        alt={product.name}
        width={96}
        height={96}
        className="w-24 h-24"
        onError={handleError}
      />
      <div>
        <h2>{product.name}</h2>
        <p>{product.description}</p>
        <p>{product.price}</p>
        <Button appearance="ghost" className="flex items-center space-x-2 mt-2">
          <FaShoppingCart />
          <span>Add to Cart</span>
        </Button>
      </div>
    </div>
  );
};

export default ProductCard;

Hostname errors:

hostname "via.placeholder.com" is not configured under images in your next.config.js file

  • Add the hostname to the images property in your next.config.js file.
const nextTranslate = require("next-translate");
require("dotenv").config();

module.exports = nextTranslate({
  images: {
    domains: ["via.placeholder.com"], // Add the external image domain here
  },
  // Any other Next.js configuration options here
});

Make image blur

how to make blur image meanwhile load resource with next/image

  • Use the placeholder attribute in the next/image component to show a blurred image while the main image is loading.
  • Use this base64 image as the value for the blurDataURL attribute.
blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAACCAYAAAB/qH1jAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAJ0lEQVR4nGPY2fXjv458/H9Bbtf/IDbD/7v//8/Mvfq/J+nEfxAbAF3NFsFiuaE1AAAAAElFTkSuQmCC"
import React, { useState } from "react";
import { FaShoppingCart } from "react-icons/fa";
import { Button } from "rsuite";
import { Product } from "../pages/api/hello";
import Image from "next/image";
import useTranslation from "next-translate/useTranslation";

type ProductCardProps = {
  product: Product;
};

const ProductCard: React.FC<ProductCardProps> = ({ product }) => {
  const { t } = useTranslation("common");
  const [imgSrc, setImgSrc] = useState(product.image);

  const handleError = () => {
    setImgSrc("/images/default-fallback-image.png"); // Path to your fallback image
  };

  return (
    <div className="flex items-center space-x-4 border rounded-xl">
      <div className="w-1/3 h-full">
        <Image
          src={imgSrc}
          alt={product.name}
          width={96}
          height={96}
          className="h-full w-full rounded-t-xl object-cover lg:rounded-l-xl lg:rounded-tr-none"
          onError={handleError}
          placeholder="blur"
          blurDataURL="/images/blur-placeholder.png" // Path to your blur placeholder image
        />
      </div>
      <div className="w-2/3 p-4">
        <h2>{product.name}</h2>
        <p>{product.description}</p>
        <p>{product.price}</p>
        <Button appearance="ghost" className="flex items-center space-x-2 mt-2">
          <FaShoppingCart />
          <span>{t("add_to_cart")}</span>
        </Button>
      </div>
    </div>
  );
};

export default ProductCard;

Step 8: Create Shopping Cart Component + Zustand

add zustand support to create a shopping card context

  • Install zustand yarn add zustand

  • Create a Zustand store: Create a new file named useCartStore.ts in your src/store directory (or any preferred directory) to define the Zustand store for the shopping cart.

  • Update the ProductCard component: Use the Zustand store in the ProductCard component to add products to the cart.

  • Create src/store/useCartStore.ts

import create from 'zustand';

type Product = {
  id: string;
  name: string;
  description: string;
  price: string;
  image: string;
};

type CartState = {
  cart: Product[];
  addToCart: (product: Product) => void;
  removeFromCart: (productId: string) => void;
};

export const useCartStore = create<CartState>((set) => ({
  cart: [],
  addToCart: (product) => set((state) => ({ cart: [...state.cart, product] })),
  removeFromCart: (productId) =>
    set((state) => ({
      cart: state.cart.filter((product) => product.id !== productId),
    })),
}));
  • Update the ProductCard component to use the Zustand store:
import React, { useState } from "react";
import { FaShoppingCart } from "react-icons/fa";
import { Button } from "rsuite";
import { Product } from "../pages/api/hello";
import Image from "next/image";
import useTranslation from "next-translate/useTranslation";
import { useCartStore } from "../store/useCartStore";

type ProductCardProps = {
  product: Product;
};

const ProductCard: React.FC<ProductCardProps> = ({ product }) => {
  const { t } = useTranslation("common");
  const [imgSrc, setImgSrc] = useState(product.image);
  const addToCart = useCartStore((state) => state.addToCart);

  const handleError = () => {
    setImgSrc("/images/default-fallback-image.png"); // Path to your fallback image
  };

  const handleAddToCart = () => {
    addToCart(product);
  };

  return (
    <div className="flex items-center space-x-4 border rounded-xl">
      <div className="w-1/3 h-full">
        <Image
          src={imgSrc}
          alt={product.name}
          width={96}
          height={96}
          className="h-full w-full rounded-t-xl object-cover rounded-l-xl rounded-tr-none"
          onError={handleError}
          placeholder="blur"
          blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAACCAYAAAB/qH1jAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAJ0lEQVR4nGPY2fXjv458/H9Bbtf/IDbD/7v//8/Mvfq/J+nEfxAbAF3NFsFiuaE1AAAAAElFTkSuQmCC"
        />
      </div>
      <div className="w-2/3 p-2 relative min-h-40">
        <span className="text-lg">{product.name}</span>
        <p className="line-clamp-2">{product.description}</p>
        <p>{product.price}</p>
        <Button
          appearance="ghost"
          className="flex items-center space-x-2 mt-2 absolute right-2 bottom-1"
          onClick={handleAddToCart}
        >
          <FaShoppingCart />
          <span>{t("add_to_cart")}</span>
        </Button>
      </div>
    </div>
  );
};

export default ProductCard;

Add a counter Component to show the number of items in the cart

how to add counter of products on navbar in #file:layout.tsx

  • Import the Zustand Store
  • Display the Count
// src/components/Layout.tsx
import React from "react";
import { Content, Nav, Navbar, Badge } from "rsuite";
import { useCartStore } from "../store/useCartStore";
import { FaShoppingCart } from "react-icons/fa";

const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const cartCount = useCartStore((state) => state.cart.length);

  return (
    <div className="h-screen flex flex-col">
      <Navbar>
        <Navbar.Brand href="#">Karluiz Shop</Navbar.Brand>
        <Nav>
          <Nav.Item href="/">Home</Nav.Item>
          <Nav.Item href="/about">About</Nav.Item>
        </Nav>
        <Nav pullRight>
          <Nav.Item href="/cart">
            <Badge content={cartCount}>
              <FaShoppingCart />
            </Badge>
          </Nav.Item>
        </Nav>
      </Navbar>
      <Content className="flex-grow p-4">{children}</Content>
    </div>
  );
};

export default Layout;

Step 9: Create a Cart Page on Drawer

i want to create a drawer to see the card content using rsuite

  • Import the necessary components from rsuite.
  • Create a state to manage the visibility of the drawer.
  • Add a Drawer component to the layout.
  • Update the cart icon to open the drawer when clicked.
  • Display the cart content inside the drawer.
// src/components/Layout.tsx
import React, { useState } from "react";
import { FaShoppingCart } from "react-icons/fa";
import { Badge, Content, Nav, Navbar, Drawer, Button } from "rsuite";
import { useCartStore } from "../store/useCartStore";

const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const cartCount = useCartStore((state) => state.cart.length);
  const cartItems = useCartStore((state) => state.cart);
  const [drawerOpen, setDrawerOpen] = useState(false);

  const toggleDrawer = () => {
    setDrawerOpen(!drawerOpen);
  };

  return (
    <div className="h-screen flex flex-col">
      <Navbar>
        <Navbar.Brand href="#">Karluiz Shop</Navbar.Brand>
        <Nav>
          <Nav.Item href="/">Home</Nav.Item>
          <Nav.Item href="/about">About</Nav.Item>
        </Nav>
        <Nav pullRight>
          <Nav.Item onClick={toggleDrawer}>
            <Badge content={cartCount}>
              <FaShoppingCart />
            </Badge>
          </Nav.Item>
        </Nav>
      </Navbar>
      <Content className="flex-grow p-4">{children}</Content>

      <Drawer
        size="sm"
        placement="right"
        show={drawerOpen}
        onHide={toggleDrawer}
      >
        <Drawer.Header>
          <Drawer.Title>Cart</Drawer.Title>
        </Drawer.Header>
        <Drawer.Body>
          {cartItems.length > 0 ? (
            <ul>
              {cartItems.map((item, index) => (
                <li key={index}>{item.name} - {item.quantity}</li>
              ))}
            </ul>
          ) : (
            <p>Your cart is empty.</p>
          )}
        </Drawer.Body>
        <Drawer.Footer>
          <Button onClick={toggleDrawer} appearance="primary">
            Close
          </Button>
        </Drawer.Footer>
      </Drawer>
    </div>
  );
};

export default Layout;

Step 10: Create a Cart Page

Create a card component to show the products added to the cart and also sumarize the total amount of the products added.

// src/components/CartCard.tsx
import React from "react";
import { useCartStore } from "../store/useCartStore";
import { Card, List, Button } from "rsuite";

const CartCard: React.FC = () => {
  const cartItems = useCartStore((state) => state.cart);

  const totalAmount = cartItems.reduce((total, item) => {
    return total + item.price * item.quantity;
  }, 0);

  return (
    <Card bordered style={{ width: 300 }}>
      <h4>Shopping Cart</h4>
      {cartItems.length > 0 ? (
        <List bordered>
          {cartItems.map((item, index) => (
            <List.Item key={index} index={index}>
              <div>
                <strong>{item.name}</strong>
                <p>Quantity: {item.quantity}</p>
                <p>Price: ${item.price.toFixed(2)}</p>
                <p>Subtotal: ${(item.price * item.quantity).toFixed(2)}</p>
              </div>
            </List.Item>
          ))}
        </List>
      ) : (
        <p>Your cart is empty.</p>
      )}
      <div className="mt-4">
        <h5>Total Amount: ${totalAmount.toFixed(2)}</h5>
        <Button appearance="primary">Proceed to Checkout</Button>
      </div>
    </Card>
  );
};

export default CartCard;

Step 11: Manage Quantity of Products and Global State

create method on #file:useCartStore.ts that return cart quantity based on items and totalQuantity

// src/store/useCartStore.ts
import { create } from "zustand";
import { Product as OriginalProduct } from "./pages/api/products";

type Product = OriginalProduct & { quantity: number };

type CartState = {
  cart: Product[];
  addToCart: (product: OriginalProduct) => void;
  removeFromCart: (productId: string) => void;
  getCartQuantity: () => number;
};

export const useCartStore = create<CartState>((set, get) => ({
  cart: [],
  addToCart: (product) =>
    set((state) => {
      const existingProduct = state.cart.find((p) => p.id === product.id);
      if (existingProduct) {
        return {
          cart: state.cart.map((p) =>
            p.id === product.id ? { ...p, quantity: p.quantity + 1 } : p
          ),
        };
      } else {
        return { cart: [...state.cart, { ...product, quantity: 1 }] };
      }
    }),
  removeFromCart: (productId) =>
    set((state) => {
      const existingProduct = state.cart.find((p) => p.id === productId);
      if (existingProduct && existingProduct.quantity > 1) {
        return {
          cart: state.cart.map((p) =>
            p.id === productId ? { ...p, quantity: p.quantity - 1 } : p
          ),
        };
      } else {
        return {
          cart: state.cart.filter((product) => product.id !== productId),
        };
      }
    }),
  getCartQuantity: () => {
    const state = get();
    return state.cart.reduce((total, product) => total + product.quantity, 0);
  },
}));
  • Now replace the login in layout.tsx to use the new method
const cartCount = useCartStore((state) => state.getCartQuantity());

Step 12: Use hookForm to make purchase (optional)

i want to use hookform to make a purchase event in my cart page #file:Cart.tsx and api/purchase.ts

  • First create a purcharse function on /api/purchase.ts
// src/pages/api/purchase.ts
import { NextApiRequest, NextApiResponse } from "next";

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method === "POST") {
    // Process the purchase
    res.status(200).json({ message: "Purchase successful!" });
  } else {
    res.status(405).json({ message: "Method not allowed" });
  }
}
  • Now create a hookForm to make the purchase in layout.tsx

Step 13: Add sound to the cart buttons (optional)

i want to add a sound effect when the user adds a product to the cart using useSound hook

About

Using GitHub Copilot in NextJS App Training

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors