hoc-lap-trinh-18

Github: https://github.com/vanhoa690/web502-typescript

ASM 1: Hoàn thiện giao diện chức năng các page sau

  • Homepage – 1.5 điểm
  • Product Detail – 1.5 điểm
  • Register: username, email, password, confirm password: – 1.5 điểm
  • Login: email, password, validate email, password min 6 ký tự: – 1.5 điểm
  • Giao diện UI đẹp mắt : – 1 điểm
  • Bảo vệ routes admin: – 1 điểm
  • Config axios chèn token vào header khi call API: – 1 điểm
  • Xử lý thông báo Error, Loading khi call API Hoặc API lỗi: – 1 điểm

Lab 3 + Lab 4: Xây dựng Giao diện Trang Register + Login (react-hook-form)

  • Trang Register (/register): có username, email, password, confirmPassword, validate (required, email) + show error – chuyển trang login và show thông báo thành công – 3 điểm
  • Trang Login (/login): lưu token vào localStorage + email + password , validate (required, email) + show error – chuyển trang Homepage và show thông báo thành công – 3 điểm
  • Có show lỗi khi call API Error – 2 điểm
  • Định nghĩa type/interface User đầy đủ theo response API trả về – 1 điểm
  • Giao diện UI đẹp mắt  – 1 điểm

Lab 1 + Lab 2: Xây dựng Giao diện Trang Homepage + Product Detail

  • Trang Hompage (/): có header, footer, danh sách sản phẩm, click vào product sang trang chi tiết sản phẩm – 3 điểm
  • Trang ProductDetail (/product/:id): có header, footer, có thông tin chi tiết sản phẩm (tiêu đề, mô tả, giá cả, đánh giá, category …) – 3 điểm
  • Có show lỗi khi call API Error và khi id product không hợp lệ hoặc không tìm thì hiện thị thông tin Product Not Found (hoặc chuyển trang Not Found) – 2 điểm
  • Định nghĩa type/interface Product đầy đủ theo response API trả về – 1 điểm
  • Giao diện UI đẹp mắt  – 1 điểm

Create Project React + Typescript

npm create vite@latest
Đặt tên project_name
Chọn React
Chọn Typescript
cd project_name
npm i
npm i axios react-router-dom
npm run dev

File package.json: Thêm script server để chạy API: npm run server

 "scripts": {
    "start": "vite --open",
    "server": "npx json-server db.json",
    "dev": "vite",
     ...
}

Tạo file db.json để chạy API

{
  "products": [
    {
      "id": 1,
      "title": "Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops",
      "price": 109.95,
      "description": "Your perfect pack for everyday use and walks in the forest. Stash your laptop (up to 15 inches) in the padded sleeve, your everyday",
      "category": "men's clothing",
      "image": "https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg",
      "rating": {
        "rate": 3.9,
        "count": 120
      }
    },
    {
      "id": 2,
      "title": "Mens Casual Premium Slim Fit T-Shirts ",
      "price": 22.3,
      "description": "Slim-fitting style, contrast raglan long sleeve, three-button henley placket, light weight & soft fabric for breathable and comfortable wearing. And Solid stitched shirts with round neck made for durability and a great fit for casual fashion wear and diehard baseball fans. The Henley style round neckline includes a three-button placket.",
      "category": "men's clothing",
      "image": "https://fakestoreapi.com/img/71-3HjGNDUL._AC_SY879._SX._UX._SY._UY_.jpg",
      "rating": {
        "rate": 4.1,
        "count": 259
      }
    }
  ]
}

Add routeConfig sử dụng useRoutes vào file App.tsx

import { useRoutes } from "react-router-dom";
import Homepage from "./pages/Homepage";
import ProductDetail from "./pages/ProductDetail";

const routeConfig = [
  { path: "/", element: <Homepage /> },
  { path: "/product/:id", element: <ProductDetail /> },
];

function App() {
  const routes = useRoutes(routeConfig);
  return <div>{routes}</div>;
}

export default App;

Thêm BrowserRouter vào file main.tsx

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import { BrowserRouter } from "react-router-dom";

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>
);

Tạo folder pages: tạo file pages/Homepage.tsx

import { useEffect, useState } from "react";
import axios from "axios";

type Product = {
  id: string;
  title: string;
  image: string;
  description: string;
};
function Homepage() {
  const [products, setProducts] = useState<Product[]>([]);
  const getAllProduct = async () => {
    const { data } = await axios.get("http://localhost:3000/products");
    setProducts(data);
  };
  useEffect(() => {
    getAllProduct();
  }, []);
  return (
    <>
      {products.map((product, index) => (
        <div key={index} className="card" style={{ width: "18rem" }}>
          <img
            src={product.image}
            className="card-img-top"
            alt="..."
            width={"100px"}
          />
          <div className="card-body">
            <h5 className="card-title">{product.title}</h5>
            <p className="card-text">{product.description}</p>
            <a href={`/product/${product.id}`} className="btn btn-primary">
              Detail
            </a>
          </div>
        </div>
      ))}
    </>
  );
}

export default Homepage;

Folder types: types/Product.ts

export type Product = {
  id: string;
  title: string;
  image: string;
  description: string;
  price: number;
  category: string;
};

Tạo file pages/ProductDetail.tsx

import { useEffect, useState } from "react";
import axios from "axios";
import { useParams } from "react-router-dom";
import { Product } from "../types/Product";

function ProductDetail() {
  const { id } = useParams();

  const [product, setProduct] = useState<Product | undefined>();

  const getProduct = async (id: string) => {
    const { data } = await axios.get(`http://localhost:3000/products/${id}`);
    setProduct(data);
  };

  useEffect(() => {
    if (!id) return;
    getProduct(id);
  }, [id]);

  return (
    <>
      {product && (
        <div className="container">
          <div className="d-flex">
            <img src={product.image} width={"400px"} />
            <div className="card-body">
              <h5 className="card-title">Title: {product.title}</h5>
              <p className="card-text">Mo ta: {product.description}</p>
              <p className="card-text">Price: {product.price}</p>
              <p className="card-text">Category: {product.category}</p>
            </div>
          </div>
        </div>
      )}
    </>
  );
}
export default ProductDetail;

Cài đặt thư viện json-server, json-server-auth

Thêm users trong db.json

{
  "products": [],
   "users": []
}

Thêm script: server (npm run server)

 "scripts": {
   "start": "vite --open",
   "server": "json-server --watch db.json -m ./node_modules/json-server-auth",
   ...
} 
"dependencies": {
    ...
    "json-server": "^0.17.4",
    "json-server-auth": "^2.1.0",
    ...
}

Tạo file pages/Register.tsx

import axios from "axios";
import { useForm, SubmitHandler } from "react-hook-form";

type Inputs = {
  username: string;
  email: string;
  password: string;
};

function Register() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<Inputs>();

  const onSubmit: SubmitHandler<Inputs> = async (data) => {
    try {
      await axios.post("http://localhost:3000/register", data);
    } catch (error) {}
  };

  return (
    <div className="container mt-4">
      <h1 className="text-center">Register User</h1>
      <form onSubmit={handleSubmit(onSubmit)}>
        <div className="form-group">
          <label htmlFor="username">Username</label>
          <input
            type="text"
            className="form-control"
            id="username"
            aria-describedby="emailHelp"
            placeholder="Enter username"
            {...register("username", {
              required: "Username is required",
            })}
          />
          {errors?.username && (
            <small id="emailHelp" className="text-danger">
              {errors?.username?.message}
            </small>
          )}
        </div>
        <div className="form-group">
          <label htmlFor="exampleInputEmail1">Email address</label>
          <input
            type="text"
            className="form-control"
            id="exampleInputEmail1"
            aria-describedby="emailHelp"
            placeholder="Enter email"
            {...register("email", {
              required: "Email is required",
            })}
          />
          {errors?.email && (
            <small id="emailHelp" className="text-danger">
              {errors?.email?.message}
            </small>
          )}
        </div>
        <div className="form-group">
          <label htmlFor="exampleInputPassword1">Password</label>
          <input
            type="password"
            className="form-control"
            id="exampleInputPassword1"
            placeholder="Password"
            {...register("password", {
              required: "Password is required",
            })}
          />
          {errors?.password && (
            <small id="emailHelp" className="text-danger">
              {errors?.password?.message}
            </small>
          )}
        </div>
        <button type="submit" className="btn btn-primary">
          Submit
        </button>
      </form>
    </div>
  );
}

export default Register;

Tạo file pages/Login.tsx

import axios from "axios";
import { useForm, SubmitHandler } from "react-hook-form";

type Inputs = {
  email: string;
  password: string;
};

function Login() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<Inputs>();

  const onSubmit: SubmitHandler<Inputs> = async (data) => {
    console.log(data);
    const res = await axios.post("http://localhost:3000/login", data);
    localStorage.setItem("token", res.data.accessToken);
  };

  return (
    <div className="container mt-4">
      <h1 className="text-center">Login User</h1>
      <form onSubmit={handleSubmit(onSubmit)}>
        <div className="form-group">
          <label htmlFor="exampleInputEmail1">Email address</label>
          <input
            type="text"
            className="form-control"
            id="exampleInputEmail1"
            {...register("email", {
              required: "Email is required",
              pattern: {
                value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
                message: "invalid email address",
              },
            })}
          />
          {errors?.email && (
            <small id="emailHelp" className="text-danger">
              {errors?.email?.message}
            </small>
          )}
        </div>
        <div className="form-group">
          <label htmlFor="exampleInputPassword1">Password</label>
          <input
            type="password"
            className="form-control"
            id="exampleInputPassword1"
            placeholder="Password"
            {...register("password", {
              required: "Password is required",
              minLength: {
                value: 6,
                message: "Password is min length 6 characters",
              },
            })}
          />
          {errors?.password && (
            <small id="emailHelp" className="text-danger">
              {errors?.password?.message}
            </small>
          )}
        </div>
        <button type="submit" className="btn btn-primary">
          Submit
        </button>
      </form>
    </div>
  );
}

export default Login;

Tạo file layouts/LayoutAdmin.tsx

import { Navigate, Outlet } from "react-router-dom";

function LayoutAdmin() {
  const token = localStorage.getItem("token");
  return (
    <>
      {token ? (
        <>
          <p>Sidebar</p>
          <Outlet />
        </>
      ) : (
        <Navigate to={"/login"} />
      )}
    </>
  );
}

export default LayoutAdmin;

File App.tsx

import { useRoutes } from "react-router-dom";
import Homepage from "./pages/Homepage";
import ProductDetail from "./pages/ProductDetail";
import NotFound from "./pages/NotFound";
import Register from "./pages/Register";
import Login from "./pages/Login";
import Admin from "./pages/admin";
import LayoutAdmin from "./layouts/LayoutAdmin";
import Dashboard from "./pages/admin/Dashboard";
import AdminProductList from "./pages/admin/product/List";
import LayoutClient from "./layouts/LayoutClient";
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";

const routeConfig = [
  {
    path: "",
    element: <LayoutClient />,
    children: [
      { path: "", element: <Homepage /> },
      { path: "/product/:id", element: <ProductDetail /> },
      { path: "/register", element: <Register /> },
      { path: "/login", element: <Login /> },
    ],
  },

  {
    path: "/admin",
    element: <LayoutAdmin />,
    children: [
      {
        path: "",
        element: <Admin />,
      },
      {
        path: "dashboard",
        element: <Dashboard />,
      },
      {
        path: "product/list",
        element: <AdminProductList />,
      },
    ],
  },
  { path: "*", element: <NotFound /> },
];

function App() {
  const routes = useRoutes(routeConfig);
  return (
    <div>
      <ToastContainer />
      {routes}
    </div>
  );
}

export default App;

File pages/admin/product/List.tsx

import { useEffect, useState } from "react";
import { Product } from "../../../types/Product";
import axios from "axios";
import Loading from "../../../components/Loading";
import { toast } from "react-toastify";

type Error = {
  message: string;
};
function AdminProductList() {
  const [products, setProducts] = useState<Product[]>([]);
  const [isLoading, setLoading] = useState<boolean>(false);

  const getAllProduct = async () => {
    try {
      setLoading(true);
      const { data } = await axios.get("http://localhost:3000/products");
      setProducts(data);
    } catch (error) {
      toast.error((error as Error)?.message);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    getAllProduct();
  }, []);

  const handleDeleteProduct = async (id: string) => {
    if (window.confirm("Xoa that ko?")) {
      try {
        setLoading(true);
        await axios.delete(`http://localhost:3000/products/${id}`);
        toast.success("Xoa thanh cong !");
        getAllProduct();
      } catch (error) {
        toast.error((error as Error)?.message);
      } finally {
        setLoading(false);
      }
    }
  };
  return (
    <div className="container">
      <Loading isShow={isLoading} />
      <h1>AdminProductList</h1>
      <table className="table">
        <thead>
          <tr>
            <th scope="col">ID</th>
            <th scope="col">Title</th>
            <th scope="col">Price</th>
            <th scope="col">Image</th>
            <th scope="col">Desc</th>
            <th scope="col">Actions</th>
          </tr>
        </thead>
        <tbody>
          {products.map((product, index) => (
            <tr key={index}>
              <th scope="row">{product.id}</th>
              <th scope="col">{product.title}</th>
              <th scope="col">{product.price}</th>
              <th scope="col">
                <img src={product.image} width={"100px"} />
              </th>
              <th scope="col">{product.description.substring(0, 60)}...</th>
              <th scope="col">
                <button
                  className="btn btn-danger"
                  onClick={() => handleDeleteProduct(product.id)}
                >
                  Delete
                </button>
              </th>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

export default AdminProductList;
import axios from "axios";
import { SubmitHandler, useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify";

type Inputs = {
  title: string;
  price: number;
  image: string;
  desc: string;
  category: string;
  isShowProduct: boolean;
};

function AdminAddProduct() {
  const nav = useNavigate();

  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<Inputs>();

  const handleAddProduct: SubmitHandler<Inputs> = async (data) => {
    try {
      await axios.post("/products", data);
      toast.success("Add Product Success"); // alert
      setTimeout(() => nav("/admin/product/list"), 3000);
    } catch (error) {
      toast.error("Loi Call API"); // alert
    }
  };
  return (
    <div className="container">
      <h1>Add Product</h1>
      <form onSubmit={handleSubmit(handleAddProduct)}>
        {/* Label + Input text*/}
        <div className="form-group">
          <label htmlFor="title">Title</label>
          <input
            type="text"
            className="form-control"
            id="title"
            placeholder="Product Title"
            {...register("title", { required: "Title xxxx is Required" })}
          />
          {errors?.title && (
            <small className="text-danger">{errors.title.message}</small>
          )}
        </div>
        {/* End Input text */}

        <div className="form-group">
          <label htmlFor="price">Price</label>
          <input
            type="number"
            className="form-control"
            id="price"
            placeholder="Product Price"
            {...register("price", {
              required: "Price is Required",
              min: {
                value: 1,
                message: "Price is min 1",
              },
            })}
          />
          {errors?.price && (
            <small className="text-danger">{errors.price.message}</small>
          )}
        </div>

        <div className="form-group">
          <label htmlFor="image">Image</label>
          <input
            type="text"
            className="form-control"
            id="image"
            placeholder="Product Image"
            {...register("image")}
          />
        </div>
        <div className="form-group">
          <label htmlFor="Desc">Desc</label>
          <textarea
            className="form-control"
            id="Desc"
            rows={3}
            defaultValue={""}
            {...register("desc")}
          />
        </div>

        {/* Check box: Show Product */}
        <div className="form-check">
          <input
            type="checkbox"
            className="form-check-input"
            id="isShowProduct"
            {...register("isShowProduct")}
          />
          <label className="form-check-label" htmlFor="isShowProduct">
            Show Product
          </label>
        </div>

        {/* Category: Select box */}
        <div className="form-group">
          <label htmlFor="Category">Category</label>
          <select
            className="form-control"
            id="Category"
            {...register("category")}
          >
            <option value="1">Hp</option>
            <option value="2">Aplle</option>
            <option value="3">Dell</option>
          </select>
        </div>

        <button className="btn btn-primary">Add Product</button>
      </form>
    </div>
  );
}

export default AdminAddProduct;

By hoadv