diff --git a/e-commerce-app/src/api/productProjectionApi.ts b/e-commerce-app/src/api/productProjectionApi.ts new file mode 100644 index 0000000..16ae591 --- /dev/null +++ b/e-commerce-app/src/api/productProjectionApi.ts @@ -0,0 +1,29 @@ +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; +import { IGetAllProductsRequest } from '../types/slicesTypes/productsApiTypes'; +import { ISearchProductsResponse } from '../types/slicesTypes/productProjectionsApiTypes'; + +export const productProjectionApi = createApi({ + reducerPath: 'productProjectionApi', + baseQuery: fetchBaseQuery({ + baseUrl: `${process.env.REACT_APP_CTP_API_URL}/${process.env.REACT_APP_CTP_PROJECT_KEY}/product-projections`, + }), + endpoints: (build) => ({ + searchProducts: build.mutation({ + query(queryObject) { + return { + url: '/search', + method: 'GET', + params: { + limit: 12, + ...queryObject.params, + }, + headers: { + Authorization: `Bearer ${queryObject.token}`, + }, + }; + }, + }), + }), +}); + +export const { useSearchProductsMutation } = productProjectionApi; diff --git a/e-commerce-app/src/api/productsApi.ts b/e-commerce-app/src/api/productsApi.ts index 943d36b..1ca9e31 100644 --- a/e-commerce-app/src/api/productsApi.ts +++ b/e-commerce-app/src/api/productsApi.ts @@ -12,7 +12,7 @@ export const productsApi = createApi({ baseUrl: `${process.env.REACT_APP_CTP_API_URL}/${process.env.REACT_APP_CTP_PROJECT_KEY}/products`, }), endpoints: (build) => ({ - getAllProducts: build.query({ + getAllProducts: build.mutation({ query(queryObject) { return { url: '', @@ -43,4 +43,4 @@ export const productsApi = createApi({ }), }); -export const { useGetAllProductsQuery, useGetProductByIdQuery } = productsApi; +export const { useGetAllProductsMutation, useGetProductByIdQuery } = productsApi; diff --git a/e-commerce-app/src/pages/ProductsPage/ProductsPage.tsx b/e-commerce-app/src/pages/ProductsPage/ProductsPage.tsx index 8f14aaa..f36408e 100644 --- a/e-commerce-app/src/pages/ProductsPage/ProductsPage.tsx +++ b/e-commerce-app/src/pages/ProductsPage/ProductsPage.tsx @@ -5,8 +5,23 @@ import { ProductsList } from '../../components/ProductsList/ProductsList'; import SearchIcon from '@mui/icons-material/Search'; import styles from './ProductsPage.module.scss'; import ProductsFilterForm from '../../components/ProductsFilterForm/ProductsFilterForm'; +import { useDispatch } from 'react-redux'; +import { SubmitHandler, useForm } from 'react-hook-form'; +import { ISearchProductForm } from '../../types/searchProductsTypes/searchFormTypes'; +import { getQueryText, setQueryText } from '../../store/slices/queryParamsSlice'; +import { useAppSelector } from '../../store/hooks'; export const ProductsPage: React.FC = () => { + const dispatch = useDispatch(); + const searchQueryText = useAppSelector(getQueryText); + const { register, handleSubmit } = useForm({ + defaultValues: { + query: searchQueryText, + }, + }); + const submitHandler: SubmitHandler = (data) => { + dispatch(setQueryText(data.query || '')); + }; return ( @@ -15,14 +30,19 @@ export const ProductsPage: React.FC = () => { img1 - + diff --git a/e-commerce-app/src/requestsComponents/ProductsQuery/ProductsQuery.tsx b/e-commerce-app/src/requestsComponents/ProductsQuery/ProductsQuery.tsx index 45a44f2..df4380b 100644 --- a/e-commerce-app/src/requestsComponents/ProductsQuery/ProductsQuery.tsx +++ b/e-commerce-app/src/requestsComponents/ProductsQuery/ProductsQuery.tsx @@ -1,40 +1,114 @@ -import React, { JSX, useEffect } from 'react'; -import { useGetAllProductsQuery } from '../../api/productsApi'; +import React, { JSX, useEffect, useState } from 'react'; +import { useGetAllProductsMutation } from '../../api/productsApi'; import { useAppDispatch, useAppSelector } from '../../store/hooks'; import { getAccessToken } from '../../store/slices/userSlice'; import LoadingProgress from '../../components/LoadingProgress/LoadingProgress'; import { ProductsPage } from '../../pages/ProductsPage/ProductsPage'; import { setProducts, startLoadingProducts } from '../../store/slices/productsSlice'; +import { getQueryLimit, getQueryOffset, getQueryText } from '../../store/slices/queryParamsSlice'; +import { IBaseQueryParams } from '../../types/slicesTypes/baseApiRequestsTypes'; +import { useSearchProductsMutation } from '../../api/productProjectionApi'; +import { makeProductSliceObjectFromSearchApiRequest } from '../../utils/makeProductSliceObjectFromSearchApiRequest'; const ProductsQuery = (): JSX.Element => { - const accessToken = useAppSelector(getAccessToken); const dispatch = useAppDispatch(); + const accessToken = useAppSelector(getAccessToken); + const searchQueryText = useAppSelector(getQueryText); + const searchQueryLimit = useAppSelector(getQueryLimit); + const searchQueryOffset = useAppSelector(getQueryOffset); - const { isLoading, isSuccess, isError, data } = useGetAllProductsQuery({ - token: accessToken as string, + const [params, setParams] = useState({ + limit: searchQueryLimit || 12, }); useEffect(() => { - if (isLoading) { + if (searchQueryOffset) { + setParams((prevState) => ({ + ...prevState, + offset: searchQueryOffset, + })); + } + }, [searchQueryOffset]); + + useEffect(() => { + if (searchQueryText) { + setParams((prevState) => ({ + ...prevState, + ['text.en']: searchQueryText, + fuzzy: searchQueryText.length > 2, + fuzzyLevel: searchQueryText.length <= 2 ? 0 : 1, + })); + } else { + setParams((prevState) => { + const newObj = { + ...prevState, + }; + delete newObj['text.en']; + delete newObj.fuzzyLevel; + delete newObj.fuzzy; + return newObj; + }); + } + }, [searchQueryText]); + + const [ + getAllProducts, + { + isLoading: isLoadingProducts, + isSuccess: isSuccessProducts, + isError: isErrorProducts, + data: dataProducts, + }, + ] = useGetAllProductsMutation(); + const [ + searchProducts, + { + isLoading: isLoadingSearch, + isSuccess: isSuccessSearch, + isError: isErrorSearch, + data: dataSearch, + }, + ] = useSearchProductsMutation(); + + useEffect(() => { + if (params['text.en']?.length && params['text.en']?.length > 0) { + searchProducts({ + token: accessToken as string, + params, + }); + } else { + getAllProducts({ + token: accessToken as string, + params, + }); + } + }, [params['text.en']]); + + useEffect(() => { + if (isLoadingProducts || isLoadingSearch) { dispatch(startLoadingProducts()); } - }, [isLoading]); + }, [isLoadingProducts, isLoadingSearch]); useEffect(() => { - if (!isSuccess) return; - if (data && 'results' in data) { - dispatch(setProducts(data)); + if (isSuccessSearch && params['text.en']) { + if (dataSearch && 'results' in dataSearch) { + const pushingObject = makeProductSliceObjectFromSearchApiRequest(dataSearch); + dispatch(setProducts(pushingObject)); + } + return; } - }, [isSuccess, data]); + if (isSuccessProducts) { + if (dataProducts && 'results' in dataProducts) { + dispatch(setProducts(dataProducts)); + } + } + }, [isSuccessProducts, dataProducts, isSuccessSearch, dataSearch]); - if (isLoading || isError) { + if (isLoadingProducts || isErrorProducts || isLoadingSearch || isErrorSearch) { return ; } - if (isSuccess) { - return ; - } - - return ; + return ; }; export default ProductsQuery; diff --git a/e-commerce-app/src/routes/AppRoutes.tsx b/e-commerce-app/src/routes/AppRoutes.tsx index becdebf..9841544 100644 --- a/e-commerce-app/src/routes/AppRoutes.tsx +++ b/e-commerce-app/src/routes/AppRoutes.tsx @@ -33,7 +33,7 @@ const router = createHashRouter( } /> }> }> - {/*TODO : add redirect to the user page */} + {/* TODO : add redirect to the user page */} }> diff --git a/e-commerce-app/src/store/slices/productsSlice.ts b/e-commerce-app/src/store/slices/productsSlice.ts index 64f568e..e3cbaf2 100644 --- a/e-commerce-app/src/store/slices/productsSlice.ts +++ b/e-commerce-app/src/store/slices/productsSlice.ts @@ -27,10 +27,18 @@ export const productsSlice = createSlice({ state.products = action.payload.results; state.fetching = false; }, + resetProducts: (state) => { + state.fetching = false; + state.products = []; + state.total = 0; + state.limit = 0; + state.offset = 0; + state.count = 0; + }, }, }); export const getProducts = (state: RootStateType) => state.products.products; export const isFetchingProducts = (state: RootStateType) => state.products.fetching; export const ProductsReducer = productsSlice.reducer; -export const { startLoadingProducts, setProducts } = productsSlice.actions; +export const { startLoadingProducts, setProducts, resetProducts } = productsSlice.actions; diff --git a/e-commerce-app/src/store/slices/queryParamsSlice.ts b/e-commerce-app/src/store/slices/queryParamsSlice.ts new file mode 100644 index 0000000..1543e80 --- /dev/null +++ b/e-commerce-app/src/store/slices/queryParamsSlice.ts @@ -0,0 +1,33 @@ +import { IQueryParamsFromSlice } from '../../types/slicesTypes/queryParamsSliceTypes'; +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { RootStateType } from '../store'; + +const initialState: IQueryParamsFromSlice = { + limit: 12, + offset: 0, + sort: '', + text: '', +}; + +export const queryParamsSlice = createSlice({ + initialState, + name: 'queryParamsSlice', + reducers: { + setQueryOffset: (state, action: PayloadAction) => { + state.offset = action.payload; + }, + setQuerySort: (state, action: PayloadAction) => { + state.sort = action.payload; + }, + setQueryText: (state, action: PayloadAction) => { + state.text = action.payload; + } + } +}); + +export const getQueryOffset = (state: RootStateType) => state.queryParams.offset; +export const getQuerySort = (state: RootStateType) => state.queryParams.sort; +export const getQueryText = (state: RootStateType) => state.queryParams.text; +export const getQueryLimit = (state: RootStateType) => state.queryParams.limit; +export const QueryParamsReducer = queryParamsSlice.reducer; +export const { setQueryOffset, setQuerySort, setQueryText } = queryParamsSlice.actions; diff --git a/e-commerce-app/src/store/store.ts b/e-commerce-app/src/store/store.ts index 581afb0..8aaf9d5 100644 --- a/e-commerce-app/src/store/store.ts +++ b/e-commerce-app/src/store/store.ts @@ -6,9 +6,12 @@ import { productsApi } from '../api/productsApi'; import { categoriesApi } from '../api/categoriesApi'; import { CategoriesReducer } from './slices/categoriesSlice'; import { ProductsReducer } from './slices/productsSlice'; +import { productProjectionApi } from '../api/productProjectionApi'; +import { QueryParamsReducer } from './slices/queryParamsSlice'; import { taxApi } from '../api/taxApi'; import { TaxesReducer } from './slices/taxesSlice'; + export const store = configureStore({ reducer: { [authApi.reducerPath]: authApi.reducer, @@ -16,9 +19,11 @@ export const store = configureStore({ [productsApi.reducerPath]: productsApi.reducer, [categoriesApi.reducerPath]: categoriesApi.reducer, [taxApi.reducerPath]: taxApi.reducer, + [productProjectionApi.reducerPath]: productProjectionApi.reducer, user: UserReducer, categories: CategoriesReducer, products: ProductsReducer, + queryParams: QueryParamsReducer, taxes: TaxesReducer, }, middleware: (getDefaultMiddleware) => @@ -27,6 +32,7 @@ export const store = configureStore({ myCustomerApi.middleware, productsApi.middleware, categoriesApi.middleware, + productProjectionApi.middleware, taxApi.middleware, ]), }); diff --git a/e-commerce-app/src/types/searchProductsTypes/searchFormTypes.ts b/e-commerce-app/src/types/searchProductsTypes/searchFormTypes.ts new file mode 100644 index 0000000..d350a41 --- /dev/null +++ b/e-commerce-app/src/types/searchProductsTypes/searchFormTypes.ts @@ -0,0 +1,3 @@ +export interface ISearchProductForm { + query: string +} diff --git a/e-commerce-app/src/types/slicesTypes/baseApiRequestsTypes.ts b/e-commerce-app/src/types/slicesTypes/baseApiRequestsTypes.ts new file mode 100644 index 0000000..865044c --- /dev/null +++ b/e-commerce-app/src/types/slicesTypes/baseApiRequestsTypes.ts @@ -0,0 +1,7 @@ +export interface IBaseQueryParams { + limit?: number; + offset?: number; + ['text.en']?: string; + fuzzy?: boolean; + fuzzyLevel?: 0 | 1 | 2 +} diff --git a/e-commerce-app/src/types/slicesTypes/productProjectionsApiTypes.ts b/e-commerce-app/src/types/slicesTypes/productProjectionsApiTypes.ts new file mode 100644 index 0000000..2d9dca2 --- /dev/null +++ b/e-commerce-app/src/types/slicesTypes/productProjectionsApiTypes.ts @@ -0,0 +1,13 @@ +import { IBaseGetAllQueryResponse, ICategoryTypeResponse } from './baseApiResponsesTypes'; +import { IProductApiDescriptionResponse } from './productsApiTypes'; + +export interface ISearchApiResponse extends IProductApiDescriptionResponse { + id: string; + key: string; + productType: ICategoryTypeResponse; + taxCategory: ICategoryTypeResponse; +} + +export interface ISearchProductsResponse extends IBaseGetAllQueryResponse{ + results: ISearchApiResponse[]; +} diff --git a/e-commerce-app/src/types/slicesTypes/productsApiTypes.ts b/e-commerce-app/src/types/slicesTypes/productsApiTypes.ts index 7a0de3e..6ebce6c 100644 --- a/e-commerce-app/src/types/slicesTypes/productsApiTypes.ts +++ b/e-commerce-app/src/types/slicesTypes/productsApiTypes.ts @@ -3,6 +3,7 @@ import { ICategoryTypeResponse, IMetaDescriptionProductResponse, } from './baseApiResponsesTypes'; +import { IBaseQueryParams } from './baseApiRequestsTypes'; export interface IAttributeProductApiResponse { name: string; @@ -73,10 +74,7 @@ export interface IGetAllProductsResponse extends IBaseGetAllQueryResponse { + const current: IProductApiDescriptionResponse = { + categories: el.categories, + categoryOrderHints: el.categoryOrderHints, + masterVariant: el.masterVariant, + metaDescription: el.metaDescription, + name: el.name, + searchKeywords: el.searchKeywords, + slug: el.slug, + variants: el.variants + }; + const masterData: IMasterDataProductApiResponse = { + current: current, + hasStagedChanges: false, + published: true, + staged: current, + }; + const newProduct: IProductApiResponse = { + id: el.id, + key: el.key, + masterData: masterData, + productType: el.productType, + taxCategory: el.taxCategory + }; + return newProduct; +}; + +export const makeProductSliceObjectFromSearchApiRequest = (dataSearch: ISearchProductsResponse): IGetAllProductsResponse => { + const productsArray = dataSearch.results.map(el => makeFromSearchApiProductApiResponses(el)); + const pushingObject: IGetAllProductsResponse = { + limit: dataSearch.limit, + offset: dataSearch.offset, + count: dataSearch.count, + total: dataSearch.total, + results: productsArray, + }; + return pushingObject; +};