From e0e1eef6d43de9f8b1e5f2c4e59eacf17c4d9d8e Mon Sep 17 00:00:00 2001 From: "jason.lam" Date: Tue, 29 Apr 2025 12:12:42 +0800 Subject: [PATCH] [feature] update axios --- .env.development | 3 +- .env.production | 3 +- src/app/(main)/axios/AxiosProvider.tsx | 38 +++++++++++ src/app/(main)/axios/axiosInstance.ts | 56 +++++++++++++++ src/app/(main)/layout.tsx | 39 ++++++----- src/components/ItemsSearch/ItemsSearch.tsx | 68 +++++++++++++++---- src/components/LoginPage/LoginForm.tsx | 8 ++- .../SearchResults/SearchResults.tsx | 10 +++ src/config/api.ts | 1 + 9 files changed, 192 insertions(+), 34 deletions(-) create mode 100644 src/app/(main)/axios/AxiosProvider.tsx create mode 100644 src/app/(main)/axios/axiosInstance.ts diff --git a/.env.development b/.env.development index 6d999ff..fb606d6 100644 --- a/.env.development +++ b/.env.development @@ -1,2 +1,3 @@ API_URL=http://localhost:8090/api -NEXTAUTH_SECRET=secret \ No newline at end of file +NEXTAUTH_SECRET=secret +NEXT_PUBLIC_API_URL=http://localhost:8090/api \ No newline at end of file diff --git a/.env.production b/.env.production index b332e4d..9fd4c12 100644 --- a/.env.production +++ b/.env.production @@ -1,3 +1,4 @@ API_URL=http://localhost:8090/api NEXTAUTH_SECRET=secret -NEXTAUTH_URL=https://fpsms-uat.2fi-solutions.com \ No newline at end of file +NEXTAUTH_URL=https://fpsms-uat.2fi-solutions.com +NEXT_PUBLIC_API_URL=http://localhost:8090/api \ No newline at end of file diff --git a/src/app/(main)/axios/AxiosProvider.tsx b/src/app/(main)/axios/AxiosProvider.tsx new file mode 100644 index 0000000..224d40c --- /dev/null +++ b/src/app/(main)/axios/AxiosProvider.tsx @@ -0,0 +1,38 @@ +"use client"; +import React, {createContext, useContext, useEffect, useState} from 'react'; +import axiosInstance, {SetupAxiosInterceptors} from './axiosInstance'; + +const AxiosContext = createContext(axiosInstance); +const TokenContext = createContext({ + setAccessToken: (token: string | null) => {}, +}); + +export const AxiosProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [accessToken, setAccessToken] = useState(localStorage.getItem("accessToken")); + + useEffect(() => { + if (accessToken) { + axiosInstance.defaults.headers.Authorization = `Bearer ${accessToken}`; + SetupAxiosInterceptors(accessToken); + console.log("[debug] Updated accessToken:", accessToken); + } + }, [accessToken]); + + return ( + + + {children} + + + ); +}; + +// Custom hook to use Axios instance +export const useAxios = () => { + return useContext(AxiosContext); +}; + +// Custom hook to manage access token +export const useToken = () => { + return useContext(TokenContext); +}; \ No newline at end of file diff --git a/src/app/(main)/axios/axiosInstance.ts b/src/app/(main)/axios/axiosInstance.ts new file mode 100644 index 0000000..4a5361c --- /dev/null +++ b/src/app/(main)/axios/axiosInstance.ts @@ -0,0 +1,56 @@ +import axios from 'axios'; + +const axiosInstance = axios.create({ + baseURL: process.env.API_URL, +}); + +// Clear existing interceptors to prevent multiple registrations +const clearInterceptors = () => { + const requestInterceptor = axiosInstance.interceptors.request.use(); + const responseInterceptor = axiosInstance.interceptors.response.use(); + + if (requestInterceptor) { + axiosInstance.interceptors.request.eject(requestInterceptor); + } + + if (responseInterceptor) { + axiosInstance.interceptors.response.eject(responseInterceptor); + } +}; + +export const SetupAxiosInterceptors = (inputToken: string | null) => { + console.log("[debug] set up interceptors", inputToken); + + // Clear existing interceptors + clearInterceptors(); + + // Request interceptor + axiosInstance.interceptors.request.use( + (config) => { + // Use the token passed in or retrieve from localStorage + const token = inputToken ?? localStorage.getItem('accessToken'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => { + return Promise.reject(error); + } + ); + + console.log("[debug] set up interceptors2", inputToken); + + // Response interceptor + axiosInstance.interceptors.response.use( + (response) => { + return response; + }, + (error) => { + console.error('API Error:', error.response?.data || error); + return Promise.reject(error); + } + ); +}; + +export default axiosInstance; \ No newline at end of file diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx index b93ed10..9193ab0 100644 --- a/src/app/(main)/layout.tsx +++ b/src/app/(main)/layout.tsx @@ -6,6 +6,7 @@ import Box from "@mui/material/Box"; import { NAVIGATION_CONTENT_WIDTH } from "@/config/uiConfig"; import Stack from "@mui/material/Stack"; import Breadcrumb from "@/components/Breadcrumb"; +import {AxiosProvider} from "@/app/(main)/axios/AxiosProvider"; export default async function MainLayout({ children, @@ -19,23 +20,25 @@ export default async function MainLayout({ } return ( - <> - - - - - {children} - - - + + <> + + + + + {children} + + + + ); } diff --git a/src/components/ItemsSearch/ItemsSearch.tsx b/src/components/ItemsSearch/ItemsSearch.tsx index 8b32786..4c3a535 100644 --- a/src/components/ItemsSearch/ItemsSearch.tsx +++ b/src/components/ItemsSearch/ItemsSearch.tsx @@ -1,14 +1,17 @@ "use client"; -import { useCallback, useMemo, useState } from "react"; +import {useCallback, useEffect, useMemo, useState} from "react"; import SearchBox, { Criterion } from "../SearchBox"; -import { ItemsResult } from "@/app/api/settings/item"; +import {fetchAllItemsByPage, ItemsResult} from "@/app/api/settings/item"; import { useTranslation } from "react-i18next"; import SearchResults, { Column } from "../SearchResults"; import { EditNote } from "@mui/icons-material"; import { useRouter, useSearchParams } from "next/navigation"; import { GridDeleteIcon } from "@mui/x-data-grid"; import { TypeEnum } from "@/app/utils/typeEnum"; +import axios from "axios"; +import {BASE_API_URL, NEXT_PUBLIC_API_URL} from "@/config/api"; +import axiosInstance from "@/app/(main)/axios/axiosInstance"; type Props = { items: ItemsResult[]; @@ -20,6 +23,11 @@ const ItemsSearch: React.FC = ({ items }) => { const [filteredItems, setFilteredItems] = useState(items); const { t } = useTranslation("items"); const router = useRouter(); + const [filterObj, setFilterObj] = useState(); + const [pagingController, setPagingController] = useState({ + pageNum: 1, + pageSize: 10, + }) const searchCriteria: Criterion[] = useMemo( () => { @@ -70,27 +78,61 @@ const ItemsSearch: React.FC = ({ items }) => { [filteredItems] ); - const onReset = useCallback(() => { - setFilteredItems(items); - }, [items]); + useEffect(() => { + if (filterObj) { + refetchData(filterObj); + } + }, [filterObj, pagingController]); + + const refetchData = async (filterObj: SearchQuery) => { + // Make sure the API endpoint is correct + const params ={ + pageNum: pagingController.pageNum, + pageSize: pagingController.pageSize, + ...filterObj, + } + try { + console.log("[debug] axiosInstance", axiosInstance) + const response = await axiosInstance.get(`${NEXT_PUBLIC_API_URL}/items/getRecordByPage`, { params }); + console.log("[debug] resposne", response) + setFilteredItems(response.data.records); + return response; // Return the data from the response + } catch (error) { + console.error('Error fetching items:', error); + throw error; // Rethrow the error for further handling + } + }; + + const onReset = useCallback(() => { + setFilteredItems(items); + }, [items]); return ( <> { - setFilteredItems( - items.filter((pm) => { - return ( - pm.code.toLowerCase().includes(query.code.toLowerCase()) && - pm.name.toLowerCase().includes(query.name.toLowerCase()) - ); + // setFilteredItems( + // items.filter((pm) => { + // return ( + // pm.code.toLowerCase().includes(query.code.toLowerCase()) && + // pm.name.toLowerCase().includes(query.name.toLowerCase()) + // ); + // }) + // ); + // @ts-ignore + setFilterObj({ + ...query }) - ); }} onReset={onReset} /> - items={filteredItems} columns={columns} /> + + items={filteredItems} + columns={columns} + setPagingController={setPagingController} + pagingController={pagingController} + /> ); }; diff --git a/src/components/LoginPage/LoginForm.tsx b/src/components/LoginPage/LoginForm.tsx index f2eb47b..1110afc 100644 --- a/src/components/LoginPage/LoginForm.tsx +++ b/src/components/LoginPage/LoginForm.tsx @@ -12,6 +12,8 @@ import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import { SubmitHandler, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; +import {SetupAxiosInterceptors} from "@/app/(main)/axios/axiosInstance"; +import {useToken} from "@/app/(main)/axios/AxiosProvider"; type LoginFields = { username: string; @@ -47,6 +49,7 @@ const LoginForm: React.FC = () => { const [serverError, setServerError] = useState(); const router = useRouter(); + const { setAccessToken } = useToken(); const onSubmit: SubmitHandler = async (data) => { const res = await signIn("credentials", { @@ -62,7 +65,10 @@ const LoginForm: React.FC = () => { // set auth to local storage const session = await getSession() as SessionWithAbilities - + // @ts-ignore + window.localStorage.setItem("accessToken", session?.accessToken) + setAccessToken(session?.accessToken); + SetupAxiosInterceptors(session?.accessToken); // console.log(session) window.localStorage.setItem("abilities", JSON.stringify(session?.abilities)) diff --git a/src/components/SearchResults/SearchResults.tsx b/src/components/SearchResults/SearchResults.tsx index 3cf10b5..39ad9f6 100644 --- a/src/components/SearchResults/SearchResults.tsx +++ b/src/components/SearchResults/SearchResults.tsx @@ -48,6 +48,8 @@ function SearchResults({ items, columns, noWrapper, + pagingController, + setPagingController }: Props) { const [page, setPage] = React.useState(0); const [rowsPerPage, setRowsPerPage] = React.useState(10); @@ -57,6 +59,10 @@ function SearchResults({ newPage, ) => { setPage(newPage); + setPagingController({ + ...pagingController, + pageNum: newPage, + }) }; const handleChangeRowsPerPage: TablePaginationProps["onRowsPerPageChange"] = ( @@ -64,6 +70,10 @@ function SearchResults({ ) => { setRowsPerPage(+event.target.value); setPage(0); + setPagingController({ + ...pagingController, + pageNum: +event.target.value, + }) }; const table = ( diff --git a/src/config/api.ts b/src/config/api.ts index 376fd05..7eb5caa 100644 --- a/src/config/api.ts +++ b/src/config/api.ts @@ -1,2 +1,3 @@ export const BASE_API_URL = `${process.env.API_URL}`; export const LOGIN_API_PATH = `${BASE_API_URL}/login`; +export const NEXT_PUBLIC_API_URL= `${process.env.NEXT_PUBLIC_API_URL}`; \ No newline at end of file