Cài đặt Project:
npm create vite@latest
Đặt tên project Name -> Chọn React -> Chọn Typescript
cd Project_name
npm i
npm run dev
Cài đặt MUI + axios + react-router-dom
npm i @mui/material @emotion/react @emotion/styled @mui/icons-material
npm i axios react-router-dom
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
}
}
]
}
types/Product.ts
export type Product = {
id: string;
title: string;
price: number;
image: string;
description: string;
category: string;
rating: {
rate: number;
count: number;
};
};
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>
);
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 <main>{routes}</main>;
}
export default App;
components/ProductCard.tsx
Truyền type Props trong Function Component
https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/function_components
import { FC } from "react";
import {
Button,
Card,
CardActions,
CardContent,
CardMedia,
Typography,
} from "@mui/material";
import { Product } from "../types/Product";
type ProductCardProps = {
product: Product;
};
const ProductCard: FC<ProductCardProps> = ({ product }) => {
return (
<Card sx={{ maxWidth: 345 }}>
<CardMedia
component="img"
alt="green iguana"
height="140"
image={product.image}
sx={{ objectFit: "contain" }}
/>
<CardContent>
<Typography gutterBottom variant="h5" component="div">
{product.title}
</Typography>
<Typography variant="body2" color="text.secondary">
Lizards are a widespread group of squamate reptiles, with over 6,000
species, ranging across all continents except Antarctica
</Typography>
</CardContent>
<CardActions>
<Button size="small">Share</Button>
<Button size="small">Learn More</Button>
</CardActions>
</Card>
);
};
export default ProductCard;
components/Loading.tsx
import { FC } from "react";
import { Box, LinearProgress } from "@mui/material";
type LoadingProps = {
isShow: boolean;
};
const Loading: FC<LoadingProps> = ({ isShow }) => {
return (
<>
{isShow && (
<Box sx={{ width: "100%" }}>
<LinearProgress />
</Box>
)}
</>
);
};
export default Loading;
Config import from src/*
tsconfig.app.json
"compilerOptions": {
...
"baseUrl": ".",
"paths": {
"src/*": ["./src/*"]
}
}
vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
src: "/src",
},
},
});
Tạo folder pages: pages/ProductDetail.tsx
import axios from "axios";
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { Product } from "src/types/Product";
import { Button, Container, Stack, Typography } from "@mui/material";
import Loading from "src/components/Loading";
function ProductDetail() {
const { id } = useParams();
const [loading, setLoading] = useState<boolean>(false);
const [product, setProduct] = useState<Product | undefined>();
const getProduct = async (id: string) => {
try {
setLoading(true);
const { data } = await axios.get(`http://localhost:3000/products/${id}`);
setProduct(data);
} catch (error) {
console.log(error);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (!id) return;
getProduct(id);
}, [id]);
return (
<>
<Loading isShow={loading} />
<Container>
{product && (
<Stack direction={"row"} gap={3}>
<img src={product.image} alt="" width={"500px"} />
<Stack gap={3}>
<Typography component="h1" fontSize={"26px"}>
{product.title}
</Typography>
<Typography fontWeight={"bold"} color={"Highlight"}>
${product.price}
</Typography>
<Typography fontSize={"20px"}>
Rate: {product.rating.count}
</Typography>
<Typography>{product.description}</Typography>
<Button variant="outlined">Add to cart</Button>
</Stack>
</Stack>
)}
</Container>
</>
);
}
export default ProductDetail;
Tạo file pages/Homepage.tsx
import { Stack } from "@mui/material";
import axios from "axios";
import { useEffect, useState } from "react";
import Loading from "src/components/Loading";
import ProductCard from "src/components/ProductCard";
import { Product } from "src/types/Product";
function Homepage() {
const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const getAllProduct = async () => {
try {
setLoading(true);
const { data } = await axios.get("http://localhost:3000/products");
setProducts(data);
} catch (error) {
console.log(error);
} finally {
setLoading(false);
}
};
useEffect(() => {
getAllProduct();
}, []);
return (
<>
<Loading isShow={loading} />
<Stack
direction={"row"}
flexWrap={"wrap"}
gap={2}
alignItems={"center"}
justifyContent={"center"}
>
{products.map((product, index) => (
<ProductCard key={index} product={product} />
))}
</Stack>
</>
);
}
export default Homepage;