B. Integrasi List book
1. Integrasi list Buku
Pada materi ini kita akan integrasikan api dari backend untuk digunakan pada frontend menggunakan NextJS. Buatlah folder book
di dalam folder app
dan buatlah file page.tsx
seperti gambar di bawah ini.
Membuat Tabel Book
Pada app/book/page.tsx
kita membuat user interface dalam bentuk tabel untuk menampilkan buku yang akan didapatkan melalui api list book
.
app/book/page.tsx
import { Pagination } from "../../components/Pagination";
import { Table, Th, Thead, Tr, Tbody, Td } from "../../components/Table";
const Book = () => {
return (
<>
<section className="container px-4 mx-auto">
<Table>
<Thead>
<Tr>
<Th scope="col">
<div className="flex items-center gap-x-3">
<input
type="checkbox"
className="text-blue-500 border-gray-300 rounded dark:bg-gray-900 dark:ring-offset-gray-900 dark:border-gray-700"
/>
<button className="flex items-center gap-x-2">
<span>Invoice</span>
<svg
className="h-3"
viewBox="0 0 10 11"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2.13347 0.0999756H2.98516L5.01902 4.79058H3.86226L3.45549 3.79907H1.63772L1.24366 4.79058H0.0996094L2.13347 0.0999756ZM2.54025 1.46012L1.96822 2.92196H3.11227L2.54025 1.46012Z"
fill="currentColor"
stroke="currentColor"
strokeWidth="0.1"
/>
<path
d="M0.722656 9.60832L3.09974 6.78633H0.811638V5.87109H4.35819V6.78633L2.01925 9.60832H4.43446V10.5617H0.722656V9.60832Z"
fill="currentColor"
stroke="currentColor"
strokeWidth="0.1"
/>
<path
d="M8.45558 7.25664V7.40664H8.60558H9.66065C9.72481 7.40664 9.74667 7.42274 9.75141 7.42691C9.75148 7.42808 9.75146 7.42993 9.75116 7.43262C9.75001 7.44265 9.74458 7.46304 9.72525 7.49314C9.72522 7.4932 9.72518 7.49326 9.72514 7.49332L7.86959 10.3529L7.86924 10.3534C7.83227 10.4109 7.79863 10.418 7.78568 10.418C7.77272 10.418 7.73908 10.4109 7.70211 10.3534L7.70177 10.3529L5.84621 7.49332C5.84617 7.49325 5.84612 7.49318 5.84608 7.49311C5.82677 7.46302 5.82135 7.44264 5.8202 7.43262C5.81989 7.42993 5.81987 7.42808 5.81994 7.42691C5.82469 7.42274 5.84655 7.40664 5.91071 7.40664H6.96578H7.11578V7.25664V0.633865C7.11578 0.42434 7.29014 0.249976 7.49967 0.249976H8.07169C8.28121 0.249976 8.45558 0.42434 8.45558 0.633865V7.25664Z"
fill="currentColor"
stroke="currentColor"
strokeWidth="0.3"
/>
</svg>
</button>
</div>
</Th>
<Th scope="col">Date</Th>
<Th scope="col">Status</Th>
<Th scope="col">Customer</Th>
<Th scope="col">Purchase</Th>
<Th scope="col">
<span className="sr-only">Actions</span>
</Th>
</Tr>
</Thead>
<Tbody>
<Tr>
<Td>
<span>ok</span>
</Td>
<Td>
<span>ok</span>
</Td>
<Td>
<span>ok</span>
</Td>
<Td>
<span>ok</span>
</Td>
<Td>
<span>ok</span>
</Td>
<Td>
<span>ok</span>
</Td>
</Tr>
</Tbody>
</Table>
</section>
</>
);
};
export default Book;
Endpoint List Book
Lihatlah kembali endpoint list book yang sudah kita buat pada materi backend seperti gambar di bawah ini.
request endpoint list
http://localhost:5002/book/list
http://localhost:5002/book/list?page=1&pageSize=1&title=up&author=ihsan&from_year=2022&to_year=2023
response endpoint list
{
"status": "Success",
"message": "OK",
"data": [
{
"id": 1,
"title": "NestJS For Backend",
"author": "Ihsan",
"year": 2023,
"created_at": "2023-08-29T06:27:27.000Z",
"updated_at": "2023-08-29T06:27:27.000Z"
},
{
"id": 2,
"title": "Server Admin",
"author": "Raihan",
"year": 2022,
"created_at": "2023-08-29T06:27:27.000Z",
"updated_at": "2023-08-29T06:27:27.000Z"
},
{
"id": 3,
"title": "NextJs Developer",
"author": "Ihsan",
"year": 2023,
"created_at": "2023-08-29T06:27:27.000Z",
"updated_at": "2023-08-29T06:27:27.000Z"
},
{
"id": 4,
"title": "HTML CSS",
"author": "Nur",
"year": 2022,
"created_at": "2023-08-29T06:27:27.000Z",
"updated_at": "2023-08-29T06:27:27.000Z"
},
{
"id": 5,
"title": "TypeScript",
"author": "Ihsan",
"year": 2023,
"created_at": "2023-08-29T06:27:27.000Z",
"updated_at": "2023-08-29T06:27:27.000Z"
},
{
"id": 6,
"title": "Become Network Engineer",
"author": "Fathi",
"year": 2021,
"created_at": "2023-08-29T06:27:27.000Z",
"updated_at": "2023-08-29T06:27:27.000Z"
},
{
"id": 7,
"title": "Database MySQL",
"author": "Akbar",
"year": 2023,
"created_at": "2023-08-29T06:27:27.000Z",
"updated_at": "2023-08-29T06:27:27.000Z"
},
{
"id": 8,
"title": "NestJS For Backend",
"author": "Ihsan",
"year": 2023,
"created_at": "2023-08-30T14:16:40.000Z",
"updated_at": "2023-08-30T14:16:40.000Z"
},
{
"id": 9,
"title": "Server Admin",
"author": "Raihan",
"year": 2022,
"created_at": "2023-08-30T14:16:40.000Z",
"updated_at": "2023-08-30T14:16:40.000Z"
},
{
"id": 10,
"title": "TypeScript",
"author": "Ihsan",
"year": 2023,
"created_at": "2023-08-30T14:16:40.000Z",
"updated_at": "2023-08-30T14:16:40.000Z"
}
],
"pagination": {
"total": 39,
"page": 1,
"total_page": 4,
"pageSize": 10
}
}
Membuat Interface Book
Pada file ini kita membuat interface yang akan mendefinikan struktur data yang ada pada fitur book ini.
app/book/interface/index.ts
import { BaseResponsePagination } from "@/lib/axiosClient";
interface Book {
id: number;
title: string;
author: string;
year: number | undefined | string;
created_at: string;
updated_at: string;
}
export interface BookListResponse extends BaseResponsePagination {
data: Book[];
}
Membuat Service Book
book/lib/index.ts
import { useQuery } from "@tanstack/react-query";
import { axiosClient } from "@/lib/axiosClient";
import { BookListResponse } from "../interface";
const useBookModule = () => {
const getBookList = async (): Promise<BookListResponse> => {
return axiosClient.get("/book/list").then((res) => res.data);
};
const useBookList = () => {
const { data, isFetching, isLoading } = useQuery(
["/book/list"],
() => getBookList(),
{
keepPreviousData: true,
select: (response) => response,
}
);
return { data, isFetching, isLoading };
};
return { useBookList };
};
export default useBookModule;
Memanggil Service List Book
book/page.tsx
"use client";
import Button from "@/components/Button";
import { Pagination } from "../../components/Pagination";
import { Table, Th, Thead, Tr, Tbody, Td } from "../../components/Table";
import useBookModule from "./lib";
import { useRouter } from "next/navigation";
const Book = () => {
const { useBookList } = useBookModule();
const { data, isFetching, isError } = useBookList();
return (
<>
<section className="container px-4 mx-auto space-y-5">
<section className="flex items-center justify-between">
<Button colorSchema="red" title="Tambah Buku" />
</section>
<section>
<Table
isFetching={isFetching}
isEmpty={data?.data?.length === 0}
isError={isError}
>
<Thead>
<Tr>
<Th scope="col">
<div className="flex items-center gap-x-3">
<input
type="checkbox"
className="text-blue-500 border-gray-300 rounded dark:bg-gray-900 dark:ring-offset-gray-900 dark:border-gray-700"
/>
</div>
</Th>
<Th scope="col">Title</Th>
<Th scope="col">Author</Th>
<Th scope="col">Year</Th>
<Th scope="col">Created At</Th>
<Th scope="col">Updated At</Th>
</Tr>
</Thead>
<Tbody>
{data &&
data.data.map((item, index) => (
<Tr key={index}>
<Td>
<input
type="checkbox"
className="text-blue-500 border-gray-300 rounded dark:bg-gray-900 dark:ring-offset-gray-900 dark:border-gray-700"
/>
</Td>
<Td>
<span>{item.title}</span>
</Td>
<Td>
<span>{item.author}</span>
</Td>
<Td>
<span>{item.year}</span>
</Td>
<Td>
<span>{item.created_at}</span>
</Td>
<Td>
<span>{item.updated_at}</span>
</Td>
</Tr>
))}
</Tbody>
</Table>
</section>
</section>
</>
);
};
export default Book;
Jalankan pada Bowser
2. Implementasi Filter
book/interface.ts
import {
BaseResponsePagination,
} from "@/lib/axiosClient";
interface Book {
id: number;
title: string;
author: string;
year: number | undefined | string;
created_at: string;
updated_at: string;
}
export interface BookListResponse extends BaseResponsePagination {
data: Book[];
}
export interface BookListFilter extends Partial<Book> {
from_year?: string;
to_year?: string;
page : number ,
pageSize : number
}
book/lib/index.ts
import { useQuery } from "@tanstack/react-query";
import { axiosClient } from "@/lib/axiosClient";
import { BookListFilter, BookListResponse } from "../interface";
import { ChangeEvent, useState } from "react";
const useBookModule = () => {
const defaultParams = {
title: "",
author: "",
from_year: "",
to_year: "",
page: 1,
pageSize: 10,
};
const getBookList = async (
params: BookListFilter
): Promise<BookListResponse> => {
return axiosClient.get("/book/list", { params }).then((res) => res.data);
};
const useBookList = () => {
let [params, setParams] = useState<BookListFilter>(defaultParams);
let [filterParams, setFilterParams] =
useState<BookListFilter>(defaultParams);
const handleFilter = () => {
setFilterParams({ ...params });
};
const handleClear = () => {
setFilterParams(defaultParams);
setParams(defaultParams);
};
const handlePageSize = (e: ChangeEvent<any>) => {
setParams((params) => ({ ...params, pageSize: e.target.value }));
setFilterParams((params) => ({ ...params, pageSize: e.target.value }));
};
const handlePage = (page: number) => {
setParams((params) => ({ ...params, page: page }));
setFilterParams((params) => ({ ...params, page: page }));
};
const { data, isFetching, isLoading, isError } = useQuery(
["/book/list", [filterParams]],
() => getBookList(filterParams),
{
keepPreviousData: true,
select: (response) => response,
}
);
return {
data,
isFetching,
isLoading,
isError,
params,
setParams,
handlePageSize,
handlePage,
handleFilter,
handleClear,
};
};
return { useBookList };
};
export default useBookModule;
book/module/filter.ts
import { ChangeEvent, Dispatch, ReactNode, SetStateAction } from "react";
import { BookListFilter } from "@/app/book/interface";
import InputText from "@/components/InputText";
import Label from "@/components/Label";
import Select from "@/components/Select";
type FilterProps = {
params: BookListFilter;
setParams: Dispatch<SetStateAction<any>>;
};
const option = [
{
value: 2020,
label: "2020",
},
{
value: 2021,
label: "2021",
},
{
value: 2022,
label: "2022",
},
{
value: 2023,
label: "2023",
},
];
const Filter: React.FC<FilterProps> = ({ params, setParams }) => {
const handleChange = (e: ChangeEvent<any>) => {
setParams((params: BookListFilter) => {
return {
...params,
[e.target.name]: e.target.value,
};
});
};
return (
<section className="space-y-2">
<section>
<Label title="Title" htmlFor="title" />
<InputText
onChange={handleChange}
value={params.title}
name="title"
id="title"
/>
</section>
<section>
<Label title="Author" htmlFor="author" />
<InputText
onChange={handleChange}
value={params.author}
name="author"
id="author"
/>
</section>
<section>
<Label title="Title" htmlFor="from_year" />
<Select
onChange={handleChange}
options={option}
value={params.from_year}
name="from_year"
id="from_year"
/>
</section>
<section>
<Label title="Title" htmlFor="to_year" />
<Select
onChange={handleChange}
options={option}
value={params.to_year}
name="to_year"
id="to_year"
/>
</section>
</section>
);
};
export default Filter;
book/page.tsx
"use client";
import Button from "@/components/Button";
import { Pagination } from "../../components/Pagination";
import { Table, Th, Thead, Tr, Tbody, Td } from "../../components/Table";
import useBookModule from "./lib";
import { Drawer } from "@/components/Drawer";
import Filter from "./module/Filter";
import { useDisclosure } from "@/hook";
const Book = () => {
const { useBookList } = useBookModule();
const {
data,
isFetching,
isError,
params,
setParams,
handleFilter,
handleClear,
handlePageSize,
handlePage,
} = useBookList();
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<>
<Drawer
onClose={onClose}
onClear={handleClear}
onSubmit={handleFilter}
title="Filter Buku"
isOpen={isOpen}
>
<Filter params={params} setParams={setParams} />
</Drawer>
<section className="w-screen h-screen p-10 overflow-auto ">
<section className="flex items-center justify-between ">
<Button
width="sm"
onClick={onOpen}
colorSchema="blue"
title="Filter"
/>
<Button width="sm" colorSchema="red" title="tambah" />
</section>
<section className="h-full w-full mt-5 ">
<Table
isFetching={isFetching}
isEmpty={data?.data?.length === 0}
isError={isError}
>
<Thead>
<Tr>
<Th scope="col">
<div className="flex items-center gap-x-3">
<input
type="checkbox"
className="text-blue-500 border-gray-300 rounded dark:bg-gray-900 dark:ring-offset-gray-900 dark:border-gray-700"
/>
</div>
</Th>
<Th scope="col">No</Th>
<Th scope="col">Title</Th>
<Th scope="col">Author</Th>
<Th scope="col">Year</Th>
<Th scope="col">Created At</Th>
<Th scope="col">Updated At</Th>
</Tr>
</Thead>
<Tbody>
{data &&
data.data.map((item, index) => (
<Tr key={index}>
<Td>
<input
type="checkbox"
className="text-blue-500 border-gray-300 rounded dark:bg-gray-900 dark:ring-offset-gray-900 dark:border-gray-700"
/>
</Td>
<Td>{(params.page - 1) * params.pageSize + index + 1}</Td>
<Td>
<span>{item.title}</span>
</Td>
<Td>
<span>{item.author}</span>
</Td>
<Td>
<span>{item.year}</span>
</Td>
<Td>
<span>{item.created_at}</span>
</Td>
<Td>
<span>{item.updated_at}</span>
</Td>
</Tr>
))}
</Tbody>
</Table>
<Pagination
page={params.page}
pageSize={params.pageSize}
handlePageSize={handlePageSize}
handlePage={handlePage}
pagination={data?.pagination}
/>
</section>
</section>
</>
);
};
export default Book;
Merapihkan koding useBookList
hook/usePagination.ts
import { ChangeEvent, useState } from "react";
interface PaginationParams {
page: number;
pageSize: number;
}
export const usePagination = <T extends PaginationParams>(defaultParams: T) => {
let [params, setParams] = useState<T>(defaultParams);
let [filterParams, setFilterParams] = useState<T>(defaultParams);
const handleFilter = () => {
setFilterParams({ ...params });
};
const handleClear = () => {
setFilterParams(defaultParams);
setParams(defaultParams);
};
const handlePageSize = (e: ChangeEvent<any>) => {
setParams((params) => ({ ...params, pageSize: e.target.value }));
setFilterParams((params) => ({ ...params, pageSize: e.target.value }));
};
const handlePage = (page: number) => {
setParams((params) => ({ ...params, page: page }));
setFilterParams((params) => ({ ...params, page: page }));
};
return {
params,
setParams,
handleFilter,
handleClear,
handlePageSize,
handlePage,
filterParams,
};
};
book/lib/index.ts
import { useQuery } from "@tanstack/react-query";
import { axiosClient } from "@/lib/axiosClient";
import { BookListFilter, BookListResponse } from "../interface";
import { usePagination } from "@/hook/usePagination";
const useBookModule = () => {
const defaultParams : BookListFilter = {
title: "",
author: "",
from_year: "",
to_year: "",
page: 1,
pageSize: 10,
};
const getBookList = async (
params: BookListFilter
): Promise<BookListResponse> => {
return axiosClient.get("/book/list", { params }).then((res) => res.data);
};
const useBookList = () => {
const{
params,
setParams,
handleFilter,
handleClear,
handlePageSize,
handlePage,
filterParams
} = usePagination(defaultParams)
const { data, isFetching, isLoading, isError } = useQuery(
["/book/list", [filterParams]],
() => getBookList(filterParams),
{
keepPreviousData: true,
select: (response) => response,
}
);
return {
data,
isFetching,
isLoading,
isError,
params,
setParams,
handlePageSize,
handlePage,
handleFilter,
handleClear,
};
};
return { useBookList };
};
export default useBookModule;